feat: update core API, database layer, and worker modules
- Remove unused imports (n8n_search, universal_search, Client, Arc, etc.) - Update API endpoints for identity, face recognition, search - Fix postgres_db.rs search_videos parent_uuid column - Add snapshot API and identity agent API - Clean up backup files (.bak, .bak2)
This commit is contained in:
@@ -1,230 +0,0 @@
|
||||
# Momentry Core API 存取指南
|
||||
|
||||
| 項目 | 內容 |
|
||||
|------|------|
|
||||
| 版本 | V1.3 |
|
||||
| 日期 | 2026-03-25 |
|
||||
| 用途 | API 存取方式、端點與整合指南 |
|
||||
|
||||
---
|
||||
|
||||
## 版本歷史
|
||||
|
||||
| 版本 | 日期 | 目的 | 操作人 | 工具/模型 |
|
||||
|------|------|------|--------|-----------|
|
||||
| V1.3 | 2026-03-25 | 更新: n8n 搜尋回傳 `file_path` 取代 `media_url`,新增 API Key 驗證說明 | OpenCode | deepseek-reasoner |
|
||||
| V1.2 | 2026-03-24 | 更新網址與服務列表 | Warren | OpenCode / MiniMax M2.5 |
|
||||
| V1.1 | 2026-03-23 | 初始版本 | Warren | OpenCode / MiniMax M2.5 |
|
||||
|
||||
---
|
||||
|
||||
## 基本網址
|
||||
|
||||
| 環境 | URL | 說明 |
|
||||
|------|-----|------|
|
||||
| **本地開發** | `http://localhost:3002` | 直接訪問 API,繞過反向代理 |
|
||||
| **外部訪問** | `https://api.momentry.ddns.net` | 通過 Caddy 反向代理訪問,需網路可達 |
|
||||
|
||||
### 何時使用哪個 URL
|
||||
|
||||
**使用 `localhost:3002`:**
|
||||
- 開發/測試環境
|
||||
- 直接在伺服器上操作
|
||||
- 當反向代理有問題時
|
||||
|
||||
**使用 `api.momentry.ddns.net`:**
|
||||
- n8n workflow 中呼叫 API
|
||||
- 外部系統整合
|
||||
- 生產環境
|
||||
|
||||
## 認證
|
||||
所有 `/api/v1/*` 端點(除了健康檢查 `/health` 與 `/health/detailed`)都需要 API Key 認證。
|
||||
|
||||
請在請求標頭中加入:
|
||||
```
|
||||
X-API-Key: YOUR_API_KEY
|
||||
```
|
||||
|
||||
**目前示範使用的 API Key**: `demo_api_key_12345`
|
||||
|
||||
> **注意**: 正式環境請使用安全的 API Key 管理機制,避免在客戶端暴露 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,
|
||||
"file_path": "/Users/accusys/momentry/var/sftpgo/data/demo/Old_Time_Movie_Show_-_Charade_1963.HD.mov"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
> **注意**: API 現在返回 `file_path`(檔案系統路徑)而非 `media_url`(網頁 URL)。如需在網頁中播放影片,請將檔案路徑轉換為可訪問的 URL(例如透過 SFTPGo 分享連結)。
|
||||
|
||||
---
|
||||
|
||||
## 影片管理 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',
|
||||
'X-API-Key': 'YOUR_API_KEY' // 替換為實際的 API Key
|
||||
},
|
||||
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',
|
||||
'X-API-Key: YOUR_API_KEY' // 替換為實際的 API Key
|
||||
]);
|
||||
$response = curl_exec($ch);
|
||||
$data = json_decode($response, true);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 影片嵌入網址
|
||||
|
||||
> **重要**: API 現在返回 `file_path`(檔案系統路徑),而非直接可訪問的網址。您需要將檔案路徑轉換為 SFTPGo 分享連結才能嵌入影片。
|
||||
|
||||
**檔案路徑轉換為網址:**
|
||||
- API 返回的 `file_path` 範例:`/Users/accusys/momentry/var/sftpgo/data/demo/video.mp4`
|
||||
- 對應的 SFTPGo 分享連結:`https://wp.momentry.ddns.net/demo/video.mp4`
|
||||
- 轉換方式:移除 `/Users/accusys/momentry/var/sftpgo/data/` 前綴,將剩餘路徑附加到 `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 個(句子 + 場景 + 時間)
|
||||
@@ -1,586 +0,0 @@
|
||||
# Momentry API 使用說明 (curl 範例)
|
||||
|
||||
| 項目 | 內容 |
|
||||
|------|------|
|
||||
| 版本 | V1.4 |
|
||||
| 日期 | 2026-03-26 |
|
||||
| Base URL | `http://localhost:3002` |
|
||||
|
||||
---
|
||||
|
||||
## 版本歷史
|
||||
|
||||
| 版本 | 日期 | 目的 | 操作人 | 工具/模型 |
|
||||
|------|------|------|--------|-----------|
|
||||
| V1.4 | 2026-03-26 | 新增: 任務管理端點 (`/api/v1/jobs`, `/api/v1/jobs/:uuid`),更新註冊端點回應格式 | OpenCode | deepseek-reasoner |
|
||||
| V1.3 | 2026-03-25 | 更新: n8n 搜尋回傳 `file_path` 取代 `media_url`,新增 API Key 驗證說明 | OpenCode | deepseek-reasoner |
|
||||
| V1.2 | 2026-03-23 | 建立 curl 範例文件 | Warren | OpenCode / MiniMax M2.5 |
|
||||
| V1.1 | 2026-03-18 | 創建文件 | Warren | OpenCode / MiniMax M2.5 |
|
||||
|
||||
---
|
||||
|
||||
> **狀態說明**:
|
||||
> - ✅ **已實作**: 健康檢查、搜尋、影片管理端點
|
||||
> - ⚠️ **規劃中**: 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
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## API 認證
|
||||
|
||||
所有 `/api/v1/*` 端點(除了健康檢查)都需要 API Key 認證。請在請求標頭中加入:
|
||||
|
||||
```
|
||||
-H "X-API-Key: YOUR_API_KEY"
|
||||
```
|
||||
|
||||
**目前示範使用的 API Key**: `demo_api_key_12345`
|
||||
|
||||
> **注意**: 正式環境請使用安全的 API Key 管理機制。
|
||||
|
||||
---
|
||||
|
||||
## 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" \
|
||||
-H "X-API-Key: YOUR_API_KEY" \
|
||||
-d '{"path": "/path/to/video.mp4"}'
|
||||
```
|
||||
|
||||
**回應範例**:
|
||||
|
||||
```json
|
||||
{
|
||||
"uuid": "a1b2c3d4e5f6g7h8",
|
||||
"video_id": 1,
|
||||
"job_id": 123,
|
||||
"file_name": "video.mp4",
|
||||
"duration": 120.5,
|
||||
"width": 1920,
|
||||
"height": 1080,
|
||||
"already_exists": false
|
||||
}
|
||||
```
|
||||
|
||||
### 3.2 列出所有影片 ✅
|
||||
|
||||
```bash
|
||||
curl -H "X-API-Key: YOUR_API_KEY" http://localhost:3002/api/v1/videos
|
||||
```
|
||||
|
||||
### 3.3 查詢影片 ✅
|
||||
|
||||
```bash
|
||||
# 依 UUID 查詢
|
||||
curl -H "X-API-Key: YOUR_API_KEY" "http://localhost:3002/api/v1/lookup?uuid=a1b2c3d4e5f6g7h8"
|
||||
|
||||
# 依路徑查詢
|
||||
curl -H "X-API-Key: YOUR_API_KEY" "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 <uuid>
|
||||
|
||||
# 或處理多個影片
|
||||
cargo run --bin momentry -- process <uuid1> <uuid2> <uuid3>
|
||||
```
|
||||
|
||||
### 3.5 取得處理進度 ✅
|
||||
|
||||
```bash
|
||||
curl -H "X-API-Key: YOUR_API_KEY" http://localhost:3002/api/v1/progress/<uuid>
|
||||
```
|
||||
|
||||
**回應範例**:
|
||||
|
||||
```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"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### 3.6 任務管理 ✅
|
||||
|
||||
```bash
|
||||
# 列出所有任務
|
||||
curl -H "X-API-Key: YOUR_API_KEY" http://localhost:3002/api/v1/jobs
|
||||
|
||||
# 取得特定任務詳情
|
||||
curl -H "X-API-Key: YOUR_API_KEY" http://localhost:3002/api/v1/jobs/<uuid>
|
||||
```
|
||||
|
||||
**任務列表回應範例**:
|
||||
```json
|
||||
{
|
||||
"jobs": [
|
||||
{
|
||||
"id": 123,
|
||||
"uuid": "a1b2c3d4e5f6g7h8",
|
||||
"status": "pending",
|
||||
"current_processor": null,
|
||||
"progress_current": 0,
|
||||
"progress_total": 100,
|
||||
"created_at": "2026-03-26 10:30:00",
|
||||
"started_at": null
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**任務詳情回應範例**:
|
||||
```json
|
||||
{
|
||||
"id": 123,
|
||||
"uuid": "a1b2c3d4e5f6g7h8",
|
||||
"status": "processing",
|
||||
"current_processor": "asr",
|
||||
"progress_current": 50,
|
||||
"progress_total": 100,
|
||||
"processors": [
|
||||
{
|
||||
"processor_type": "asr",
|
||||
"status": "complete",
|
||||
"started_at": "2026-03-26 10:30:00",
|
||||
"completed_at": "2026-03-26 10:35:00",
|
||||
"duration_secs": 300.5,
|
||||
"error_message": null
|
||||
},
|
||||
{
|
||||
"processor_type": "cut",
|
||||
"status": "pending",
|
||||
"started_at": null,
|
||||
"completed_at": null,
|
||||
"duration_secs": null,
|
||||
"error_message": null
|
||||
}
|
||||
],
|
||||
"created_at": "2026-03-26 10:30:00",
|
||||
"started_at": "2026-03-26 10:30:00",
|
||||
"updated_at": "2026-03-26 10:35:00"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. 查詢與搜索
|
||||
|
||||
### 4.1 語意搜尋 ✅
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:3002/api/v1/search \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "X-API-Key: YOUR_API_KEY" \
|
||||
-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" \
|
||||
-H "X-API-Key: YOUR_API_KEY" \
|
||||
-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,
|
||||
"file_path": "/Users/accusys/momentry/var/sftpgo/data/demo/video.mp4"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### 4.3 混合搜尋 ✅
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:3002/api/v1/search/hybrid \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "X-API-Key: YOUR_API_KEY" \
|
||||
-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/jobs` | GET | ✅ | 任務列表 |
|
||||
| `/api/v1/jobs/: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" \
|
||||
-H "X-API-Key: YOUR_API_KEY" \
|
||||
-d '{"query": "test", "limit": 3}' | jq .
|
||||
|
||||
# 列出影片
|
||||
echo -e "\n=== Videos ==="
|
||||
curl -s -H "X-API-Key: YOUR_API_KEY" "$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 使用範例
|
||||
@@ -1,321 +0,0 @@
|
||||
# Momentry Core API 端點總覽
|
||||
|
||||
| 項目 | 內容 |
|
||||
|------|------|
|
||||
| 建立者 | Warren |
|
||||
| 建立時間 | 2026-03-18 |
|
||||
| 文件版本 | V1.3 |
|
||||
|
||||
---
|
||||
|
||||
## 版本歷史
|
||||
|
||||
| 版本 | 日期 | 目的 | 操作人 |
|
||||
|------|------|------|--------|
|
||||
| V1.0 | 2026-03-18 | 創建文件 | OpenCode |
|
||||
| V1.1 | 2026-03-23 | 更新端點與實際一致 | OpenCode |
|
||||
| V1.2 | 2026-03-25 | 新增快取/刪除 API | OpenCode |
|
||||
| V1.3 | 2026-03-26 | 更新API回應格式 (media_url→file_path) | OpenCode |
|
||||
|
||||
---
|
||||
|
||||
## Base URL
|
||||
|
||||
| 環境 | URL |
|
||||
|------|-----|
|
||||
| 本地 | `http://localhost:3002` |
|
||||
| 外部 | `https://api.momentry.ddns.net` |
|
||||
|
||||
---
|
||||
|
||||
## 認證
|
||||
|
||||
除健康檢查端點外,所有 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
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 端點列表
|
||||
|
||||
### 健康檢查(公開)
|
||||
|
||||
| 方法 | 端點 | 說明 |
|
||||
|------|------|------|
|
||||
| GET | `/health` | 基本健康檢查 |
|
||||
| GET | `/health/detailed` | 詳細健康檢查(含服務狀態) |
|
||||
|
||||
**範例**:
|
||||
```bash
|
||||
curl http://localhost:3002/health
|
||||
# {"status":"ok","version":"0.1.0","uptime_ms":123456}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 影片搜尋
|
||||
|
||||
| 方法 | 端點 | 說明 |
|
||||
|------|------|------|
|
||||
| POST | `/api/v1/search` | 語意搜尋(標準格式) |
|
||||
| POST | `/api/v1/n8n/search` | 語意搜尋(n8n 專用格式) |
|
||||
| POST | `/api/v1/search/hybrid` | 混合搜尋 |
|
||||
|
||||
**標準搜尋** (`/api/v1/search`):
|
||||
```bash
|
||||
curl -X POST http://localhost:3002/api/v1/search \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "X-API-Key: your-api-key" \
|
||||
-d '{"query": "test", "limit": 10}'
|
||||
```
|
||||
|
||||
**n8n 格式搜尋** (`/api/v1/n8n/search`):
|
||||
```bash
|
||||
curl -X POST http://localhost:3002/api/v1/n8n/search \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "X-API-Key: your-api-key" \
|
||||
-d '{"query": "test", "limit": 10}'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 影片管理
|
||||
|
||||
| 方法 | 端點 | 說明 |
|
||||
|------|------|------|
|
||||
| POST | `/api/v1/register` | 註冊影片 |
|
||||
| POST | `/api/v1/probe` | 探測影片資訊(不註冊) |
|
||||
| GET | `/api/v1/videos` | 列出所有影片 |
|
||||
| GET | `/api/v1/lookup` | 查詢影片資訊 |
|
||||
| GET | `/api/v1/progress/:uuid` | 取得處理進度 |
|
||||
|
||||
**註冊影片**:
|
||||
```bash
|
||||
curl -X POST http://localhost:3002/api/v1/register \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "X-API-Key: your-api-key" \
|
||||
-d '{"path": "/path/to/video.mp4"}'
|
||||
```
|
||||
|
||||
**註冊回應範例**:
|
||||
```json
|
||||
{
|
||||
"uuid": "a1b10138a6bbb0cd",
|
||||
"video_id": 1,
|
||||
"job_id": 10,
|
||||
"file_name": "video.mp4",
|
||||
"duration": 120.5,
|
||||
"width": 1920,
|
||||
"height": 1080,
|
||||
"already_exists": false
|
||||
}
|
||||
```
|
||||
|
||||
**探測影片** (不註冊,只取得影片資訊):
|
||||
```bash
|
||||
curl -X POST http://localhost:3002/api/v1/probe \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "X-API-Key: your-api-key" \
|
||||
-d '{"path": "./demo/video.mp4"}'
|
||||
```
|
||||
|
||||
**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"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**列出影片**:
|
||||
```bash
|
||||
curl -H "X-API-Key: your-api-key" http://localhost:3002/api/v1/videos
|
||||
```
|
||||
|
||||
**查詢影片**:
|
||||
```bash
|
||||
curl -H "X-API-Key: your-api-key" "http://localhost:3002/api/v1/lookup?uuid=5dea6618a606e7c7"
|
||||
```
|
||||
|
||||
**處理進度**:
|
||||
```bash
|
||||
curl -H "X-API-Key: your-api-key" http://localhost:3002/api/v1/progress/5dea6618a606e7c7
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 工作管理
|
||||
|
||||
| 方法 | 端點 | 說明 |
|
||||
|------|------|------|
|
||||
| GET | `/api/v1/jobs` | 列出所有工作 |
|
||||
| GET | `/api/v1/jobs/:uuid` | 取得指定工作的詳細資訊 |
|
||||
|
||||
**列出工作**:
|
||||
```bash
|
||||
curl -H "X-API-Key: your-api-key" http://localhost:3002/api/v1/jobs
|
||||
```
|
||||
|
||||
**取得工作詳細資訊**:
|
||||
```bash
|
||||
curl -H "X-API-Key: your-api-key" http://localhost:3002/api/v1/jobs/a03485a40b2df2d3
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 系統管理
|
||||
|
||||
| 方法 | 端點 | 說明 |
|
||||
|------|------|------|
|
||||
| POST | `/api/v1/config/cache` | 切換快取功能(管理員) |
|
||||
| POST | `/api/v1/unregister` | 刪除影片及其所有資料(管理員) |
|
||||
|
||||
**快取設定**:
|
||||
```bash
|
||||
curl -X POST http://localhost:3002/api/v1/config/cache \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "X-API-Key: your-api-key" \
|
||||
-d '{"enabled": true}'
|
||||
```
|
||||
|
||||
**刪除影片**:
|
||||
```bash
|
||||
curl -X POST http://localhost:3002/api/v1/unregister \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "X-API-Key: your-api-key" \
|
||||
-d '{"uuid": "5dea6618a606e7c7"}'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 端點對照表
|
||||
|
||||
| 功能 | n8n 使用 | WordPress 使用 | curl 測試 |
|
||||
|------|-----------|----------------|------------|
|
||||
| 健康檢查 | ✓ | ✓ | ✓ |
|
||||
| 語意搜尋 | ✓ (n8n格式) | ✓ (標準格式) | ✓ |
|
||||
| 影片探測 | ✓ | ✓ | ✓ |
|
||||
| 註冊影片 | ✓ | ✓ | ✓ |
|
||||
| 列出影片 | ✓ | ✓ | ✓ |
|
||||
| 查詢影片 | ✓ | ✓ | ✓ |
|
||||
| 處理進度 | ✓ | ✓ | ✓ |
|
||||
| 工作管理 | ✓ | ✓ | ✓ |
|
||||
| 快取設定 | ✓ (管理員) | ✓ (管理員) | ✓ (管理員) |
|
||||
| 刪除影片 | ✓ (管理員) | ✓ (管理員) | ✓ (管理員) |
|
||||
|
||||
---
|
||||
|
||||
## 回應格式
|
||||
|
||||
### 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": "...",
|
||||
"score": 0.92,
|
||||
"file_path": "/Users/accusys/momentry/var/sftpgo/data/demo/video.mp4"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### 標準格式 (`/api/v1/search`)
|
||||
```json
|
||||
{
|
||||
"results": [
|
||||
{
|
||||
"uuid": "a1b10138a6bbb0cd",
|
||||
"chunk_id": "sentence_0001",
|
||||
"chunk_type": "sentence",
|
||||
"start_time": 48.8,
|
||||
"end_time": 55.44,
|
||||
"text": "...",
|
||||
"score": 0.92
|
||||
}
|
||||
],
|
||||
"query": "charade"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## HTTP 狀態碼
|
||||
|
||||
| 狀態 | 說明 |
|
||||
|------|------|
|
||||
| 200 | 成功 |
|
||||
| 400 | 請求格式錯誤 |
|
||||
| 404 | 端點或資源不存在 |
|
||||
| 500 | 伺服器內部錯誤 |
|
||||
| 502 | API 服務未啟動 |
|
||||
|
||||
---
|
||||
|
||||
## 錯誤處理
|
||||
|
||||
### 502 Bad Gateway
|
||||
|
||||
**原因**: Momentry API 服務未啟動
|
||||
|
||||
**解決**:
|
||||
```bash
|
||||
sudo launchctl load /Library/LaunchDaemons/com.momentry.api.plist
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 相關文件
|
||||
|
||||
- [API_INDEX.md](./API_INDEX.md) - 文件總覽(起點)
|
||||
- [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 範例
|
||||
@@ -1,771 +0,0 @@
|
||||
# Momentry Core API 使用範例總覽
|
||||
|
||||
| 項目 | 內容 |
|
||||
|------|------|
|
||||
| 版本 | V2.1 |
|
||||
| 日期 | 2026-03-26 |
|
||||
| Base URL (本地) | `http://localhost:3002` |
|
||||
| Base URL (外部) | `https://api.momentry.ddns.net` |
|
||||
|
||||
---
|
||||
|
||||
## 版本歷史
|
||||
|
||||
| 版本 | 日期 | 目的 | 操作人 |
|
||||
|------|------|------|--------|
|
||||
| V2.0 | 2026-03-25 | 創建完整範例總覽 | OpenCode |
|
||||
| V2.1 | 2026-03-26 | 更新API回應格式 (media_url→file_path) 與認證標頭 | OpenCode |
|
||||
|
||||
---
|
||||
|
||||
## 快速參考
|
||||
|
||||
### 環境 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" \
|
||||
-H "X-API-Key: YOUR_API_KEY" \
|
||||
-d '{"query": "charade", "limit": 5}'
|
||||
|
||||
# n8n 格式搜尋(推薦)
|
||||
curl -X POST http://localhost:3002/api/v1/n8n/search \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "X-API-Key: YOUR_API_KEY" \
|
||||
-d '{"query": "charade", "limit": 5}'
|
||||
|
||||
# 混合搜尋
|
||||
curl -X POST http://localhost:3002/api/v1/search/hybrid \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "X-API-Key: YOUR_API_KEY" \
|
||||
-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,
|
||||
"file_path": "/Users/accusys/momentry/var/sftpgo/data/demo/video.mp4"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### 1.3 影片管理
|
||||
|
||||
```bash
|
||||
# 列出所有影片
|
||||
curl -H "X-API-Key: YOUR_API_KEY" http://localhost:3002/api/v1/videos
|
||||
|
||||
# 查詢特定影片(依 UUID)
|
||||
curl -H "X-API-Key: YOUR_API_KEY" "http://localhost:3002/api/v1/lookup?uuid=a1b10138a6bbb0cd"
|
||||
|
||||
# 查詢特定影片(依路徑)
|
||||
curl -H "X-API-Key: YOUR_API_KEY" "http://localhost:3002/api/v1/lookup?path=/path/to/video.mp4"
|
||||
|
||||
# 取得處理進度
|
||||
curl -H "X-API-Key: YOUR_API_KEY" http://localhost:3002/api/v1/progress/a1b10138a6bbb0cd
|
||||
|
||||
# 探測影片(不註冊)
|
||||
curl -X POST http://localhost:3002/api/v1/probe \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "X-API-Key: YOUR_API_KEY" \
|
||||
-d '{"path": "/path/to/video.mp4"}'
|
||||
|
||||
# 註冊影片
|
||||
curl -X POST http://localhost:3002/api/v1/register \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "X-API-Key: YOUR_API_KEY" \
|
||||
-d '{"path": "/path/to/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" \
|
||||
-H "X-API-Key: YOUR_API_KEY" \
|
||||
-d '{"query": "charade", "limit": 3}' | jq .
|
||||
|
||||
echo -e "\n=== 影片列表 ==="
|
||||
curl -s -H "X-API-Key: YOUR_API_KEY" "$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" \
|
||||
-H "X-API-Key: YOUR_API_KEY" \
|
||||
-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 }}"
|
||||
│ }
|
||||
├── Send Headers: ✓ (checked)
|
||||
└── Header Parameters:
|
||||
└── X-API-Key: {{ $env.MOMENTRY_API_KEY }}
|
||||
```
|
||||
|
||||
### 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
|
||||
<?php
|
||||
// 搜尋影片
|
||||
$api_url = 'https://api.momentry.ddns.net/api/v1/n8n/search';
|
||||
|
||||
$data = [
|
||||
'query' => '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
|
||||
<?php
|
||||
$api_url = 'https://api.momentry.ddns.net/api/v1/videos';
|
||||
|
||||
$response = wp_remote_get($api_url, ['timeout' => 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
|
||||
<?php
|
||||
$uuid = 'a1b10138a6bbb0cd';
|
||||
$api_url = 'https://api.momentry.ddns.net/api/v1/lookup?uuid=' . $uuid;
|
||||
|
||||
$response = wp_remote_get($api_url, ['timeout' => 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
|
||||
<?php
|
||||
// 將文件路徑轉換為可訪問的 URL
|
||||
function convert_file_path_to_url($file_path) {
|
||||
// 範例: 將 SFTPGo 文件路徑轉換為 web URL
|
||||
// /Users/accusys/momentry/var/sftpgo/data/demo/video.mp4
|
||||
// → https://sftpgo.example.com/demo/video.mp4
|
||||
|
||||
// 移除基本路徑
|
||||
$base_path = '/Users/accusys/momentry/var/sftpgo/data/';
|
||||
if (strpos($file_path, $base_path) === 0) {
|
||||
$relative_path = substr($file_path, strlen($base_path));
|
||||
// 替換為實際的 SFTPGo web URL
|
||||
return 'https://sftpgo.example.com/' . $relative_path;
|
||||
}
|
||||
|
||||
// 如果無法轉換,返回原始路徑
|
||||
return $file_path;
|
||||
}
|
||||
|
||||
// 註冊短碼
|
||||
add_shortcode('momentry_search', function($atts) {
|
||||
$atts = shortcode_atts([
|
||||
'query' => '',
|
||||
'limit' => '10'
|
||||
], $atts);
|
||||
|
||||
if (empty($atts['query'])) {
|
||||
return '<p>請提供搜尋關鍵字</p>';
|
||||
}
|
||||
|
||||
$response = wp_remote_post('https://api.momentry.ddns.net/api/v1/n8n/search', [
|
||||
'headers' => [
|
||||
'Content-Type' => 'application/json',
|
||||
'X-API-Key' => 'YOUR_API_KEY' // 替換為實際的 API Key
|
||||
],
|
||||
'body' => json_encode([
|
||||
'query' => $atts['query'],
|
||||
'limit' => (int)$atts['limit']
|
||||
]),
|
||||
'timeout' => 30
|
||||
]);
|
||||
|
||||
if (is_wp_error($response)) {
|
||||
return '<p>搜尋服務暫時無法使用</p>';
|
||||
}
|
||||
|
||||
$data = json_decode(wp_remote_retrieve_body($response), true);
|
||||
|
||||
if (empty($data['hits'])) {
|
||||
return '<p>找不到相關結果</p>';
|
||||
}
|
||||
|
||||
$output = '<ul class="momentry-results">';
|
||||
foreach ($data['hits'] as $hit) {
|
||||
// 注意: API 現在返回 file_path 而非 media_url
|
||||
// 需要將文件路徑轉換為可訪問的 URL
|
||||
$file_path = $hit['file_path'];
|
||||
$video_url = convert_file_path_to_url($file_path); // 需要實作此函數
|
||||
|
||||
$output .= sprintf(
|
||||
'<li>%s <a href="%s?start=%s">播放</a></li>',
|
||||
esc_html($hit['text']),
|
||||
$video_url,
|
||||
$hit['start']
|
||||
);
|
||||
}
|
||||
$output .= '</ul>';
|
||||
|
||||
return $output;
|
||||
});
|
||||
?>
|
||||
```
|
||||
|
||||
**使用方式**:
|
||||
```
|
||||
[momentry_search query="charade" limit="5"]
|
||||
```
|
||||
|
||||
### 3.6 WordPress REST API Endpoint
|
||||
|
||||
在 WordPress REST API 中註冊自定義端點:
|
||||
|
||||
```php
|
||||
<?php
|
||||
// 註冊 REST API 端點
|
||||
add_action('rest_api_init', function() {
|
||||
register_rest_route('momentry/v1', '/search', [
|
||||
'methods' => '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,
|
||||
"file_path": "/Users/accusys/momentry/var/sftpgo/data/demo/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 詳細指南
|
||||
@@ -1,119 +0,0 @@
|
||||
# Momentry Core API 文件總覽
|
||||
|
||||
| 項目 | 內容 |
|
||||
|------|------|
|
||||
| 建立者 | OpenCode |
|
||||
| 建立時間 | 2026-03-25 |
|
||||
| 文件版本 | V2.2 |
|
||||
|
||||
---
|
||||
|
||||
## 版本歷史
|
||||
|
||||
| 版本 | 日期 | 目的 | 操作人 | 工具/模型 |
|
||||
|------|------|------|--------|-----------|
|
||||
| V2.0 | 2026-03-22 | 創建 API 文件總覽 | Warren | OpenCode |
|
||||
| V2.1 | 2026-03-24 | 新增文件分類與快速選擇指南 | OpenCode | deepseek-reasoner |
|
||||
| V2.2 | 2026-03-25 | 更新 API Key 驗證說明與文件連結 | OpenCode | deepseek-reasoner |
|
||||
|
||||
---
|
||||
|
||||
## 文件架構
|
||||
|
||||
```
|
||||
docs/
|
||||
├── API_INDEX.md ← 本文件:總覽與入口
|
||||
├── API_ENDPOINTS.md ← API 端點完整說明
|
||||
├── API_EXAMPLES.md ← 完整範例總覽(curl / n8n / WordPress)
|
||||
├── API_REFERENCE.md ← 詳細技術參考
|
||||
├── DEMO_MANUAL.md ← ⭐ 示範手冊(含 Demo API Key)
|
||||
├── API_N8N_GUIDE.md ← n8n 詳細指南
|
||||
├── API_WORDPRESS_GUIDE.md ← WordPress 詳細指南
|
||||
├── API_CURL_EXAMPLES.md ← curl 快速範例
|
||||
│
|
||||
├── API_TRAINING_MARCOM.md ← ⭐ marcom 團隊教育訓練手冊
|
||||
├── API_WORKFLOW_WORDPRESS_N8N.md ← WordPress/n8n 完整工作流程
|
||||
└── CHUNK_DATA_STRUCTURE.md ← Chunk 資料結構說明
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 快速選擇指南
|
||||
|
||||
| 需求 | 閱讀文件 |
|
||||
|------|----------|
|
||||
| **我要快速開始測試** | ⭐ [DEMO_MANUAL.md](./DEMO_MANUAL.md) |
|
||||
| **我要查看所有範例** | [API_EXAMPLES.md](./API_EXAMPLES.md) |
|
||||
| **我是 marcom 團隊** | ⭐ [API_TRAINING_MARCOM.md](./API_TRAINING_MARCOM.md) |
|
||||
| 我想了解有哪些 API 端點 | [API_ENDPOINTS.md](./API_ENDPOINTS.md) |
|
||||
| 我要整合 WordPress/n8n | [API_WORKFLOW_WORDPRESS_N8N.md](./API_WORKFLOW_WORDPRESS_N8N.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) - 待解決問題追蹤
|
||||
@@ -1,195 +0,0 @@
|
||||
# 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 │ │
|
||||
│ └─────────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
@@ -1,236 +0,0 @@
|
||||
# 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
|
||||
```
|
||||
@@ -1,713 +0,0 @@
|
||||
# Momentry API Key 管理系統設計
|
||||
|
||||
| 項目 | 內容 |
|
||||
|------|------|
|
||||
| 建立者 | Warren |
|
||||
| 建立時間 | 2026-03-21 |
|
||||
| 文件版本 | V1.2 |
|
||||
|
||||
---
|
||||
|
||||
## 版本歷史
|
||||
|
||||
| 版本 | 日期 | 目的 | 操作人 | 工具/模型 |
|
||||
|------|------|------|--------|-----------|
|
||||
| V1.0 | 2026-03-18 | 創建文件 | Warren | OpenCode / MiniMax M2.5 |
|
||||
| V1.1 | 2026-03-20 | 新增 Key 類型與管理流程 | Warren | OpenCode |
|
||||
| V1.2 | 2026-03-21 | 更新 API Key 格式與驗證流程 | Warren | OpenCode |
|
||||
|
||||
---
|
||||
|
||||
**狀態**: 開發中
|
||||
|
||||
---
|
||||
|
||||
## 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 <key_id>
|
||||
momentry api-key revoke <key_id> --reason "安全原因"
|
||||
|
||||
# 輪換管理
|
||||
momentry api-key rotate <key_id> # 正常輪換
|
||||
momentry api-key force-rotate <key_id> # 強制輪換
|
||||
momentry api-key rotation-status <key_id> # 查看輪換狀態
|
||||
|
||||
# 異常管理
|
||||
momentry api-key suspend <key_id> --reason "異常使用"
|
||||
momentry api-key unsuspend <key_id>
|
||||
momentry api-key blacklist <key_id> # 列入黑名單
|
||||
|
||||
# 審計
|
||||
momentry api-key audit <key_id> --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 <gitea_user> \
|
||||
--password <gitea_password> \
|
||||
--token-name <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 <gitea_user> \
|
||||
--password <gitea_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 <gitea_user> \
|
||||
--password <gitea_password> \
|
||||
--token-name <token_name>
|
||||
```
|
||||
|
||||
#### 查詢本地記錄
|
||||
|
||||
```bash
|
||||
# 查詢已納管的 Token 記錄
|
||||
momentry gitea verify --token-name <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: <your-api-key>" https://n8n.example.com/api/v1/workflows
|
||||
```
|
||||
|
||||
### 12.5 CLI 命令
|
||||
|
||||
#### 建立 API Key
|
||||
|
||||
```bash
|
||||
# 基本用法
|
||||
momentry n8n create \
|
||||
--api-key <existing_n8n_api_key> \
|
||||
--label <key_label> \
|
||||
--expires-in-days <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 <existing_n8n_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 <existing_n8n_api_key> \
|
||||
--label <key_label>
|
||||
```
|
||||
|
||||
#### 查詢本地記錄
|
||||
|
||||
```bash
|
||||
# 查詢已納管的 API Key 記錄
|
||||
momentry n8n verify --label <key_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/)
|
||||
@@ -1,399 +0,0 @@
|
||||
# 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<Self> { ... }
|
||||
}
|
||||
|
||||
// After: Option A - Rename
|
||||
impl GiteaScope {
|
||||
pub fn parse(s: &str) -> Option<Self> { ... }
|
||||
}
|
||||
|
||||
// After: Option B - Implement FromStr
|
||||
impl std::str::FromStr for GiteaScope {
|
||||
type Err = ();
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> { ... }
|
||||
}
|
||||
```
|
||||
|
||||
### 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<i64>,
|
||||
service_name: Option<&str>,
|
||||
permissions: &serde_json::Value,
|
||||
expires_at: Option<DateTime<Utc>>,
|
||||
) -> Result<i64>
|
||||
|
||||
// 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<i64>,
|
||||
pub service_name: Option<&'a str>,
|
||||
pub permissions: &'a serde_json::Value,
|
||||
pub expires_at: Option<DateTime<Utc>>,
|
||||
}
|
||||
|
||||
pub async fn create_api_key(&self, config: CreateApiKeyConfig<'_>) -> Result<i64>
|
||||
```
|
||||
|
||||
### AKO-CODE-03 細節
|
||||
|
||||
```rust
|
||||
#[async_trait]
|
||||
pub trait ExternalTokenStore<T> {
|
||||
async fn create(&self, record: T) -> Result<i64>;
|
||||
async fn get_by_label(&self, label: &str) -> Result<Option<T>>;
|
||||
async fn list(&self) -> Result<Vec<T>>;
|
||||
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<String, CachedApiKey>,
|
||||
}
|
||||
|
||||
pub struct CachedApiKey {
|
||||
pub record: ApiKeyRecord,
|
||||
pub cached_at: chrono::DateTime<chrono::Utc>,
|
||||
}
|
||||
|
||||
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<ApiKeyRecord> {
|
||||
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<String, AttemptInfo>,
|
||||
max_attempts: u32,
|
||||
window_seconds: u64,
|
||||
}
|
||||
|
||||
pub struct AttemptInfo {
|
||||
pub count: u32,
|
||||
pub first_attempt: chrono::DateTime<chrono::Utc>,
|
||||
pub locked_until: Option<chrono::DateTime<chrono::Utc>>,
|
||||
}
|
||||
|
||||
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<WebhookEvent>,
|
||||
}
|
||||
|
||||
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 模組
|
||||
@@ -1,222 +0,0 @@
|
||||
# n8n 呼叫 Momentry API 指南
|
||||
|
||||
| 項目 | 內容 |
|
||||
|------|------|
|
||||
| 建立者 | Warren |
|
||||
| 建立時間 | 2026-03-23 |
|
||||
| 文件版本 | V1.1 |
|
||||
|
||||
---
|
||||
|
||||
## 版本歷史
|
||||
|
||||
| 版本 | 日期 | 目的 | 操作人 | 工具/模型 |
|
||||
|------|------|------|--------|-----------|
|
||||
| V1.0 | 2026-03-23 | 創建文件 | Warren | OpenCode / MiniMax M2.5 |
|
||||
| V1.1 | 2026-03-26 | 新增 API Key 驗證說明,更新 HTTP Request Node 設定 | OpenCode | deepseek-reasoner |
|
||||
|
||||
---
|
||||
|
||||
**用途**: 在 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` | 處理進度 |
|
||||
| GET | `/api/v1/jobs` | 任務列表 |
|
||||
| GET | `/api/v1/jobs/: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 }}"
|
||||
│ }
|
||||
├── Send Headers: ✓ (checked)
|
||||
└── Header Parameters:
|
||||
└── X-API-Key: {{ $env.MOMENTRY_API_KEY }}
|
||||
```
|
||||
|
||||
### 測試用(固定關鍵字)
|
||||
|
||||
```
|
||||
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
|
||||
│ }
|
||||
├── Send Headers: ✓ (checked)
|
||||
└── Header Parameters:
|
||||
└── X-API-Key: {{ $env.MOMENTRY_API_KEY }}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 完整 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:
|
||||
|
||||
> **注意**: 所有 `/api/v1/*` 端點都需要 API Key 驗證。請設定環境變數或直接替換 API Key。
|
||||
|
||||
```bash
|
||||
# 設定環境變數(使用您的 API Key)
|
||||
export MOMENTRY_API_KEY="muser_68600856036340bcafc01930eb4bd839_1774418104_97221b69"
|
||||
```
|
||||
|
||||
```bash
|
||||
# 健康檢查
|
||||
curl https://api.momentry.ddns.net/health
|
||||
|
||||
# 搜尋測試 (需要 API Key)
|
||||
curl -X POST https://api.momentry.ddns.net/api/v1/n8n/search \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "X-API-Key: $MOMENTRY_API_KEY" \
|
||||
-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 詳細設定
|
||||
@@ -1,532 +0,0 @@
|
||||
# Momentry Core API 快速查詢表
|
||||
|
||||
| 版本 | 日期 | 建立者 |
|
||||
|------|------|--------|
|
||||
| V1.0 | 2026-03-26 | OpenCode |
|
||||
|
||||
---
|
||||
|
||||
## 📋 快速導覽
|
||||
|
||||
| 類別 | 端點數量 | 說明 |
|
||||
|------|----------|------|
|
||||
| 健康檢查 | 2 | 系統狀態監控 |
|
||||
| 影片管理 | 5 | 影片註冊、查詢、刪除 |
|
||||
| 搜尋功能 | 3 | 語意搜尋、混合搜尋 |
|
||||
| 任務管理 | 2 | 處理任務狀態查詢 |
|
||||
| 系統管理 | 2 | 快取設定、進度查詢 |
|
||||
|
||||
---
|
||||
|
||||
## 🔐 認證
|
||||
|
||||
所有 `/api/v1/*` 端點需要 `X-API-Key` header:
|
||||
|
||||
```bash
|
||||
curl -H "X-API-Key: YOUR_API_KEY" ...
|
||||
```
|
||||
|
||||
**公開端點(無需認證):**
|
||||
- `GET /health`
|
||||
- `GET /health/detailed`
|
||||
|
||||
---
|
||||
|
||||
## 📊 端點總表
|
||||
|
||||
### 健康檢查
|
||||
|
||||
| 方法 | 端點 | 認證 | 描述 |
|
||||
|------|------|------|------|
|
||||
| GET | `/health` | 公開 | 基本健康檢查 |
|
||||
| GET | `/health/detailed` | 公開 | 詳細健康檢查(包含所有服務狀態) |
|
||||
|
||||
### 影片管理
|
||||
|
||||
| 方法 | 端點 | 認證 | 描述 |
|
||||
|------|------|------|------|
|
||||
| POST | `/api/v1/register` | 需要 | 註冊影片並開始處理 |
|
||||
| POST | `/api/v1/unregister` | 需要 | 刪除影片及其所有資料 |
|
||||
| POST | `/api/v1/probe` | 需要 | 探測影片資訊(不註冊) |
|
||||
| GET | `/api/v1/videos` | 需要 | 列出所有已註冊影片 |
|
||||
| GET | `/api/v1/lookup` | 需要 | 查詢影片資訊 |
|
||||
|
||||
### 搜尋功能
|
||||
|
||||
| 方法 | 端點 | 認證 | 描述 |
|
||||
|------|------|------|------|
|
||||
| POST | `/api/v1/search` | 需要 | 語意搜尋(標準格式) |
|
||||
| POST | `/api/v1/n8n/search` | 需要 | 語意搜尋(n8n 格式) |
|
||||
| POST | `/api/v1/search/hybrid` | 需要 | 混合搜尋(向量 + 關鍵字) |
|
||||
|
||||
### 任務管理
|
||||
|
||||
| 方法 | 端點 | 認證 | 描述 |
|
||||
|------|------|------|------|
|
||||
| GET | `/api/v1/jobs` | 需要 | 列出所有處理任務 |
|
||||
| GET | `/api/v1/jobs/:uuid` | 需要 | 取得特定任務詳情 |
|
||||
|
||||
### 系統管理
|
||||
|
||||
| 方法 | 端點 | 認證 | 描述 |
|
||||
|------|------|------|------|
|
||||
| GET | `/api/v1/progress/:uuid` | 需要 | 取得影片處理進度 |
|
||||
| POST | `/api/v1/config/cache` | 需要 | 切換快取功能 |
|
||||
|
||||
---
|
||||
|
||||
## 🔧 詳細端點說明
|
||||
|
||||
### 1. 健康檢查
|
||||
|
||||
#### GET /health
|
||||
**基本健康檢查**
|
||||
```bash
|
||||
curl http://localhost:3002/health
|
||||
```
|
||||
|
||||
**回應:**
|
||||
```json
|
||||
{
|
||||
"status": "ok",
|
||||
"version": "0.1.0",
|
||||
"uptime_ms": 14426558
|
||||
}
|
||||
```
|
||||
|
||||
#### GET /health/detailed
|
||||
**詳細健康檢查**
|
||||
```bash
|
||||
curl http://localhost:3002/health/detailed
|
||||
```
|
||||
|
||||
**回應:**
|
||||
```json
|
||||
{
|
||||
"status": "ok",
|
||||
"version": "0.1.0",
|
||||
"uptime_ms": 14441362,
|
||||
"services": {
|
||||
"postgres": {"status": "ok", "latency_ms": 50, "error": null},
|
||||
"redis": {"status": "ok", "latency_ms": 0, "error": null},
|
||||
"qdrant": {"status": "ok", "latency_ms": 2, "error": null},
|
||||
"mongodb": {"status": "ok", "latency_ms": 2, "error": null}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 影片管理
|
||||
|
||||
#### POST /api/v1/register
|
||||
**註冊影片並開始處理**
|
||||
```bash
|
||||
curl -X POST http://localhost:3002/api/v1/register \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "X-API-Key: YOUR_API_KEY" \
|
||||
-d '{"path": "/path/to/video.mp4"}'
|
||||
```
|
||||
|
||||
**請求:**
|
||||
```json
|
||||
{
|
||||
"path": "/path/to/video.mp4"
|
||||
}
|
||||
```
|
||||
|
||||
**回應:**
|
||||
```json
|
||||
{
|
||||
"uuid": "5dea6618a606e7c7",
|
||||
"video_id": 10,
|
||||
"job_id": 1,
|
||||
"file_name": "video.mp4",
|
||||
"duration": 596.458333,
|
||||
"width": 320,
|
||||
"height": 180,
|
||||
"already_exists": false
|
||||
}
|
||||
```
|
||||
|
||||
#### POST /api/v1/unregister
|
||||
**刪除影片及其所有資料**
|
||||
```bash
|
||||
curl -X POST http://localhost:3002/api/v1/unregister \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "X-API-Key: YOUR_API_KEY" \
|
||||
-d '{"uuid": "5dea6618a606e7c7"}'
|
||||
```
|
||||
|
||||
**請求:**
|
||||
```json
|
||||
{
|
||||
"uuid": "5dea6618a606e7c7"
|
||||
}
|
||||
```
|
||||
|
||||
**回應:**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"uuid": "5dea6618a606e7c7",
|
||||
"message": "Video unregistered successfully"
|
||||
}
|
||||
```
|
||||
|
||||
#### POST /api/v1/probe
|
||||
**探測影片資訊(不註冊)**
|
||||
```bash
|
||||
curl -X POST http://localhost:3002/api/v1/probe \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "X-API-Key: YOUR_API_KEY" \
|
||||
-d '{"path": "/path/to/video.mp4"}'
|
||||
```
|
||||
|
||||
**請求:**
|
||||
```json
|
||||
{
|
||||
"path": "/path/to/video.mp4"
|
||||
}
|
||||
```
|
||||
|
||||
**回應:**
|
||||
```json
|
||||
{
|
||||
"uuid": "5dea6618a606e7c7",
|
||||
"file_name": "video.mp4",
|
||||
"duration": 596.458333,
|
||||
"width": 320,
|
||||
"height": 180,
|
||||
"fps": 24.0,
|
||||
"cached": true,
|
||||
"format": {...},
|
||||
"streams": [...]
|
||||
}
|
||||
```
|
||||
|
||||
#### GET /api/v1/videos
|
||||
**列出所有已註冊影片**
|
||||
```bash
|
||||
curl -H "X-API-Key: YOUR_API_KEY" http://localhost:3002/api/v1/videos
|
||||
```
|
||||
|
||||
**回應:**
|
||||
```json
|
||||
{
|
||||
"videos": [
|
||||
{
|
||||
"uuid": "a03485a40b2df2d3",
|
||||
"file_path": "/path/to/video.mp4",
|
||||
"file_name": "video.mp4",
|
||||
"duration": 596.458333,
|
||||
"width": 320,
|
||||
"height": 180
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
#### GET /api/v1/lookup
|
||||
**查詢影片資訊**
|
||||
```bash
|
||||
# 依 UUID 查詢
|
||||
curl -H "X-API-Key: YOUR_API_KEY" "http://localhost:3002/api/v1/lookup?uuid=a03485a40b2df2d3"
|
||||
|
||||
# 依路徑查詢
|
||||
curl -H "X-API-Key: YOUR_API_KEY" "http://localhost:3002/api/v1/lookup?path=/path/to/video.mp4"
|
||||
```
|
||||
|
||||
**回應:**
|
||||
```json
|
||||
{
|
||||
"uuid": "a03485a40b2df2d3",
|
||||
"file_path": "/path/to/video.mp4",
|
||||
"file_name": "video.mp4",
|
||||
"duration": 596.458333
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 搜尋功能
|
||||
|
||||
#### POST /api/v1/search
|
||||
**語意搜尋(標準格式)**
|
||||
```bash
|
||||
curl -X POST http://localhost:3002/api/v1/search \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "X-API-Key: YOUR_API_KEY" \
|
||||
-d '{"query": "search term", "limit": 5}'
|
||||
```
|
||||
|
||||
**請求:**
|
||||
```json
|
||||
{
|
||||
"query": "search term",
|
||||
"limit": 5
|
||||
}
|
||||
```
|
||||
|
||||
**回應:**
|
||||
```json
|
||||
{
|
||||
"results": [
|
||||
{
|
||||
"uuid": "a1b10138a6bbb0cd",
|
||||
"chunk_id": "sentence_0001",
|
||||
"chunk_type": "sentence",
|
||||
"start_time": 10.5,
|
||||
"end_time": 15.2,
|
||||
"text": "Found text matching query",
|
||||
"score": 0.85
|
||||
}
|
||||
],
|
||||
"query": "search term"
|
||||
}
|
||||
```
|
||||
|
||||
#### POST /api/v1/n8n/search
|
||||
**語意搜尋(n8n 格式)**
|
||||
```bash
|
||||
curl -X POST http://localhost:3002/api/v1/n8n/search \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "X-API-Key: YOUR_API_KEY" \
|
||||
-d '{"query": "search term", "limit": 5}'
|
||||
```
|
||||
|
||||
**回應:**
|
||||
```json
|
||||
{
|
||||
"query": "search term",
|
||||
"count": 1,
|
||||
"hits": [
|
||||
{
|
||||
"id": "sentence_0001",
|
||||
"vid": "a1b10138a6bbb0cd",
|
||||
"start_time": 10.5,
|
||||
"end_time": 15.2,
|
||||
"title": "Chunk sentence_0001",
|
||||
"text": "Found text matching query",
|
||||
"score": 0.85,
|
||||
"file_path": "/path/to/video.mp4"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
#### POST /api/v1/search/hybrid
|
||||
**混合搜尋(向量 + 關鍵字)**
|
||||
```bash
|
||||
curl -X POST http://localhost:3002/api/v1/search/hybrid \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "X-API-Key: YOUR_API_KEY" \
|
||||
-d '{"query": "search term", "limit": 5}'
|
||||
```
|
||||
|
||||
**請求:**
|
||||
```json
|
||||
{
|
||||
"query": "search term",
|
||||
"limit": 5
|
||||
}
|
||||
```
|
||||
|
||||
**回應:** 與 `/api/v1/search` 相同格式
|
||||
|
||||
### 4. 任務管理
|
||||
|
||||
#### GET /api/v1/jobs
|
||||
**列出所有處理任務**
|
||||
```bash
|
||||
curl -H "X-API-Key: YOUR_API_KEY" http://localhost:3002/api/v1/jobs
|
||||
```
|
||||
|
||||
**回應:**
|
||||
```json
|
||||
{
|
||||
"jobs": [
|
||||
{
|
||||
"id": 10,
|
||||
"uuid": "a03485a40b2df2d3",
|
||||
"status": "running",
|
||||
"current_processor": null,
|
||||
"progress_current": 0,
|
||||
"progress_total": 0,
|
||||
"created_at": "2026-03-26 13:39:37.830465",
|
||||
"started_at": null
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
#### GET /api/v1/jobs/:uuid
|
||||
**取得特定任務詳情**
|
||||
```bash
|
||||
curl -H "X-API-Key: YOUR_API_KEY" http://localhost:3002/api/v1/jobs/a03485a40b2df2d3
|
||||
```
|
||||
|
||||
**回應:**
|
||||
```json
|
||||
{
|
||||
"id": 10,
|
||||
"uuid": "a03485a40b2df2d3",
|
||||
"status": "running",
|
||||
"current_processor": null,
|
||||
"progress_current": 0,
|
||||
"progress_total": 0,
|
||||
"processors": [
|
||||
{
|
||||
"processor_type": "asr",
|
||||
"status": "completed",
|
||||
"started_at": "2026-03-26 05:39:40.275468",
|
||||
"completed_at": "2026-03-26 07:19:43.166613",
|
||||
"duration_secs": 6002.891145,
|
||||
"error_message": null
|
||||
},
|
||||
// ... 其他處理器
|
||||
],
|
||||
"created_at": "2026-03-26 13:39:37.830465",
|
||||
"started_at": null,
|
||||
"updated_at": "2026-03-26 07:19:16.338406"
|
||||
}
|
||||
```
|
||||
|
||||
### 5. 系統管理
|
||||
|
||||
#### GET /api/v1/progress/:uuid
|
||||
**取得影片處理進度**
|
||||
```bash
|
||||
curl -H "X-API-Key: YOUR_API_KEY" http://localhost:3002/api/v1/progress/a03485a40b2df2d3
|
||||
```
|
||||
|
||||
**回應:**
|
||||
```json
|
||||
{
|
||||
"uuid": "a03485a40b2df2d3",
|
||||
"user": null,
|
||||
"group": null,
|
||||
"file_name": "video.mp4",
|
||||
"duration": 596.458333,
|
||||
"overall_progress": 0,
|
||||
"cpu_percent": 0.2,
|
||||
"gpu_percent": null,
|
||||
"memory_percent": 0.1,
|
||||
"memory_mb": 16720,
|
||||
"processors": [
|
||||
{
|
||||
"name": "asr",
|
||||
"status": "pending",
|
||||
"current": 0,
|
||||
"total": 0,
|
||||
"progress": 0,
|
||||
"message": ""
|
||||
},
|
||||
// ... 其他處理器
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
#### POST /api/v1/config/cache
|
||||
**切換快取功能**
|
||||
```bash
|
||||
curl -X POST http://localhost:3002/api/v1/config/cache \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "X-API-Key: YOUR_API_KEY" \
|
||||
-d '{"enabled": true}'
|
||||
```
|
||||
|
||||
**請求:**
|
||||
```json
|
||||
{
|
||||
"enabled": true
|
||||
}
|
||||
```
|
||||
|
||||
**回應:**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"cache_enabled": true,
|
||||
"message": "Cache enabled"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🚀 快速工作流程
|
||||
|
||||
### 1. 註冊並處理影片
|
||||
```bash
|
||||
# 1. 註冊影片
|
||||
curl -X POST http://localhost:3002/api/v1/register \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "X-API-Key: YOUR_API_KEY" \
|
||||
-d '{"path": "/path/to/video.mp4"}'
|
||||
|
||||
# 回應包含 UUID: 5dea6618a606e7c7
|
||||
|
||||
# 2. 監控進度
|
||||
curl -H "X-API-Key: YOUR_API_KEY" http://localhost:3002/api/v1/progress/5dea6618a606e7c7
|
||||
|
||||
# 3. 查看任務狀態
|
||||
curl -H "X-API-Key: YOUR_API_KEY" http://localhost:3002/api/v1/jobs/5dea6618a606e7c7
|
||||
```
|
||||
|
||||
### 2. 搜尋影片內容
|
||||
```bash
|
||||
# 1. 列出所有影片
|
||||
curl -H "X-API-Key: YOUR_API_KEY" http://localhost:3002/api/v1/videos
|
||||
|
||||
# 2. 搜尋內容
|
||||
curl -X POST http://localhost:3002/api/v1/n8n/search \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "X-API-Key: YOUR_API_KEY" \
|
||||
-d '{"query": "charade scene", "limit": 10}'
|
||||
```
|
||||
|
||||
### 3. 系統管理
|
||||
```bash
|
||||
# 1. 檢查系統健康
|
||||
curl http://localhost:3002/health/detailed
|
||||
|
||||
# 2. 管理快取
|
||||
curl -X POST http://localhost:3002/api/v1/config/cache \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "X-API-Key: YOUR_API_KEY" \
|
||||
-d '{"enabled": false}'
|
||||
|
||||
# 3. 刪除影片(需要時)
|
||||
curl -X POST http://localhost:3002/api/v1/unregister \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "X-API-Key: YOUR_API_KEY" \
|
||||
-d '{"uuid": "5dea6618a606e7c7"}'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📝 注意事項
|
||||
|
||||
1. **API Key 格式:**
|
||||
- 使用完整 API Key:`muser_68600856036340bcafc01930eb4bd839_1774418104_97221b69`
|
||||
- 系統存儲的是 SHA256 哈希值
|
||||
|
||||
2. **路徑格式:**
|
||||
- 絕對路徑:`/Users/accusys/test_video/video.mp4`
|
||||
- 相對路徑:`./demo/video.mp4`(相對於 SFTPGo 資料目錄)
|
||||
|
||||
3. **回應時間:**
|
||||
- 健康檢查:< 100ms
|
||||
- 搜尋:取決於查詢複雜度,通常 100-500ms
|
||||
- 影片註冊:立即返回,背景處理可能需要數分鐘到數小時
|
||||
|
||||
4. **錯誤處理:**
|
||||
- 401: 認證失敗
|
||||
- 404: 資源不存在
|
||||
- 500: 伺服器內部錯誤
|
||||
|
||||
---
|
||||
|
||||
## 🔗 相關文件
|
||||
|
||||
- [API 參考指南](./API_REFERENCE.md) - 詳細 API 說明
|
||||
- [API 範例總覽](./API_EXAMPLES.md) - 完整使用範例
|
||||
- [API 端點列表](./API_ENDPOINTS.md) - 端點簡介
|
||||
- [Curl 範例指南](./API_CURL_EXAMPLES.md) - curl 命令範例
|
||||
- [n8n 整合指南](./API_N8N_GUIDE.md) - n8n 工作流程整合
|
||||
@@ -1,528 +0,0 @@
|
||||
# Momentry Core API 安裝指南
|
||||
|
||||
| 項目 | 內容 |
|
||||
|------|------|
|
||||
| 建立者 | Warren |
|
||||
| 建立時間 | 2026-03-18 |
|
||||
| 文件版本 | V1.3 |
|
||||
|
||||
---
|
||||
|
||||
## 版本歷史
|
||||
|
||||
| 版本 | 日期 | 目的 | 操作人 | 工具/模型 |
|
||||
|------|------|------|--------|-----------|
|
||||
| V1.0 | 2026-03-18 | 創建文件 | Warren | OpenCode / MiniMax M2.5 |
|
||||
| V1.1 | 2026-03-23 | 更新端點與實際一致 | OpenCode | - |
|
||||
| V1.2 | 2026-03-25 | 新增快取/刪除 API | OpenCode | - |
|
||||
| V1.3 | 2026-03-26 | 修正認證聲明與API回應格式 | 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
|
||||
|
||||
**API Key 認證:**
|
||||
|
||||
所有 `/api/v1/*` 端點需要 `X-API-Key` header 進行認證。
|
||||
|
||||
**公開端點:**
|
||||
- `GET /health` - 健康檢查
|
||||
- `GET /health/detailed` - 詳細健康檢查
|
||||
|
||||
**認證格式:**
|
||||
```bash
|
||||
curl -H "X-API-Key: YOUR_API_KEY" http://localhost:3002/api/v1/videos
|
||||
```
|
||||
|
||||
**API Key 管理:**
|
||||
- 使用 `/api/v1/api-keys` 端點管理 API Keys
|
||||
- 詳細說明請參考 [API Key Management Guide](../docs/API_KEY_MANAGEMENT.md)
|
||||
|
||||
---
|
||||
|
||||
## 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,
|
||||
"job_id": 10,
|
||||
"file_name": "video.mp4",
|
||||
"duration": 120.5,
|
||||
"width": 1920,
|
||||
"height": 1080,
|
||||
"already_exists": false
|
||||
}
|
||||
```
|
||||
|
||||
**Example:**
|
||||
```bash
|
||||
curl -X POST http://localhost:3002/api/v1/register \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "X-API-Key: YOUR_API_KEY" \
|
||||
-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 -H "X-API-Key: YOUR_API_KEY" 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" \
|
||||
-H "X-API-Key: YOUR_API_KEY" \
|
||||
-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,
|
||||
"file_path": "/Users/accusys/momentry/var/sftpgo/data/demo/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[].file_path` | string | Full file path to video file |
|
||||
|
||||
**Example:**
|
||||
```bash
|
||||
curl -X POST http://localhost:3002/api/v1/n8n/search \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "X-API-Key: YOUR_API_KEY" \
|
||||
-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 -H "X-API-Key: YOUR_API_KEY" "http://localhost:3002/api/v1/lookup?path=/path/to/video.mp4"
|
||||
|
||||
# Lookup by UUID
|
||||
curl -H "X-API-Key: YOUR_API_KEY" "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 -H "X-API-Key: YOUR_API_KEY" 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 |
|
||||
|
||||
---
|
||||
|
||||
## Cache Toggle
|
||||
|
||||
Toggle caching at runtime.
|
||||
|
||||
**Endpoint:** `POST /api/v1/config/cache`
|
||||
|
||||
**Request Body:**
|
||||
```json
|
||||
{
|
||||
"enabled": true
|
||||
}
|
||||
```
|
||||
|
||||
| Field | Type | Required | Description |
|
||||
|-------|------|----------|-------------|
|
||||
| `enabled` | boolean | Yes | Enable (true) or disable (false) cache |
|
||||
|
||||
**Response (200):**
|
||||
```json
|
||||
{
|
||||
"cache_enabled": true,
|
||||
"message": "Cache toggled successfully"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Unregister Video
|
||||
|
||||
Delete a video and all associated data (chunks, processor results, thumbnails).
|
||||
|
||||
**Endpoint:** `POST /api/v1/unregister`
|
||||
|
||||
**Request Body:**
|
||||
```json
|
||||
{
|
||||
"uuid": "5dea6618a606e7c7"
|
||||
}
|
||||
```
|
||||
|
||||
| Field | Type | Required | Description |
|
||||
|-------|------|----------|-------------|
|
||||
| `uuid` | string | Yes | Video UUID (16 character hex) |
|
||||
|
||||
**Response (200):**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "Video unregistered successfully",
|
||||
"uuid": "5dea6618a606e7c7"
|
||||
}
|
||||
```
|
||||
|
||||
**Warning:** This operation is irreversible and will delete all associated chunks, processor results, and thumbnails.
|
||||
|
||||
---
|
||||
|
||||
## 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 <uuid>` |
|
||||
| Check progress | `GET /api/v1/progress/<uuid>` |
|
||||
| Search | `POST /api/v1/search` |
|
||||
| List videos | `GET /api/v1/videos` |
|
||||
| Lookup | `GET /api/v1/lookup?uuid=<uuid>` |
|
||||
| Toggle cache | `POST /api/v1/config/cache` |
|
||||
| Delete video | `POST /api/v1/unregister` |
|
||||
@@ -1,391 +0,0 @@
|
||||
# Momentry Core API 教育訓練手冊
|
||||
|
||||
> **對象**: marcom 團隊
|
||||
> **版本**: V1.4 | **日期**: 2026-03-25
|
||||
|
||||
---
|
||||
|
||||
## 1. 快速開始
|
||||
|
||||
### 基本資訊
|
||||
|
||||
| 項目 | 值 |
|
||||
|------|-----|
|
||||
| API 網址 | `https://api.momentry.ddns.net` |
|
||||
| 認證方式 | Header `X-API-Key` |
|
||||
| 格式 | JSON |
|
||||
|
||||
### Demo 測試帳號
|
||||
|
||||
#### API Key(用於 API 認證)
|
||||
|
||||
```
|
||||
X-API-Key: muser_68600856036340bcafc01930eb4bd839
|
||||
```
|
||||
|
||||
#### SFTPGo(用於影片上傳)
|
||||
|
||||
| 項目 | 值 |
|
||||
|------|-----|
|
||||
| SFTP 主機 | `sftpgo.momentry.ddns.net` |
|
||||
| SFTP 連接埠 | `2022` |
|
||||
| 用戶名 | `demo` |
|
||||
| 密碼 | `demopassword123` |
|
||||
| Web 管理介面 | `https://sftpgo.momentry.ddns.net` |
|
||||
|
||||
**使用方式**:透過 SFTP 上傳影片,系統會自動註冊並處理。
|
||||
|
||||
---
|
||||
|
||||
## 2. 快速範例
|
||||
|
||||
### 查詢所有影片
|
||||
|
||||
```bash
|
||||
curl -s -H "X-API-Key: muser_68600856036340bcafc01930eb4bd839_1774418104_97221b69" \
|
||||
"https://api.momentry.ddns.net/api/v1/videos"
|
||||
```
|
||||
|
||||
### 查詢單一影片
|
||||
|
||||
```bash
|
||||
curl -s -H "X-API-Key: muser_68600856036340bcafc01930eb4bd839_1774418104_97221b69" \
|
||||
"https://api.momentry.ddns.net/api/v1/videos/{uuid}"
|
||||
```
|
||||
|
||||
### 查詢處理任務狀態
|
||||
|
||||
```bash
|
||||
curl -s -H "X-API-Key: muser_68600856036340bcafc01930eb4bd839_1774418104_97221b69" \
|
||||
"https://api.momentry.ddns.net/api/v1/jobs/{uuid}"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. API 端點說明
|
||||
|
||||
### 3.1 影片相關
|
||||
|
||||
#### GET /api/v1/videos
|
||||
取得所有影片列表
|
||||
|
||||
**回應範例**:
|
||||
```json
|
||||
{
|
||||
"videos": [
|
||||
{
|
||||
"uuid": "5dea6618a606e7c7",
|
||||
"filename": "demo_video.mp4",
|
||||
"duration": 123.45,
|
||||
"status": "ready",
|
||||
"created_at": "2026-03-25T10:00:00Z"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
#### GET /api/v1/videos/:uuid
|
||||
取得單一影片詳情
|
||||
|
||||
### 3.2 搜尋與分段查詢
|
||||
|
||||
#### POST /api/v1/search
|
||||
向量搜尋,查詢分段(Chunk)詳情
|
||||
|
||||
**請求參數**:
|
||||
| 參數 | 類型 | 必填 | 說明 |
|
||||
|------|------|------|------|
|
||||
| `query` | string | 是 | 搜尋關鍵字 |
|
||||
| `limit` | number | 否 | 回傳數量(預設 10) |
|
||||
| `uuid` | string | 否 | 只搜尋指定影片 |
|
||||
|
||||
**請求範例**:
|
||||
```json
|
||||
{
|
||||
"query": "天氣",
|
||||
"limit": 10,
|
||||
"uuid": "5dea6618a606e7c7"
|
||||
}
|
||||
```
|
||||
|
||||
**回應範例**:
|
||||
```json
|
||||
{
|
||||
"results": [
|
||||
{
|
||||
"uuid": "39567a0eb16f39fd",
|
||||
"chunk_id": "sentence_1471",
|
||||
"chunk_type": "sentence",
|
||||
"start_time": 5309.08,
|
||||
"end_time": 5311.08,
|
||||
"text": "influenced by a vital way,",
|
||||
"score": 0.68
|
||||
}
|
||||
],
|
||||
"query": "天氣"
|
||||
}
|
||||
```
|
||||
|
||||
**Chunk 欄位說明**:
|
||||
| 欄位 | 說明 | 範例 |
|
||||
|------|------|------|
|
||||
| `uuid` | 影片唯一識別碼 | `39567a0eb16f39fd` |
|
||||
| `chunk_id` | 分段識別碼 | `sentence_1471` |
|
||||
| `chunk_type` | 分段類型 | `sentence` / `cut` / `time` / `trace` / `story` |
|
||||
| `start_time` | 開始時間(秒) | `5309.08` |
|
||||
| `end_time` | 結束時間(秒) | `5311.08` |
|
||||
| `text` | 內容文字 | `influenced by a vital way` |
|
||||
| `score` | 相似度分數(0-1) | `0.68` |
|
||||
|
||||
**Chunk 類型說明**:
|
||||
| 類型 | 說明 | 來源 |
|
||||
|------|------|------|
|
||||
| `sentence` | 語音轉文字片段 | ASR 處理 |
|
||||
| `cut` | 場景變化片段 | CUT 處理 |
|
||||
| `time` | 固定時間分段 | 系統自動切割 |
|
||||
| `trace` | 軌跡追蹤片段 | YOLO 追蹤 |
|
||||
| `story` | 故事線片段(父子關係) | Story 分析 |
|
||||
|
||||
#### POST /api/v1/n8n/search
|
||||
n8n 專用搜尋(包含完整影片檔案路徑 file_path)
|
||||
|
||||
**請求參數**: 與 `/search` 相同
|
||||
|
||||
**回應範例**:
|
||||
```json
|
||||
{
|
||||
"query": "天氣",
|
||||
"count": 2,
|
||||
"hits": [
|
||||
{
|
||||
"id": "sentence_1471",
|
||||
"vid": "39567a0eb16f39fd",
|
||||
"chunk_type": "sentence",
|
||||
"start_frame": 318545,
|
||||
"end_frame": 318665,
|
||||
"fps": 59.94,
|
||||
"start_time": 5314.31,
|
||||
"end_time": 5316.32,
|
||||
"text": "influenced by a vital way,",
|
||||
"score": 0.68
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**與 /search 的差異**:
|
||||
| 欄位 | `/search` | `/n8n/search` |
|
||||
|------|-----------|----------------|
|
||||
| 影片 UUID | `uuid` | `vid` |
|
||||
| Chunk ID | `chunk_id` | `id` |
|
||||
| 開始時間 | `start_time` | `start_time` |
|
||||
| 結束時間 | `end_time` | `end_time` |
|
||||
| 相似度分數 | `score` | `score` |
|
||||
| **檔案路徑** | ❌ | ✅ `file_path` |
|
||||
|
||||
> **注意**: `file_path` 是影片的實際路徑,可用於本地播放。
|
||||
|
||||
### 3.3 任務相關
|
||||
|
||||
### 3.4 任務相關
|
||||
|
||||
#### GET /api/v1/jobs/:uuid
|
||||
查詢處理任務狀態
|
||||
|
||||
**回應範例**:
|
||||
```json
|
||||
{
|
||||
"uuid": "9760d0820f0cf9a7",
|
||||
"video_uuid": "5dea6618a606e7c7",
|
||||
"status": "completed",
|
||||
"progress": 100,
|
||||
"created_at": "2026-03-25T10:00:00Z",
|
||||
"completed_at": "2026-03-25T10:05:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
#### GET /api/v1/jobs
|
||||
查詢所有任務
|
||||
|
||||
**查詢參數**:
|
||||
| 參數 | 說明 | 範例 |
|
||||
|------|------|------|
|
||||
| `status` | 篩選狀態 | `pending`, `processing`, `completed`, `failed` |
|
||||
| `limit` | 回傳數量 | `10` |
|
||||
|
||||
**範例**:
|
||||
```bash
|
||||
curl -s -H "X-API-Key: ..." \
|
||||
"https://api.momentry.ddns.net/api/v1/jobs?status=completed&limit=5"
|
||||
```
|
||||
|
||||
### 3.5 系統管理
|
||||
|
||||
#### POST /api/v1/config/cache
|
||||
切換快取功能(管理員專用)
|
||||
|
||||
**請求範例**:
|
||||
```json
|
||||
{
|
||||
"enabled": true
|
||||
}
|
||||
```
|
||||
|
||||
**回應範例**:
|
||||
```json
|
||||
{
|
||||
"cache_enabled": true,
|
||||
"message": "Cache toggled successfully"
|
||||
}
|
||||
```
|
||||
|
||||
#### POST /api/v1/unregister
|
||||
刪除影片及其所有關聯資料(管理員專用)
|
||||
|
||||
**請求範例**:
|
||||
```json
|
||||
{
|
||||
"uuid": "5dea6618a606e7c7"
|
||||
}
|
||||
```
|
||||
|
||||
**回應範例**:
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "Video unregistered successfully",
|
||||
"uuid": "5dea6618a606e7c7"
|
||||
}
|
||||
```
|
||||
|
||||
**注意**: 此操作會刪除影片及其所有分段、處理結果、縮圖等關聯資料,**無法復原**。
|
||||
|
||||
### 3.6 健康檢查
|
||||
|
||||
#### GET /health
|
||||
服務健康狀態(**無需認證**)
|
||||
|
||||
**回應**:
|
||||
```json
|
||||
{
|
||||
"status": "ok",
|
||||
"version": "0.9.20260325_144654"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. n8n Workflow 範例
|
||||
|
||||
### 4.1 基本設定
|
||||
|
||||
在 n8n workflow 中使用 HTTP Request 節點:
|
||||
|
||||
```
|
||||
┌─────────────────┐
|
||||
│ HTTP Request │
|
||||
├─────────────────┤
|
||||
│ Method: GET │
|
||||
│ URL: https://api.momentry.ddns.net/api/v1/videos
|
||||
│ Headers: │
|
||||
│ X-API-Key: │
|
||||
│ [YOUR_KEY] │
|
||||
└────────┬────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────┐
|
||||
│ 處理回應資料 │
|
||||
└─────────────────┘
|
||||
```
|
||||
|
||||
### 4.2 範例:檢查任務狀態
|
||||
|
||||
```javascript
|
||||
// n8n Function Node 範例
|
||||
const jobUuid = $input.item.json.uuid;
|
||||
|
||||
return [{
|
||||
json: {
|
||||
method: "GET",
|
||||
url: `https://api.momentry.ddns.net/api/v1/jobs/${jobUuid}`,
|
||||
headers: {
|
||||
"X-API-Key": "YOUR_API_KEY"
|
||||
}
|
||||
}
|
||||
}];
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. 常見問題
|
||||
|
||||
### Q: 返回 401 錯誤怎麼辦?
|
||||
確認 Header 中有正確的 `X-API-Key` 值
|
||||
|
||||
### Q: 如何確認影片處理完成?
|
||||
```
|
||||
GET /api/v1/jobs/{uuid}
|
||||
```
|
||||
檢查 `status` 是否為 `completed`
|
||||
|
||||
### Q: 查不到資料?
|
||||
確認 UUID 格式正確(16碼 hex 字串)
|
||||
|
||||
---
|
||||
|
||||
## 6. 快速參考卡
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ Momentry API 速查 │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ 查詢所有影片 GET /api/v1/videos │
|
||||
│ 查詢單一影片 GET /api/v1/videos/:uuid │
|
||||
│ 向量搜尋 POST /api/v1/search │
|
||||
│ n8n 搜尋 POST /api/v1/n8n/search │
|
||||
│ 查詢任務狀態 GET /api/v1/jobs/:uuid │
|
||||
│ 查詢所有任務 GET /api/v1/jobs │
|
||||
│ 快取設定 POST /api/v1/config/cache (管理員) │
|
||||
│ 刪除影片 POST /api/v1/unregister (管理員) │
|
||||
│ 健康檢查 GET /health (免認證) │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ Header: X-API-Key: [YOUR_KEY] │
|
||||
│ URL: https://api.momentry.ddns.net │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 附錄:回應狀態說明
|
||||
|
||||
### 任務狀態 (status)
|
||||
|
||||
| 狀態 | 說明 |
|
||||
|------|------|
|
||||
| `pending` | 等待處理 |
|
||||
| `processing` | 處理中 |
|
||||
| `completed` | 已完成 |
|
||||
| `failed` | 處理失敗 |
|
||||
|
||||
### 影片狀態 (status)
|
||||
|
||||
| 狀態 | 說明 |
|
||||
|------|------|
|
||||
| `uploading` | 上傳中 |
|
||||
| `pending` | 等待處理 |
|
||||
| `processing` | 處理中 |
|
||||
| `ready` | 已就緒 |
|
||||
| `error` | 錯誤 |
|
||||
|
||||
---
|
||||
|
||||
## 附錄:版本歷史
|
||||
|
||||
| 版本 | 日期 | 內容 | 操作人 |
|
||||
|------|------|------|--------|
|
||||
| V1.0 | 2026-03-25 | 初版建立 | OpenCode |
|
||||
| V1.1 | 2026-03-25 | 新增快取/刪除 API、搜尋端點文件 | OpenCode |
|
||||
| V1.2 | 2026-03-25 | 新增 Chunk 欄位說明、類型、播放方式 | OpenCode |
|
||||
| V1.3 | 2026-03-25 | 新增 Demo 測試帳號(SFTPGo)| OpenCode |
|
||||
| V1.4 | 2026-03-25 | 更新 n8n 搜尋回傳欄位說明 (media_url→file_path) | OpenCode |
|
||||
| V1.5 | 2026-04-17 | 修正 API Key 格式、統一 n8n/search 欄位名稱 (start/end → start_time/end_time) | OpenCode |
|
||||
@@ -1,325 +0,0 @@
|
||||
# WordPress 呼叫 Momentry API 指南
|
||||
|
||||
| 項目 | 內容 |
|
||||
|------|------|
|
||||
| 版本 | V1.1 |
|
||||
| 日期 | 2026-03-25 |
|
||||
| 用途 | 在 WordPress 中呼叫 Momentry API |
|
||||
|
||||
---
|
||||
|
||||
## 版本歷史
|
||||
|
||||
| 版本 | 日期 | 目的 | 操作人 | 工具/模型 |
|
||||
|------|------|------|--------|-----------|
|
||||
| V1.1 | 2026-03-25 | 更新: n8n 搜尋回傳 `file_path` 取代 `media_url`,新增 API Key 驗證說明 | OpenCode | deepseek-reasoner |
|
||||
| V1.0 | 2026-03-23 | 創建 WordPress API 指南 | Warren | OpenCode / MiniMax M2.5 |
|
||||
|
||||
---
|
||||
|
||||
## API URL
|
||||
|
||||
在 WordPress 中呼叫 API,**請使用外部 URL**:
|
||||
|
||||
```
|
||||
https://api.momentry.ddns.net
|
||||
```
|
||||
|
||||
> ⚠️ WordPress 運行於瀏覽器端,無法直接訪問 `localhost`。
|
||||
|
||||
---
|
||||
|
||||
## API 認證
|
||||
|
||||
所有 `/api/v1/*` 端點(除了健康檢查)都需要 API Key 認證。請在請求標頭中加入:
|
||||
|
||||
```
|
||||
'headers' => ['Content-Type' => 'application/json', 'X-API-Key' => 'YOUR_API_KEY']
|
||||
```
|
||||
|
||||
**目前示範使用的 API Key**: `demo_api_key_12345`
|
||||
|
||||
> **注意**: 正式環境請使用安全的 API Key 管理機制,避免在客戶端 JavaScript 中暴露 API Key。
|
||||
|
||||
---
|
||||
|
||||
## 常用端點
|
||||
|
||||
| 方法 | 端點 | 說明 |
|
||||
|------|------|------|
|
||||
| GET | `/health` | 健康檢查 |
|
||||
| POST | `/api/v1/search` | 語意搜尋(標準格式) |
|
||||
| GET | `/api/v1/videos` | 列出所有影片 |
|
||||
| GET | `/api/v1/lookup` | 查詢影片 |
|
||||
|
||||
---
|
||||
|
||||
## PHP 範例
|
||||
|
||||
### 基本搜尋
|
||||
|
||||
```php
|
||||
<?php
|
||||
$api_url = 'https://api.momentry.ddns.net/api/v1/n8n/search';
|
||||
|
||||
$data = [
|
||||
'query' => 'charade',
|
||||
'limit' => 10
|
||||
];
|
||||
|
||||
$response = wp_remote_post($api_url, [
|
||||
'headers' => ['Content-Type' => 'application/json', 'X-API-Key' => 'YOUR_API_KEY'],
|
||||
'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
|
||||
<?php
|
||||
$api_url = 'https://api.momentry.ddns.net/api/v1/videos';
|
||||
|
||||
$response = wp_remote_get($api_url, [
|
||||
'headers' => ['X-API-Key' => 'YOUR_API_KEY'],
|
||||
'timeout' => 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
|
||||
<?php
|
||||
$uuid = '5dea6618a606e7c7';
|
||||
$api_url = 'https://api.momentry.ddns.net/api/v1/lookup?uuid=' . $uuid;
|
||||
|
||||
$response = wp_remote_get($api_url, [
|
||||
'headers' => ['X-API-Key' => 'YOUR_API_KEY'],
|
||||
'timeout' => 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', 'X-API-Key': 'YOUR_API_KEY' },
|
||||
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
|
||||
<?php
|
||||
// 將文件路徑轉換為可訪問的 URL
|
||||
function convert_file_path_to_url($file_path) {
|
||||
// 範例: 將 SFTPGo 文件路徑轉換為 web URL
|
||||
// /Users/accusys/momentry/var/sftpgo/data/demo/video.mp4
|
||||
// → https://sftpgo.example.com/demo/video.mp4
|
||||
|
||||
// 移除基本路徑
|
||||
$base_path = '/Users/accusys/momentry/var/sftpgo/data/';
|
||||
if (strpos($file_path, $base_path) === 0) {
|
||||
$relative_path = substr($file_path, strlen($base_path));
|
||||
// 替換為實際的 SFTPGo web URL
|
||||
return 'https://sftpgo.example.com/' . $relative_path;
|
||||
}
|
||||
|
||||
// 如果無法轉換,返回原始路徑
|
||||
return $file_path;
|
||||
}
|
||||
|
||||
// 註冊短碼
|
||||
add_shortcode('momentry_search', function($atts) {
|
||||
$atts = shortcode_atts([
|
||||
'query' => '',
|
||||
'limit' => '10'
|
||||
], $atts);
|
||||
|
||||
if (empty($atts['query'])) {
|
||||
return '<p>請提供搜尋關鍵字</p>';
|
||||
}
|
||||
|
||||
$response = wp_remote_post('https://api.momentry.ddns.net/api/v1/n8n/search', [
|
||||
'headers' => [
|
||||
'Content-Type' => 'application/json',
|
||||
'X-API-Key' => 'YOUR_API_KEY' // 替換為實際的 API Key
|
||||
],
|
||||
'body' => json_encode([
|
||||
'query' => $atts['query'],
|
||||
'limit' => (int)$atts['limit']
|
||||
]),
|
||||
'timeout' => 30
|
||||
]);
|
||||
|
||||
if (is_wp_error($response)) {
|
||||
return '<p>搜尋服務暫時無法使用</p>';
|
||||
}
|
||||
|
||||
$data = json_decode(wp_remote_retrieve_body($response), true);
|
||||
|
||||
if (empty($data['hits'])) {
|
||||
return '<p>找不到相關結果</p>';
|
||||
}
|
||||
|
||||
$output = '<ul class="momentry-results">';
|
||||
foreach ($data['hits'] as $hit) {
|
||||
// 注意: API 現在返回 file_path 而非 media_url
|
||||
// 需要將文件路徑轉換為可訪問的 URL
|
||||
$file_path = $hit['file_path'];
|
||||
$video_url = convert_file_path_to_url($file_path); // 需要實作此函數
|
||||
|
||||
$output .= sprintf(
|
||||
'<li>%s <a href="%s?start=%s">播放</a></li>',
|
||||
esc_html($hit['text']),
|
||||
$video_url,
|
||||
$hit['start']
|
||||
);
|
||||
}
|
||||
$output .= '</ul>';
|
||||
|
||||
return $output;
|
||||
});
|
||||
?>
|
||||
```
|
||||
|
||||
**使用方式**:
|
||||
```
|
||||
[momentry_search query="charade" limit="5"]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## REST API Endpoint (WP >= 5.0)
|
||||
|
||||
在 WordPress REST API 中註冊自定義端點:
|
||||
|
||||
```php
|
||||
<?php
|
||||
// 註冊 REST API 端點
|
||||
add_action('rest_api_init', function() {
|
||||
register_rest_route('momentry/v1', '/search', [
|
||||
'methods' => 'POST',
|
||||
'callback' => function($request) {
|
||||
$response = wp_remote_post(
|
||||
'https://api.momentry.ddns.net/api/v1/n8n/search',
|
||||
[
|
||||
'headers' => ['Content-Type' => 'application/json', 'X-API-Key' => 'YOUR_API_KEY'],
|
||||
'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 使用範例
|
||||
@@ -1,461 +0,0 @@
|
||||
# Momentry API 使用流程
|
||||
|
||||
> **目標**: 從影片上傳到搜尋的完整流程
|
||||
> **適用**: WordPress / n8n 整合
|
||||
> **版本**: V1.0 | **日期**: 2026-03-25
|
||||
|
||||
---
|
||||
|
||||
## 流程總覽
|
||||
|
||||
```
|
||||
┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐
|
||||
│ 1. 上傳 │ → │ 2. 註冊 │ → │ 3. 確認 │ → │ 4. 處理 │ → │ 5. 搜尋 │
|
||||
│ SFTPGo │ │ 自動完成 │ │ UUID │ │ 查詢進度 │ │ 測試 │
|
||||
└─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Step 1: 上傳影片
|
||||
|
||||
### 方式 A: SFTP 上傳(推薦)
|
||||
|
||||
```bash
|
||||
# 連線資訊
|
||||
主機: sftpgo.momentry.ddns.net
|
||||
連接埠: 2022
|
||||
用戶名: demo
|
||||
密碼: demopassword123
|
||||
```
|
||||
|
||||
使用 FileZilla 或 SFTP 客戶端上傳到 `/` 目錄
|
||||
|
||||
### 方式 B: SFTP 命令列
|
||||
|
||||
```bash
|
||||
sshpass -p "demopassword123" sftp -P 2022 demo@sftpgo.momentry.ddns.net
|
||||
```
|
||||
|
||||
上傳後確認檔案在 SFTPGo 中的位置
|
||||
|
||||
---
|
||||
|
||||
## Step 2: 自動註冊
|
||||
|
||||
上傳後,系統會自動:
|
||||
1. 偵測新檔案
|
||||
2. 計算 UUID(SHA256)
|
||||
3. 建立資料庫記錄
|
||||
|
||||
**無需手動操作**
|
||||
|
||||
---
|
||||
|
||||
## Step 3: 確認註冊成功
|
||||
|
||||
### 查詢所有影片
|
||||
|
||||
```bash
|
||||
curl -s -H "X-API-Key: YOUR_API_KEY" \
|
||||
"https://api.momentry.ddns.net/api/v1/videos" | jq '.videos | length'
|
||||
```
|
||||
|
||||
### 查詢特定檔案
|
||||
|
||||
```bash
|
||||
curl -s -H "X-API-Key: YOUR_API_KEY" \
|
||||
"https://api.momentry.ddns.net/api/v1/videos" | jq '.videos[] | select(.file_name | contains("你的檔案名"))'
|
||||
```
|
||||
|
||||
### 預期回應
|
||||
|
||||
```json
|
||||
{
|
||||
"uuid": "952f5854b9febad1",
|
||||
"file_path": "/Users/accusys/momentry/var/sftpgo/data/demo/你的檔案.mp4",
|
||||
"file_name": "你的檔案.mp4",
|
||||
"duration": 123.45,
|
||||
"width": 1920,
|
||||
"height": 1080
|
||||
}
|
||||
```
|
||||
|
||||
**確認要點**:
|
||||
- ✅ UUID 已產生(16位 hex)
|
||||
- ✅ `file_path` 正確
|
||||
- ✅ `duration` > 0
|
||||
|
||||
---
|
||||
|
||||
## Step 4: 查詢處理進度
|
||||
|
||||
### 取得任務 UUID
|
||||
|
||||
```bash
|
||||
# 從影片資訊取得 job_id
|
||||
curl -s -H "X-API-Key: YOUR_API_KEY" \
|
||||
"https://api.momentry.ddns.net/api/v1/videos" | \
|
||||
jq '.videos[] | select(.file_name == "你的檔案.mp4") | {uuid, job_id}'
|
||||
```
|
||||
|
||||
### 查詢任務狀態
|
||||
|
||||
```bash
|
||||
curl -s -H "X-API-Key: YOUR_API_KEY" \
|
||||
"https://api.momentry.ddns.net/api/v1/jobs/{uuid}"
|
||||
```
|
||||
|
||||
### 任務狀態說明
|
||||
|
||||
| status | 說明 | 動作 |
|
||||
|--------|------|------|
|
||||
| `pending` | 等待處理 | 等待中 |
|
||||
| `processing` | 處理中 | 繼續輪詢 |
|
||||
| `completed` | 已完成 | 可進入 Step 5 |
|
||||
| `failed` | 處理失敗 | 檢查錯誤 |
|
||||
|
||||
### n8n 輪詢範例
|
||||
|
||||
```javascript
|
||||
// n8n Workflow: 檢查處理狀態
|
||||
const jobUuid = $input.item.json.job_uuid;
|
||||
|
||||
const response = await fetch(
|
||||
`https://api.momentry.ddns.net/api/v1/jobs/${jobUuid}`,
|
||||
{
|
||||
headers: {
|
||||
"X-API-Key": "YOUR_API_KEY"
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const job = await response.json();
|
||||
|
||||
// 狀態檢查
|
||||
if (job.status === 'completed') {
|
||||
return [{ json: { done: true, video_uuid: job.video_uuid } }];
|
||||
} else {
|
||||
return [{ json: { done: false, status: job.status } }];
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Step 5: 搜尋測試
|
||||
|
||||
處理完成後,資料會入庫到向量資料庫,可進行搜尋測試。
|
||||
|
||||
### 測試向量搜尋
|
||||
|
||||
```bash
|
||||
curl -s -X POST "https://api.momentry.ddns.net/api/v1/search" \
|
||||
-H "X-API-Key: YOUR_API_KEY" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"query": "測試關鍵字",
|
||||
"limit": 5
|
||||
}'
|
||||
```
|
||||
|
||||
### 取得分段(Chunk)內容
|
||||
|
||||
搜尋結果會返回影片分段(Chunk),包含可播放的時間軸資訊:
|
||||
|
||||
```json
|
||||
{
|
||||
"results": [
|
||||
{
|
||||
"uuid": "39567a0eb16f39fd",
|
||||
"chunk_id": "sentence_1471",
|
||||
"chunk_type": "sentence",
|
||||
"start_time": 5309.08,
|
||||
"end_time": 5311.08,
|
||||
"text": "influenced by a vital way,",
|
||||
"score": 0.68
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**Chunk 欄位說明**:
|
||||
| 欄位 | 說明 |
|
||||
|------|------|
|
||||
| `uuid` | 影片 UUID(用於取得影片網址) |
|
||||
| `chunk_id` | 分段 ID |
|
||||
| `chunk_type` | 分段類型(sentence/cut/time/trace/story) |
|
||||
| `start_time` | 開始時間(秒) |
|
||||
| `end_time` | 結束時間(秒) |
|
||||
| `text` | 語音內容文字 |
|
||||
| `score` | 相似度分數(0-1) |
|
||||
|
||||
### 播放分段
|
||||
|
||||
取得 Chunk 後可組合成播放網址:
|
||||
|
||||
```
|
||||
影片網址?start={start_time}&end={end_time}
|
||||
```
|
||||
|
||||
範例:
|
||||
```
|
||||
https://wp.momentry.ddns.net/video.mp4?start=5309.08&end=5311.08
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 完整 n8n Workflow 範例
|
||||
|
||||
```
|
||||
┌──────────────┐
|
||||
│ 觸發 (定時) │
|
||||
└──────┬───────┘
|
||||
▼
|
||||
┌──────────────┐ ┌──────────────┐
|
||||
│ 查詢影片 │────►│ 比對新檔案 │
|
||||
│ /videos │ │ │
|
||||
└──────┬───────┘ └──────────────┘
|
||||
│ │
|
||||
▼ ▼
|
||||
┌──────────────┐ ┌──────────────┐
|
||||
│ 等待處理 │◄────│ 輪詢任務狀態 │
|
||||
│ /jobs/:uuid │ │ /jobs/:uuid │
|
||||
└──────┬───────┘ └──────────────┘
|
||||
│
|
||||
▼ (completed)
|
||||
┌──────────────┐
|
||||
│ 搜尋測試 │
|
||||
│ /search │
|
||||
└──────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 快速參考
|
||||
|
||||
| 步驟 | API | 用途 |
|
||||
|------|-----|------|
|
||||
| 查詢影片 | `GET /api/v1/videos` | 確認上傳成功 |
|
||||
| 查詢任務 | `GET /api/v1/jobs/:uuid` | 查看處理進度 |
|
||||
| 搜尋內容 | `POST /api/v1/search` | 測試搜尋功能 |
|
||||
|
||||
---
|
||||
|
||||
## WordPress PHP 範例
|
||||
|
||||
### 基本設定
|
||||
|
||||
```php
|
||||
<?php
|
||||
class Momentry_API {
|
||||
private const API_URL = 'https://api.momentry.ddns.net';
|
||||
private const API_KEY = 'YOUR_API_KEY';
|
||||
|
||||
public static function request(string $method, string $endpoint, ?array $data = null): array {
|
||||
$url = self::API_URL . $endpoint;
|
||||
|
||||
$args = [
|
||||
'method' => $method,
|
||||
'headers' => [
|
||||
'X-API-Key' => self::API_KEY,
|
||||
'Content-Type' => 'application/json',
|
||||
],
|
||||
'timeout' => 30,
|
||||
];
|
||||
|
||||
if ($data !== null) {
|
||||
$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 static function getVideos(): array {
|
||||
return self::request('GET', '/api/v1/videos');
|
||||
}
|
||||
|
||||
public static function getVideo(string $uuid): array {
|
||||
return self::request('GET', "/api/v1/videos/{$uuid}");
|
||||
}
|
||||
|
||||
public static function getJob(string $uuid): array {
|
||||
return self::request('GET', "/api/v1/jobs/{$uuid}");
|
||||
}
|
||||
|
||||
public static function search(string $query, int $topK = 5): array {
|
||||
return self::request('POST', '/api/v1/search', [
|
||||
'query' => $query,
|
||||
'top_k' => $topK,
|
||||
]);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Step 3: 確認註冊成功
|
||||
|
||||
```php
|
||||
<?php
|
||||
// 查詢所有影片
|
||||
$videos = Momentry_API::getVideos();
|
||||
|
||||
foreach ($videos['videos'] as $video) {
|
||||
echo "UUID: " . $video['uuid'] . "\n";
|
||||
echo "檔案: " . $video['file_name'] . "\n";
|
||||
echo "時長: " . $video['duration'] . " 秒\n";
|
||||
echo "---\n";
|
||||
}
|
||||
|
||||
// 查詢特定影片
|
||||
$video = Momentry_API::getVideo('952f5854b9febad1');
|
||||
print_r($video);
|
||||
```
|
||||
|
||||
### Step 4: 查詢處理進度
|
||||
|
||||
```php
|
||||
<?php
|
||||
// 取得任務狀態
|
||||
$job = Momentry_API::getJob('9760d0820f0cf9a7');
|
||||
|
||||
switch ($job['status']) {
|
||||
case 'pending':
|
||||
echo "等待處理中...\n";
|
||||
break;
|
||||
case 'processing':
|
||||
echo "處理中: " . $job['progress'] . "%\n";
|
||||
break;
|
||||
case 'completed':
|
||||
echo "處理完成!\n";
|
||||
break;
|
||||
case 'failed':
|
||||
echo "處理失敗: " . ($job['error'] ?? '未知錯誤') . "\n";
|
||||
break;
|
||||
}
|
||||
```
|
||||
|
||||
### Step 5: 搜尋內容並取得 Chunk
|
||||
|
||||
```php
|
||||
<?php
|
||||
// 搜尋相關片段
|
||||
$results = Momentry_API::search('測試關鍵字', 5);
|
||||
|
||||
foreach ($results['results'] as $result) {
|
||||
echo "影片 UUID: " . $result['uuid'] . "\n";
|
||||
echo "Chunk ID: " . $result['chunk_id'] . "\n";
|
||||
echo "類型: " . $result['chunk_type'] . "\n";
|
||||
echo "開始: " . $result['start_time'] . "s\n";
|
||||
echo "結束: " . $result['end_time'] . "s\n";
|
||||
echo "內容: " . ($result['text'] ?? '') . "\n";
|
||||
echo "相似度: " . $result['score'] . "\n";
|
||||
echo "---\n";
|
||||
}
|
||||
```
|
||||
|
||||
### WordPress Shortcode 範例(可點擊播放)
|
||||
|
||||
```php
|
||||
<?php
|
||||
// 在 functions.php 中加入
|
||||
add_shortcode('momentry_search', function($atts) {
|
||||
$atts = shortcode_atts([
|
||||
'query' => '',
|
||||
'limit' => 10,
|
||||
], $atts);
|
||||
|
||||
if (empty($atts['query'])) {
|
||||
return '<p>請輸入搜尋關鍵字</p>';
|
||||
}
|
||||
|
||||
try {
|
||||
$results = Momentry_API::search($atts['query'], $atts['limit']);
|
||||
|
||||
if (empty($results['results'])) {
|
||||
return '<p>找不到相關結果</p>';
|
||||
}
|
||||
|
||||
$html = '<div class="momentry-results">';
|
||||
$html .= '<h3>搜尋結果: ' . esc_html($atts['query']) . '</h3>';
|
||||
$html .= '<ul>';
|
||||
|
||||
foreach ($results['results'] as $result) {
|
||||
$video_uuid = $result['uuid'];
|
||||
$start = $result['start_time'] ?? 0;
|
||||
$end = $result['end_time'] ?? 0;
|
||||
$text = $result['text'] ?? '無文字描述';
|
||||
|
||||
$html .= '<li>';
|
||||
$html .= '<a href="/player?uuid=' . esc_attr($video_uuid) .
|
||||
'&start=' . esc_attr($start) .
|
||||
'&end=' . esc_attr($end) . '">';
|
||||
$html .= '播放 ' . $start . 's - ' . $end . 's';
|
||||
$html .= '</a>';
|
||||
$html .= '<br>';
|
||||
$html .= '<small>相似度: ' . round($result['score'] * 100) . '%</small>';
|
||||
$html .= '<br>';
|
||||
$html .= esc_html($text);
|
||||
$html .= '</li>';
|
||||
}
|
||||
|
||||
$html .= '</ul></div>';
|
||||
return $html;
|
||||
|
||||
} catch (Exception $e) {
|
||||
return '<p>搜尋服務暫時無法使用</p>';
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
**使用方式**:
|
||||
```html
|
||||
[momentry_search query="關鍵字" limit="5"]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 完整 n8n Workflow 範例
|
||||
|
||||
```
|
||||
┌──────────────┐
|
||||
│ 觸發 (定時) │
|
||||
└──────┬───────┘
|
||||
▼
|
||||
┌──────────────┐ ┌──────────────┐
|
||||
│ 查詢影片 │────►│ 比對新檔案 │
|
||||
│ /videos │ │ │
|
||||
└──────┬───────┘ └──────────────┘
|
||||
│ │
|
||||
▼ ▼
|
||||
┌──────────────┐ ┌──────────────┐
|
||||
│ 等待處理 │◄────│ 輪詢任務狀態 │
|
||||
│ /jobs/:uuid │ │ /jobs/:uuid │
|
||||
└──────┬───────┘ └──────────────┘
|
||||
│
|
||||
▼ (completed)
|
||||
┌──────────────┐
|
||||
│ 搜尋測試 │
|
||||
│ /search │
|
||||
└──────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
**注意**:
|
||||
- 處理時間視影片長度而定(1分鐘影片約需 2-5 分鐘處理)
|
||||
- 大量影片時建議分批上傳
|
||||
|
||||
---
|
||||
|
||||
## 附錄:版本歷史
|
||||
|
||||
| 版本 | 日期 | 內容 | 操作人 |
|
||||
|------|------|------|--------|
|
||||
| V1.0 | 2026-03-25 | 初版建立 | OpenCode |
|
||||
| V1.1 | 2026-03-25 | 新增 Chunk 取得與播放說明、Shortcode 範例 | OpenCode |
|
||||
| V1.2 | 2026-03-25 | 修正 SFTPGo 主機名稱為 sftpgo.momentry.ddns.net | OpenCode |
|
||||
@@ -1,331 +0,0 @@
|
||||
# 架構優化待評估事項
|
||||
|
||||
| 項目 | 內容 |
|
||||
|------|------|
|
||||
| 建立者 | 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)
|
||||
|
||||
**實作內容**:
|
||||
- 驗證格式: `<type>(<scope>): <description>`
|
||||
- 有效類型: 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 的一部分)
|
||||
@@ -1,450 +0,0 @@
|
||||
# Momentry 備份版本管理規範
|
||||
|
||||
| 項目 | 內容 |
|
||||
|------|------|
|
||||
| 建立者 | Warren / OpenCode |
|
||||
| 建立時間 | 2026-03-25 |
|
||||
| 文件版本 | V1.0 |
|
||||
|
||||
---
|
||||
|
||||
## 版本歷史
|
||||
|
||||
| 版本 | 日期 | 目的 | 操作人 |
|
||||
|------|------|------|--------|
|
||||
| V1.0 | 2026-03-25 | 建立備份版本管理規範 | OpenCode |
|
||||
|
||||
---
|
||||
|
||||
## 1. 概述
|
||||
|
||||
本文檔定義 Momentry 系統的備份版本管理規範,確保新舊架構之間的回滾相容性。
|
||||
|
||||
### 1.1 版本定義
|
||||
|
||||
| 版本 | 日期 | 說明 |
|
||||
|------|------|------|
|
||||
| v1 | 2026-03-18 | 初始備份架構(不包含新架構組件)|
|
||||
| v2 | 2026-03-25 | 新架構備份(包含 monitor_jobs, processor_results, Output 目錄)|
|
||||
|
||||
### 1.2 備份版本格式
|
||||
|
||||
| 版本 | 檔案命名格式 |
|
||||
|------|-------------|
|
||||
| v1 | `{service}_{type}_{YYYYMMDD}_{HHMMSS}.{ext}` |
|
||||
| v2 | `{service}_{type}_v2_{YYYYMMDD}_{HHMMSS}.{ext}` |
|
||||
|
||||
### 1.3 各版本涵蓋範圍
|
||||
|
||||
| 組件 | v1 | v2 |
|
||||
|------|-----|-----|
|
||||
| PostgreSQL (videos, chunks) | ✅ | ✅ |
|
||||
| PostgreSQL (monitor_jobs) | ❌ | ✅ |
|
||||
| PostgreSQL (processor_results) | ❌ | ✅ |
|
||||
| Redis | ✅ | ✅ |
|
||||
| MongoDB Cache | ⚠️ | ⚠️ |
|
||||
| Output (probe.json) | ❌ | ✅ |
|
||||
|
||||
> ⚠️ MongoDB 備份目前存在路徑問題,正在修復中
|
||||
|
||||
---
|
||||
|
||||
## 2. 備份版本識別
|
||||
|
||||
### 2.1 檔名識別
|
||||
|
||||
```bash
|
||||
# 識別版本
|
||||
detect_version() {
|
||||
local backup_file=$1
|
||||
if echo "$backup_file" | grep -q "_v2_"; then
|
||||
echo "v2"
|
||||
else
|
||||
echo "v1"
|
||||
fi
|
||||
}
|
||||
|
||||
# 使用範例
|
||||
detect_version "postgresql_db_momentry_v2_20260325_030000.sql.gz"
|
||||
# 輸出: v2
|
||||
|
||||
detect_version "postgresql_db_momentry_20260324_030000.sql.gz"
|
||||
# 輸出: v1
|
||||
```
|
||||
|
||||
### 2.2 內容識別
|
||||
|
||||
```bash
|
||||
# 檢查是否為 v2 備份
|
||||
is_v2_backup() {
|
||||
local backup_file=$1
|
||||
gzip -dc "$backup_file" 2>/dev/null | grep -q "monitor_jobs" && echo "yes" || echo "no"
|
||||
}
|
||||
|
||||
# 檢查是否包含 processor_results
|
||||
has_processor_results() {
|
||||
local backup_file=$1
|
||||
gzip -dc "$backup_file" 2>/dev/null | grep -q "processor_results" && echo "yes" || echo "no"
|
||||
}
|
||||
```
|
||||
|
||||
### 2.3 檔案大小比較
|
||||
|
||||
| 版本 | PostgreSQL 備份大小 | 說明 |
|
||||
|------|---------------------|------|
|
||||
| v1 | ~18-19 MB | 基本資料表 |
|
||||
| v2 | >19 MB | 包含新表格和索引 |
|
||||
|
||||
---
|
||||
|
||||
## 3. 回滾策略
|
||||
|
||||
### 3.1 回滾流程圖
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────┐
|
||||
│ 選擇還原目標 │
|
||||
└─────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────┐
|
||||
│ 選擇備份版本 │
|
||||
│ ┌───────────┐ ┌───────────┐ │
|
||||
│ │ v1 備份 │ │ v2 備份 │ │
|
||||
│ └───────────┘ └───────────┘ │
|
||||
└─────────────────────────────────────────┘
|
||||
│
|
||||
┌─────────┴─────────┐
|
||||
▼ ▼
|
||||
┌──────────┐ ┌──────────┐
|
||||
│ v1 回滾 │ │ v2 回滾 │
|
||||
└──────────┘ └──────────┘
|
||||
│ │
|
||||
▼ ▼
|
||||
┌──────────┐ ┌──────────┐
|
||||
│ 基本資料庫 │ │ 完整還原 │
|
||||
└──────────┘ └──────────┘
|
||||
```
|
||||
|
||||
### 3.2 回滾矩陣
|
||||
|
||||
| 還原目標 | v1 備份 | v2 備份 |
|
||||
|----------|---------|---------|
|
||||
| 基本資料庫 | ✅ | ✅ |
|
||||
| + monitor_jobs | ❌ | ✅ |
|
||||
| + processor_results | ❌ | ✅ |
|
||||
| + Output 檔案 | ❌ | ✅ |
|
||||
| + MongoDB Cache | ⚠️ | ⚠️ |
|
||||
|
||||
### 3.3 回滾相容性說明
|
||||
|
||||
#### v1 → v2(支援)
|
||||
|
||||
- v1 備份可以還原到 v2 架構
|
||||
- 新架構組件會從空白狀態開始
|
||||
- 不會造成資料損壞
|
||||
|
||||
#### v2 → v1(⚠️ 警告)
|
||||
|
||||
```
|
||||
⚠️ v2 回滾到 v1 可能導致資料丟失
|
||||
|
||||
影響範圍:
|
||||
- monitor_jobs 資料會消失
|
||||
- processor_results 資料會消失
|
||||
- Output 檔案參照可能失效
|
||||
|
||||
建議:
|
||||
1. 在還原前建立 v2 快照
|
||||
2. 或使用隔離還原(staging restore)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. 還原腳本保護機制
|
||||
|
||||
### 4.1 還原前檢查
|
||||
|
||||
```bash
|
||||
# 還原前檢查版本相容性
|
||||
pre_restore_check() {
|
||||
local backup_file=$1
|
||||
local version=$(detect_version "$backup_file")
|
||||
local current_db_version=$(check_current_db_version)
|
||||
|
||||
echo "備份版本: $version"
|
||||
echo "目前版本: $current_db_version"
|
||||
|
||||
# v2 → v1: 警告但允許(使用者需確認)
|
||||
if [ "$version" = "v1" ] && [ "$current_db_version" = "v2" ]; then
|
||||
echo "⚠️ 警告:即將回滾到 v1"
|
||||
echo "影響:monitor_jobs 和 processor_results 資料將被清除"
|
||||
read -p "確認繼續?(y/N): " confirm
|
||||
[ "$confirm" != "y" ] && exit 1
|
||||
fi
|
||||
|
||||
# v1 → v2: 直接允許
|
||||
if [ "$version" = "v1" ] && [ "$current_db_version" = "v2" ]; then
|
||||
echo "ℹ️ 提示:新架構組件將重新初始化"
|
||||
fi
|
||||
|
||||
# v2 → v2: 直接允許
|
||||
# v1 → v1: 直接允許
|
||||
}
|
||||
```
|
||||
|
||||
### 4.2 隔離還原(Staging Restore)
|
||||
|
||||
```bash
|
||||
# 只還原到暫存資料庫,不影響生產
|
||||
restore_to_staging() {
|
||||
local backup_file=$1
|
||||
local version=$(detect_version "$backup_file")
|
||||
|
||||
echo "執行隔離還原..."
|
||||
echo "版本: $version"
|
||||
|
||||
# 建立暫存資料庫
|
||||
PGPASSWORD="$PG_PASSWORD" psql -U "$PG_USER" -d postgres << EOF
|
||||
DROP DATABASE IF EXISTS momentry_staging;
|
||||
CREATE DATABASE momentry_staging;
|
||||
EOF
|
||||
|
||||
# 還原到暫存資料庫
|
||||
PGPASSWORD="$PG_PASSWORD" pg_restore -U "$PG_USER" -d "momentry_staging" \
|
||||
--no-owner --no-acl "$backup_file"
|
||||
|
||||
echo "✅ 還原完成:momentry_staging"
|
||||
echo "驗證命令:psql -U accusys -d momentry_staging -c '\\dt'"
|
||||
}
|
||||
```
|
||||
|
||||
### 4.3 版本驗證命令
|
||||
|
||||
```bash
|
||||
# 識別所有備份版本
|
||||
ls /Users/accusys/momentry/backup/daily/postgresql/*.sql.gz | \
|
||||
xargs -I {} sh -c 'echo "{}: $(detect_version {})"'
|
||||
|
||||
# 驗證 v2 備份內容
|
||||
verify_v2_backup() {
|
||||
local backup_file=$1
|
||||
|
||||
echo "驗證備份: $backup_file"
|
||||
|
||||
# 檢查 monitor_jobs
|
||||
if gzip -dc "$backup_file" | grep -q "monitor_jobs"; then
|
||||
echo "✅ 包含 monitor_jobs"
|
||||
else
|
||||
echo "❌ 缺少 monitor_jobs"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# 檢查 processor_results
|
||||
if gzip -dc "$backup_file" | grep -q "processor_results"; then
|
||||
echo "✅ 包含 processor_results"
|
||||
else
|
||||
echo "❌ 缺少 processor_results"
|
||||
return 1
|
||||
fi
|
||||
|
||||
echo "✅ v2 備份驗證通過"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. 版本遷移
|
||||
|
||||
### 5.1 v1 → v2 遷移步驟
|
||||
|
||||
| 步驟 | 說明 | 驗證 |
|
||||
|------|------|------|
|
||||
| 1 | 確認所有 v1 備份已完成 | `ls *.sql.gz \| grep -v v2` |
|
||||
| 2 | 修改 `backup_all.sh` 加入 v2 標記 | 確認 TIMESTAMP 包含 `v2_` |
|
||||
| 3 | 修正 MongoDB 路徑 | 確認指向正確目錄 |
|
||||
| 4 | 新增 Output 目錄備份 | 確認 probe.json 被備份 |
|
||||
| 5 | 執行測試備份 | 驗證命名格式正確 |
|
||||
| 6 | 驗證 v2 備份完整性 | `verify_v2_backup` |
|
||||
| 7 | 正式啟用 v2 備份 | 確認 crontab 使用新版 |
|
||||
|
||||
### 5.2 遷移驗證清單
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# verify_v2_migration.sh
|
||||
|
||||
echo "=== v2 遷移驗證 ==="
|
||||
|
||||
# 1. 檢查備份腳本
|
||||
echo "1. 檢查備份腳本..."
|
||||
if grep -q "v2_" /Users/accusys/momentry/scripts/backup_all.sh; then
|
||||
echo " ✅ 版本標記已啟用"
|
||||
else
|
||||
echo " ❌ 版本標記未啟用"
|
||||
fi
|
||||
|
||||
# 2. 檢查 MongoDB 路徑
|
||||
echo "2. 檢查 MongoDB 路徑..."
|
||||
if grep -q "/opt/homebrew/var/mongodb" /Users/accusys/momentry/scripts/backup_all.sh; then
|
||||
echo " ✅ MongoDB 路徑已修正"
|
||||
else
|
||||
echo " ❌ MongoDB 路徑未修正"
|
||||
fi
|
||||
|
||||
# 3. 檢查 Output 目錄備份
|
||||
echo "3. 檢查 Output 目錄備份..."
|
||||
if grep -q "momentry_output" /Users/accusys/momentry/scripts/backup_all.sh; then
|
||||
echo " ✅ Output 目錄備份已啟用"
|
||||
else
|
||||
echo " ❌ Output 目錄備份未啟用"
|
||||
fi
|
||||
|
||||
# 4. 檢查最新備份
|
||||
echo "4. 檢查最新備份..."
|
||||
latest_backup=$(ls -t /Users/accusys/momentry/backup/daily/postgresql/*.sql.gz 2>/dev/null | head -1)
|
||||
if [ -n "$latest_backup" ]; then
|
||||
version=$(detect_version "$latest_backup")
|
||||
echo " 最新備份: $(basename $latest_backup)"
|
||||
echo " 版本: $version"
|
||||
if [ "$version" = "v2" ]; then
|
||||
verify_v2_backup "$latest_backup"
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "=== 驗證完成 ==="
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. 疑難排解
|
||||
|
||||
### 6.1 常見問題
|
||||
|
||||
| 問題 | 原因 | 解決方案 |
|
||||
|------|------|----------|
|
||||
| 無法識別版本 | 檔名被修改 | 使用內容分析 `gzip -dc \| grep "monitor_jobs"` |
|
||||
| v2 備份還原失敗 | 磁碟空間不足 | 清理空間後重試 |
|
||||
| v1 還原覆蓋 v2 | 操作失誤 | 使用隔離還原保護生產資料 |
|
||||
| MongoDB 備份為空 | 路徑錯誤 | 修正為 `/opt/homebrew/var/mongodb` |
|
||||
|
||||
### 6.2 緊急回滾流程
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# emergency_restore.sh
|
||||
|
||||
set -e
|
||||
|
||||
BACKUP_FILE=$1
|
||||
VERSION=$2
|
||||
|
||||
echo "=== 緊急回滾 ==="
|
||||
echo "備份檔案: $BACKUP_FILE"
|
||||
echo "目標版本: $VERSION"
|
||||
|
||||
# 1. 建立當前狀態快照
|
||||
echo "1. 建立當前狀態快照..."
|
||||
NOW=$(date +%Y%m%d_%H%M%S)
|
||||
pg_dump -U accusys -d momentry | gzip > "/tmp/momentry_emergency_$NOW.sql.gz"
|
||||
echo " 快照: /tmp/momentry_emergency_$NOW.sql.gz"
|
||||
|
||||
# 2. 執行還原
|
||||
echo "2. 執行還原..."
|
||||
gunzip -c "$BACKUP_FILE" | psql -U accusys -d momentry
|
||||
|
||||
# 3. 驗證
|
||||
echo "3. 驗證還原..."
|
||||
psql -U accusys -d momentry -c "SELECT COUNT(*) FROM monitor_jobs;"
|
||||
|
||||
echo "=== 回滾完成 ==="
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. 備份清單(v2)
|
||||
|
||||
### 7.1 每日備份(v2 格式)
|
||||
|
||||
| 服務 | 備份項目 | 檔案命名 | 說明 |
|
||||
|------|----------|----------|------|
|
||||
| PostgreSQL | momentry | `postgresql_db_momentry_v2_{date}_{time}.sql.gz` | 完整資料庫 |
|
||||
| PostgreSQL | video_register | `postgresql_db_video_register_v2_{date}_{time}.sql.gz` | 影片註冊資料 |
|
||||
| Redis | RDB | `redis_rdb_v2_{date}_{time}.rdb` | Redis 快照 |
|
||||
| MongoDB | 資料 | `mongodb_data_v2_{date}_{time}.tar.gz` | MongoDB 資料 |
|
||||
| n8n | 資料+DB | `n8n_{date}_{time}.tar.gz`, `n8n_db_{date}_{time}.sql.gz` | n8n 完整 |
|
||||
| SFTPGo | 配置+DB | `sftpgo_{date}_{time}.tar.gz`, `sftpgo_db_{date}_{time}.sql.gz` | SFTPGo |
|
||||
| Gitea | 資料 | `gitea_{date}_{time}.tar.gz` | Gitea |
|
||||
| Output | 檔案 | `momentry_output_v2_{date}_{time}.tar.gz` | probe.json 等 |
|
||||
|
||||
### 7.2 備份保留策略
|
||||
|
||||
| 類型 | 保留期限 | 位置 |
|
||||
|------|----------|------|
|
||||
| 每日備份 | 7 天 | `backup/daily/` |
|
||||
| 每週備份 | 4 週 | `backup/weekly/` |
|
||||
| 每月備份 | 12 個月 | `backup/monthly/` |
|
||||
| 歸檔 | 1 年+ | `backup/archive/` |
|
||||
|
||||
---
|
||||
|
||||
## 8. 相關文件
|
||||
|
||||
| 文件 | 說明 |
|
||||
|------|------|
|
||||
| [SERVICES.md](./SERVICES.md) | 服務說明 |
|
||||
| [MOMENTRY_CORE_MONITORING.md](./MOMENTRY_CORE_MONITORING.md) | 監控規範 |
|
||||
| `/Users/accusys/momentry/scripts/backup_all.sh` | 備份腳本 |
|
||||
| `/Users/accusys/momentry/scripts/restore_all.sh` | 還原腳本 |
|
||||
| `/Users/accusys/momentry_core_0.1/monitor/storage/backup_monitor.sh` | 備份監控 |
|
||||
|
||||
---
|
||||
|
||||
## 附錄 A:v2 備份完整性檢查清單
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# check_v2_integrity.sh
|
||||
|
||||
BACKUP_DIR="/Users/accusys/momentry/backup/daily"
|
||||
|
||||
echo "=== v2 備份完整性檢查 ==="
|
||||
|
||||
# 檢查 PostgreSQL
|
||||
echo "1. PostgreSQL..."
|
||||
pg_backup=$(ls -t "$BACKUP_DIR/postgresql"/postgresql_db_momentry_v2_*.sql.gz 2>/dev/null | head -1)
|
||||
if [ -n "$pg_backup" ]; then
|
||||
echo " 備份: $(basename $pg_backup)"
|
||||
verify_v2_backup "$pg_backup"
|
||||
else
|
||||
echo " ❌ 未找到 v2 備份"
|
||||
fi
|
||||
|
||||
# 檢查 Output
|
||||
echo "2. Output 目錄..."
|
||||
output_backup=$(ls -t "$BACKUP_DIR/momentry"/momentry_output_v2_*.tar.gz 2>/dev/null | head -1)
|
||||
if [ -n "$output_backup" ]; then
|
||||
echo " 備份: $(basename $output_backup)"
|
||||
echo " ✅ Output 備份已存在"
|
||||
else
|
||||
echo " ❌ 未找到 Output 備份"
|
||||
fi
|
||||
|
||||
# 檢查 MongoDB
|
||||
echo "3. MongoDB..."
|
||||
mongo_backup=$(ls -t "$BACKUP_DIR/mongodb"/mongodb_data_v2_*.tar.gz 2>/dev/null | head -1)
|
||||
if [ -n "$mongo_backup" ]; then
|
||||
size=$(stat -f%z "$mongo_backup" 2>/dev/null || stat -c%s "$mongo_backup" 2>/dev/null)
|
||||
echo " 備份: $(basename $mongo_backup)"
|
||||
echo " 大小: $size bytes"
|
||||
if [ "$size" -gt 1000 ]; then
|
||||
echo " ✅ MongoDB 備份有效"
|
||||
else
|
||||
echo " ⚠️ MongoDB 備份可能為空"
|
||||
fi
|
||||
else
|
||||
echo " ❌ 未找到 MongoDB 備份"
|
||||
fi
|
||||
|
||||
echo "=== 檢查完成 ==="
|
||||
```
|
||||
@@ -1,667 +0,0 @@
|
||||
# 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')*
|
||||
\`\`\`
|
||||
|
||||
```
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,379 +0,0 @@
|
||||
# Momentry Chunk 資料結構說明
|
||||
|
||||
> **對象**: marcom 團隊
|
||||
> **版本**: V1.0 | **日期**: 2026-03-25
|
||||
|
||||
---
|
||||
|
||||
## 1. 什麼是 Chunk?
|
||||
|
||||
Chunk(片段)是影片處理後的最小單位。當影片上傳後,系統會自動:
|
||||
|
||||
1. **分析** - 偵測場景、人臉、姿態
|
||||
2. **轉換** - 語音轉文字(ASR)
|
||||
3. **分段** - 將內容切割成可搜尋的片段
|
||||
4. **向量化** - 產生可搜尋的特徵向量
|
||||
|
||||
每個 Chunk 就是一個**可獨立搜尋的內容單位**。
|
||||
|
||||
---
|
||||
|
||||
## 2. Chunk 資料結構
|
||||
|
||||
### 2.1 主要欄位
|
||||
|
||||
| 欄位名 | 類型 | 說明 | 範例 |
|
||||
|--------|------|------|------|
|
||||
| `uuid` | 字串 (32) | 影片唯一識別碼 | `952f5854b9febad1` |
|
||||
| `chunk_id` | 字串 (64) | Chunk 唯一識別碼 | `asr_00001` |
|
||||
| `chunk_index` | 整數 | Chunk 順序號碼 | `1` |
|
||||
| `chunk_type` | 字串 (32) | Chunk 類型 | `sentence` |
|
||||
| `start_time` | 浮點數 | 開始時間(秒) | `12.5` |
|
||||
| `end_time` | 浮點數 | 結束時間(秒) | `18.3` |
|
||||
| `content` | JSONB | 詳細內容 | 見下方 |
|
||||
| `vector_id` | 字串 (64) | 向量 ID | `vec_12345` |
|
||||
| `text_content` | 文字 | 純文字內容 | `這是一段話` |
|
||||
| `fps` | 浮點數 | 影片幀率 | `24.0` |
|
||||
| `start_frame` | 整數 | 開始幀數 | `300` |
|
||||
| `end_frame` | 整數 | 結束幀數 | `439` |
|
||||
| `frame_count` | 整數 | 總幀數 | `139` |
|
||||
|
||||
### 2.2 Chunk 類型說明
|
||||
|
||||
| 類型 | ID | 說明 | 來源處理器 |
|
||||
|------|-----|------|-----------|
|
||||
| `sentence` | `sentence` | 語音轉文字片段 | ASR 處理 |
|
||||
| `time` | `time_based` | 固定時間分段 | 系統自動切割 |
|
||||
| `cut` | `cut` | 場景變化片段 | CUT 處理 |
|
||||
| `trace` | `trace` | 軌跡追蹤片段 | YOLO 追蹤處理 |
|
||||
| `story` | `story` | 故事線片段(父子區塊) | Story 分析處理 |
|
||||
|
||||
**父子區塊關係**:
|
||||
- `story` 是**父區塊**,可包含多個 `sentence`、`cut`、`trace` 子區塊
|
||||
- 透過 `parent_chunk_id` 和 `child_chunk_ids` 建立階層關係
|
||||
|
||||
---
|
||||
|
||||
## 3. Content JSON 結構
|
||||
|
||||
每個 Chunk 的 `content` 欄位包含詳細的處理結果:
|
||||
|
||||
### 3.1 ASR Chunk(語音轉文字)
|
||||
|
||||
```json
|
||||
{
|
||||
"text": "今天天氣非常好,我們去郊外踏青吧",
|
||||
"words": [
|
||||
{
|
||||
"word": "今天",
|
||||
"start": 12.5,
|
||||
"end": 12.8,
|
||||
"confidence": 0.95
|
||||
},
|
||||
{
|
||||
"word": "天氣",
|
||||
"start": 12.8,
|
||||
"end": 13.1,
|
||||
"confidence": 0.92
|
||||
}
|
||||
],
|
||||
"language": "zh-TW",
|
||||
"speaker": null
|
||||
}
|
||||
```
|
||||
|
||||
### 3.2 Cut Chunk(場景偵測)
|
||||
|
||||
```json
|
||||
{
|
||||
"scenes": [
|
||||
{
|
||||
"scene_id": "cut_001",
|
||||
"start_time": 12.5,
|
||||
"end_time": 45.2,
|
||||
"transition": "cut",
|
||||
"confidence": 0.98
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### 3.3 Trace Chunk(軌跡追蹤)
|
||||
|
||||
```json
|
||||
{
|
||||
"track_id": "track_001",
|
||||
"object_class": "person",
|
||||
"frames": [
|
||||
{
|
||||
"frame": 300,
|
||||
"bbox": [120, 80, 200, 300],
|
||||
"confidence": 0.95
|
||||
},
|
||||
{
|
||||
"frame": 301,
|
||||
"bbox": [122, 82, 202, 302],
|
||||
"confidence": 0.94
|
||||
}
|
||||
],
|
||||
"total_frames": 180
|
||||
}
|
||||
```
|
||||
|
||||
### 3.4 Story Chunk(故事線)
|
||||
|
||||
```json
|
||||
{
|
||||
"story_id": "story_001",
|
||||
"title": "開場介紹",
|
||||
"summary": "主持人介紹節目主題",
|
||||
"child_chunk_ids": ["sentence_00001", "sentence_00002", "cut_00001"],
|
||||
"tags": ["intro", "host"]
|
||||
}
|
||||
```
|
||||
|
||||
### 3.5 Metadata(其他偵測資訊)
|
||||
|
||||
人臉(Face)、文字辨識(OCR)、姿態(Pose)等偵測結果會附加在 `metadata` 欄位:
|
||||
|
||||
```json
|
||||
{
|
||||
"metadata": {
|
||||
"faces": [
|
||||
{
|
||||
"bbox": [120, 80, 200, 180],
|
||||
"confidence": 0.87,
|
||||
"emotion": "neutral"
|
||||
}
|
||||
],
|
||||
"ocr": {
|
||||
"text": "MOMENTRY",
|
||||
"confidence": 0.96
|
||||
},
|
||||
"pose": {
|
||||
"keypoints": [
|
||||
{"name": "nose", "x": 192, "y": 85, "confidence": 0.95}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. 時間格式說明
|
||||
|
||||
### 4.1 秒數格式(常用)
|
||||
|
||||
```
|
||||
格式: 秒.幀數
|
||||
範例: 1234.60 = 第 1234 秒 + 第 60 幀
|
||||
```
|
||||
|
||||
### 4.2 時間軸格式
|
||||
|
||||
```
|
||||
格式: HH:MM:SS.FF
|
||||
範例: 00:20:34.12 = 20分34秒12幀
|
||||
```
|
||||
|
||||
### 4.3 幀數計算
|
||||
|
||||
```
|
||||
幀數 = 秒數 × fps
|
||||
例如: 12.5秒 × 24fps = 300幀
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. 實際資料範例
|
||||
|
||||
假設有一個影片,包含以下處理結果:
|
||||
|
||||
### 5.1 語音片段
|
||||
|
||||
```json
|
||||
{
|
||||
"uuid": "952f5854b9febad1",
|
||||
"chunk_id": "asr_00001",
|
||||
"chunk_type": "sentence",
|
||||
"start_time": 12.5,
|
||||
"end_time": 18.3,
|
||||
"content": {
|
||||
"text": "今天天氣非常好,我們去郊外踏青吧",
|
||||
"language": "zh-TW"
|
||||
},
|
||||
"text_content": "今天天氣非常好,我們去郊外踏青吧",
|
||||
"start_frame": 300,
|
||||
"end_frame": 439,
|
||||
"fps": 24.0
|
||||
}
|
||||
```
|
||||
|
||||
### 5.2 場景片段
|
||||
|
||||
```json
|
||||
{
|
||||
"uuid": "952f5854b9febad1",
|
||||
"chunk_id": "cut_00001",
|
||||
"chunk_type": "cut",
|
||||
"start_time": 45.0,
|
||||
"end_time": 120.5,
|
||||
"content": {
|
||||
"scenes": [{
|
||||
"scene_id": "cut_001",
|
||||
"transition": "cut",
|
||||
"confidence": 0.98
|
||||
}]
|
||||
},
|
||||
"start_frame": 1080,
|
||||
"end_frame": 2892,
|
||||
"fps": 24.0
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. 如何使用 Chunk
|
||||
|
||||
### 6.1 API 取得 Chunk
|
||||
|
||||
使用搜尋 API 取得 Chunk:
|
||||
|
||||
```bash
|
||||
curl -X POST "https://api.momentry.ddns.net/api/v1/search" \
|
||||
-H "X-API-Key: YOUR_API_KEY" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"query": "關鍵字",
|
||||
"limit": 10
|
||||
}'
|
||||
```
|
||||
|
||||
**指定影片搜尋**:
|
||||
```bash
|
||||
curl -X POST "https://api.momentry.ddns.net/api/v1/search" \
|
||||
-H "X-API-Key: YOUR_API_KEY" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"query": "關鍵字",
|
||||
"uuid": "39567a0eb16f39fd",
|
||||
"limit": 5
|
||||
}'
|
||||
```
|
||||
|
||||
### 6.2 搜尋相關片段
|
||||
|
||||
當使用者搜尋「天氣」時,系統會:
|
||||
|
||||
1. 將「天氣」轉換為向量
|
||||
2. 在向量資料庫中搜尋相似向量
|
||||
3. 找到相關的 Chunk
|
||||
4. 返回時間軸和內容
|
||||
|
||||
### 6.3 播放指定片段
|
||||
|
||||
取得 Chunk 後可播放:
|
||||
|
||||
```
|
||||
開始時間: 12.5 秒
|
||||
結束時間: 18.3 秒
|
||||
影片 UUID: 39567a0eb16f39fd
|
||||
```
|
||||
|
||||
**播放器連結格式**:
|
||||
```
|
||||
/player?uuid={uuid}&start={start_time}&end={end_time}
|
||||
```
|
||||
|
||||
### 6.4 組合多個 Chunk
|
||||
|
||||
多個相關 Chunk 可以組合成一個章節或故事線。
|
||||
|
||||
### 6.5 Story Chunk(父子關係)
|
||||
|
||||
Story Chunk 可包含多個子 Chunk:
|
||||
|
||||
```json
|
||||
{
|
||||
"chunk_id": "story_001",
|
||||
"chunk_type": "story",
|
||||
"content": {
|
||||
"story_id": "story_001",
|
||||
"title": "開場介紹",
|
||||
"child_chunk_ids": ["sentence_00001", "sentence_00002", "cut_00001"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. API 回應格式
|
||||
|
||||
### /search 回應
|
||||
|
||||
```json
|
||||
{
|
||||
"results": [
|
||||
{
|
||||
"uuid": "39567a0eb16f39fd",
|
||||
"chunk_id": "sentence_1471",
|
||||
"chunk_type": "sentence",
|
||||
"start_time": 5309.08,
|
||||
"end_time": 5311.08,
|
||||
"text": "influenced by a vital way,",
|
||||
"score": 0.68
|
||||
}
|
||||
],
|
||||
"query": "關鍵字"
|
||||
}
|
||||
```
|
||||
|
||||
### /n8n/search 回應
|
||||
|
||||
```json
|
||||
{
|
||||
"query": "關鍵字",
|
||||
"count": 1,
|
||||
"hits": [
|
||||
{
|
||||
"id": "sentence_1471",
|
||||
"vid": "39567a0eb16f39fd",
|
||||
"start": 5309.08,
|
||||
"end": 5311.08,
|
||||
"title": "Chunk sentence_1471",
|
||||
"text": "influenced by a vital way,",
|
||||
"score": 0.68,
|
||||
"file_path": "/Users/accusys/momentry/var/sftpgo/data/demo/video.mp4"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
> **注意**: `file_path` 是影片的實際路徑,可用於本地播放。
|
||||
|
||||
---
|
||||
|
||||
## 8. 快速參考
|
||||
|
||||
| 項目 | 說明 |
|
||||
|------|------|
|
||||
| UUID | 影片唯一識別碼(16位 hex) |
|
||||
| Chunk ID | 片段識別碼(如 `sentence_00001`) |
|
||||
| chunk_type | 片段類型(sentence/time/cut/trace/story) |
|
||||
| start_time | 開始時間(秒) |
|
||||
| end_time | 結束時間(秒) |
|
||||
| text_content | 純文字內容 |
|
||||
| content | 詳細 JSON 結構 |
|
||||
| metadata | 人臉、OCR、姿態等偵測結果 |
|
||||
| parent_chunk_id | 父區塊 ID(用於 story 區塊) |
|
||||
| child_chunk_ids | 子區塊 ID 列表(story 區塊專用) | |
|
||||
|
||||
---
|
||||
|
||||
## 附錄:版本歷史
|
||||
|
||||
| 版本 | 日期 | 內容 | 操作人 |
|
||||
|------|------|------|--------|
|
||||
| V1.0 | 2026-03-25 | 初版建立 | OpenCode |
|
||||
| V1.1 | 2026-03-25 | 新增 API 取得 Chunk 方式、播放連結格式 | OpenCode |
|
||||
@@ -1,534 +0,0 @@
|
||||
# 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;
|
||||
```
|
||||
1113
docs/CHUNK_SPEC.md
1113
docs/CHUNK_SPEC.md
File diff suppressed because it is too large
Load Diff
@@ -1,686 +0,0 @@
|
||||
# Momentry Core API 示範手冊
|
||||
|
||||
| 項目 | 內容 |
|
||||
|------|------|
|
||||
| 建立者 | OpenCode |
|
||||
| 建立時間 | 2026-03-25 |
|
||||
| 文件版本 | V1.0 |
|
||||
|
||||
---
|
||||
|
||||
## 版本歷史
|
||||
|
||||
| 版本 | 日期 | 目的 | 操作人 | 工具/模型 |
|
||||
|------|------|------|--------|-----------|
|
||||
| V1.0 | 2026-03-25 | 創建示範手冊,包含 Demo API Key 與完整範例 | OpenCode | deepseek-reasoner |
|
||||
|
||||
---
|
||||
|
||||
**狀態**: 完成
|
||||
|
||||
---
|
||||
|
||||
## 快速開始
|
||||
|
||||
### 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
|
||||
<?php
|
||||
/**
|
||||
* Momentry API Client
|
||||
*/
|
||||
|
||||
class Momentry_API {
|
||||
private const API_URL = 'https://api.momentry.ddns.net';
|
||||
private const API_KEY = 'muser_68600856036340bcafc01930eb4bd839_1774418104_97221b69';
|
||||
|
||||
/**
|
||||
* 發送 API 請求
|
||||
*/
|
||||
private function request(string $endpoint, array $data = [], string $method = 'GET'): array {
|
||||
$url = self::API_URL . $endpoint;
|
||||
|
||||
$args = [
|
||||
'headers' => [
|
||||
'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
|
||||
<?php
|
||||
/**
|
||||
* WordPress 短代碼範例
|
||||
*/
|
||||
|
||||
// 註冊短代碼
|
||||
add_shortcode('momentry_videos', function($atts) {
|
||||
$atts = shortcode_atts([
|
||||
'limit' => 10,
|
||||
], $atts);
|
||||
|
||||
$api = new Momentry_API();
|
||||
|
||||
try {
|
||||
$result = $api->list_videos();
|
||||
$videos = array_slice($result['videos'], 0, $atts['limit']);
|
||||
|
||||
ob_start();
|
||||
?>
|
||||
<div class="momentry-videos">
|
||||
<h3>影片列表</h3>
|
||||
<ul>
|
||||
<?php foreach ($videos as $video): ?>
|
||||
<li>
|
||||
<strong><?= esc_html($video['file_name']) ?></strong>
|
||||
<br>
|
||||
<small>
|
||||
UUID: <?= esc_html($video['uuid']) ?>
|
||||
| 時長: <?= gmdate("H:i:s", $video['duration']) ?>
|
||||
</small>
|
||||
</li>
|
||||
<?php endforeach; ?>
|
||||
</ul>
|
||||
</div>
|
||||
<?php
|
||||
return ob_get_clean();
|
||||
|
||||
} catch (Exception $e) {
|
||||
return '<p class="error">載入失敗: ' . esc_html($e->getMessage()) . '</p>';
|
||||
}
|
||||
});
|
||||
|
||||
// 搜尋短代碼
|
||||
add_shortcode('momentry_search', function($atts, $content = '') {
|
||||
$query = sanitize_text_field($content);
|
||||
|
||||
if (empty($query)) {
|
||||
return '<p>請提供搜尋關鍵字</p>';
|
||||
}
|
||||
|
||||
$api = new Momentry_API();
|
||||
|
||||
try {
|
||||
$result = $api->search($query);
|
||||
|
||||
ob_start();
|
||||
?>
|
||||
<div class="momentry-search-results">
|
||||
<h3>「<?= esc_html($query) ?>」搜尋結果</h3>
|
||||
<?php if (empty($result['results'])): ?>
|
||||
<p>沒有找到相關結果</p>
|
||||
<?php else: ?>
|
||||
<ul>
|
||||
<?php foreach ($result['results'] as $item): ?>
|
||||
<li>
|
||||
<a href="/video/<?= esc_attr($item['uuid']) ?>?t=<?= (int)$item['start_time'] ?>">
|
||||
<?= esc_html($item['text']) ?>
|
||||
</a>
|
||||
<br>
|
||||
<small>相似度: <?= round($item['score'] * 100) ?>%</small>
|
||||
</li>
|
||||
<?php endforeach; ?>
|
||||
</ul>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php
|
||||
return ob_get_clean();
|
||||
|
||||
} catch (Exception $e) {
|
||||
return '<p class="error">搜尋失敗: ' . esc_html($e->getMessage()) . '</p>';
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
### 3.3 使用方式
|
||||
|
||||
在 WordPress 頁面或文章中:
|
||||
|
||||
```
|
||||
[momentry_videos limit="5"]
|
||||
|
||||
[momentry_search]ExaSAN[/momentry_search]
|
||||
```
|
||||
|
||||
### 3.4 REST API 整合
|
||||
|
||||
```php
|
||||
<?php
|
||||
/**
|
||||
* 註冊 WordPress REST API 端點
|
||||
*/
|
||||
|
||||
add_action('rest_api_init', function() {
|
||||
register_rest_route('momentry/v1', '/search', [
|
||||
'methods' => '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
|
||||
@@ -1,540 +0,0 @@
|
||||
# Momentry Core 開發日誌
|
||||
|
||||
| 項目 | 內容 |
|
||||
|------|------|
|
||||
| 建立者 | Warren |
|
||||
| 建立時間 | 2026-03-18 |
|
||||
| 文件版本 | V1.0 |
|
||||
|
||||
---
|
||||
|
||||
## 版本歷史
|
||||
|
||||
| 版本 | 日期 | 目的 | 操作人 | 工具/模型 |
|
||||
|------|------|------|--------|-----------|
|
||||
| V1.0 | 2026-03-18 | 創建文件 | Warren | OpenCode / MiniMax M2.5 |
|
||||
|
||||
---
|
||||
|
||||
> **文檔維護開始**:2026-03-18
|
||||
> **⚠️ 補充說明**:事後補記(2026-03-18 以前),僅供參考。未來紀錄將即時記錄,參考價值較高。
|
||||
|
||||
---
|
||||
|
||||
## 開發工具
|
||||
|
||||
### Coding LLM 模型
|
||||
|
||||
| 階段 | 工具 | 模型 | ID | 說明 |
|
||||
|------|------|------|-----|------|
|
||||
| **初期** | Claude CLI | - | - | 初始專案架構建立 |
|
||||
| **中期** | OpenCode | big-pickle | opencode/big-pickle | 主要開發協作者 |
|
||||
|
||||
**切換記錄**:
|
||||
- 初期使用 Claude CLI 建立專案基本架構
|
||||
- 中期切換至 OpenCode (big-pickle) 進行主要功能開發
|
||||
|
||||
---
|
||||
|
||||
## 2026-03-17
|
||||
|
||||
### ML 模型選用
|
||||
|
||||
| Processor | 模型 | 版本/大小 | 說明 |
|
||||
|----------|------|-----------|------|
|
||||
| **ASR** | WhisperX (faster-whisper) | base, int8 | 語音識別 + 對話分段 |
|
||||
| **CUT** | PySceneDetect | 0.6.7.1 | ContentDetector 場景檢測 |
|
||||
| **YOLO** | YOLOv8n | yolov8n.pt (6.2MB) | 物體檢測(nano 版本最快) |
|
||||
| **OCR** | EasyOCR | 1.7.2 | 文字識別 |
|
||||
| **Face** | OpenCV Haar Cascade | built-in | 人臉檢測(無需額外下載) |
|
||||
| **Pose** | YOLOv8n-Pose | yolov8n-pose.pt (6.5MB) | 姿態估計(nano 版本) |
|
||||
|
||||
**模型下載**:
|
||||
- YOLOv8n: `yolov8n.pt` (6.2MB)
|
||||
- YOLOv8n-Pose: `yolov8n-pose.pt` (6.5MB)
|
||||
|
||||
**Python 依賴**:
|
||||
```
|
||||
torch==2.8.0
|
||||
whisperx==3.8.2
|
||||
ultralytics==8.4.23
|
||||
scenedetect==0.6.7.1
|
||||
easyocr==1.7.2
|
||||
opencv-python==4.13.0.92
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### ASR 實作完成
|
||||
- 完成 Python ML processor scripts(使用本地模型)
|
||||
- `asrx_processor.py` - whisperx for speaker diarization
|
||||
- `cut_processor.py` - PySceneDetect for scene detection
|
||||
- `yolo_processor.py` - YOLOv8 for object detection
|
||||
- `ocr_processor.py` - EasyOCR for text recognition
|
||||
- `face_processor.py` - OpenCV Haar Cascade for face detection
|
||||
- `pose_processor.py` - YOLOv8 Pose for pose estimation
|
||||
|
||||
- 更新 `requirements.txt` with all dependencies
|
||||
- 安裝完成:torch 2.8.0, whisperx 3.8.2, ultralytics 8.4.23, scenedetect 0.6.7.1, easyocr 1.7.2, opencv-python 4.13.0.92
|
||||
- 下載模型:YOLOv8n.pt (6.2MB), YOLOv8n-Pose.pt (6.5MB)
|
||||
|
||||
### Async Streaming 實作
|
||||
- 更新 Rust processor modules 使用 async streaming 進行 real-time progress
|
||||
- `src/core/processor/asr.rs`
|
||||
- `src/core/processor/cut.rs`
|
||||
- `src/core/processor/yolo.rs`
|
||||
- `src/core/processor/ocr.rs`
|
||||
- `src/core/processor/face.rs`
|
||||
- `src/core/processor/pose.rs`
|
||||
|
||||
### 測試結果
|
||||
- 測試影片:BigBuckBunny_320x180.mp4
|
||||
- ASR: 4 segments
|
||||
- CUT: 134 scenes
|
||||
- YOLO: 14315 frames(每幀處理耗時)
|
||||
- OCR: 40 frames with text
|
||||
- Face: 44 frames with faces
|
||||
- Pose: Timeout
|
||||
|
||||
---
|
||||
|
||||
### Warning 清理
|
||||
修復 clippy warnings:
|
||||
- 移除未使用的 imports (HashMap in mongodb_db.rs, postgres_db.rs)
|
||||
- 新增 `#[allow(dead_code)]` 標註未使用變數
|
||||
- 新增 `Default` implementation for MongoDb, QdrantDb
|
||||
- 將 `probe` module 重新命名為 `ffprobe`
|
||||
- 新增 `player` feature in Cargo.toml
|
||||
- 修復 `format_in_format_args` 警告
|
||||
|
||||
---
|
||||
|
||||
### TUI Progress Window 實作
|
||||
建立新的 UI module:
|
||||
- 建立 `src/ui/mod.rs`
|
||||
- 建立 `src/ui/progress/mod.rs`
|
||||
|
||||
實作功能:
|
||||
- ProcessorProgress 結構(追蹤每個 processor 狀態)
|
||||
- ProgressState 結構(管理所有 processors)
|
||||
- ProgressUi 結構(ratatui TUI 渲染)
|
||||
- 整合到 `src/main.rs` 的 process 命令
|
||||
|
||||
TUI 顯示:
|
||||
```
|
||||
┌ Processing: BigBuckBunny_320x180.mp4 ────────────────────────────────────────┐
|
||||
│ ASR [████████████] 100% (4 segs) │
|
||||
│ CUT [████████████] 100% (134 scenes) │
|
||||
│ ASRX [████████████] 100% (0 segs) │
|
||||
│ YOLO [██░░░░░░░░░░░] 30% (4200/14315) ETA 2:30 │
|
||||
│ OCR [---------] 0% │
|
||||
│ Face [---------] 0% │
|
||||
│ Pose [---------] 0% │
|
||||
└──────────────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 輸出位置討論
|
||||
討論 stdout vs stderr vs TUI 的輸出配置:
|
||||
- 最終結果 → stdout
|
||||
- Python progress → 需改用 Redis Pub/Sub
|
||||
- TUI Progress → stderr (ratatui)
|
||||
|
||||
---
|
||||
|
||||
## 2026-03-18
|
||||
|
||||
### Redis Message Bus 設計
|
||||
討論使用 Redis 作為消息總線,分離 Python 輸出與 Rust TUI 顯示。
|
||||
|
||||
設計重點:
|
||||
1. 頻道命名:`momentry:progress:{uuid}`
|
||||
2. 本地 Redis:`localhost:6379`
|
||||
3. 失敗策略:完全失效(因 stdout 問題未解決)
|
||||
|
||||
### UUID 使用時機分析
|
||||
分析 Redis Key 上使用 UUID 的時機:
|
||||
|
||||
**全局 Keys(無 UUID)**:
|
||||
- health, stats, jobs 管理
|
||||
|
||||
**Per-Video Keys(UUID 必要)**:
|
||||
- job:{uuid}, progress:{uuid}, metrics:{uuid}
|
||||
|
||||
**Per-Processor Keys(UUID + Processor 必要)**:
|
||||
- job:{uuid}:processor:{name}
|
||||
|
||||
### 備份系統整合
|
||||
參考 `docs/SERVICE_ADDITION_GUIDE.md` 設計規範,規劃 OutputDir 模組:
|
||||
|
||||
1. **環境變數**:
|
||||
- `MOMENTRY_OUTPUT_DIR` - JSON 輸出目錄
|
||||
- `MOMENTRY_BACKUP_DIR` - 備份目錄(預設:`/Users/accusys/momentry/backup/momentry`)
|
||||
- `MOMENTRY_BACKUP_ENABLED` - 啟用備份
|
||||
|
||||
2. **命名格式**:
|
||||
- 備份格式:`momentry_data_{YYYYMMDD}_{HHMMSS}_{uuid}.{ext}`
|
||||
- 校驗和:`{filename}.sha256`
|
||||
|
||||
3. **CLI 命令**:
|
||||
- `cargo run -- backup list` - 列出備份
|
||||
- `cargo run -- backup cleanup` - 清理舊備份
|
||||
- `cargo run -- backup verify` - 驗證備份
|
||||
|
||||
---
|
||||
|
||||
### 監控系統整合
|
||||
討論將 momentry_core 納入監控系統:
|
||||
|
||||
1. **Layer 2: Service 監控**
|
||||
- 新增 momentry_core CLI 檢查
|
||||
|
||||
2. **Layer 7: Backup 監控**
|
||||
- 新增 momentry 備份配置
|
||||
|
||||
3. **Redis 監控**
|
||||
- 健康檢查
|
||||
- Job 狀態監控
|
||||
- 即時進度監控
|
||||
|
||||
---
|
||||
|
||||
## 實作完成項目
|
||||
|
||||
### 程式碼變更
|
||||
|
||||
| 日期 | 檔案 | 變更 |
|
||||
|------|------|------|
|
||||
| 2026-03-17 | `src/core/processor/*.rs` | Async streaming 更新 |
|
||||
| 2026-03-17 | `src/ui/mod.rs` | 新增 UI module |
|
||||
| 2026-03-17 | `src/ui/progress/mod.rs` | 新增 Progress TUI |
|
||||
| 2026-03-17 | `src/main.rs` | 整合 Progress UI |
|
||||
| 2026-03-18 | `src/core/storage/output_dir.rs` | 新增 OutputDir 模組 |
|
||||
| 2026-03-18 | `src/core/storage/mod.rs` | 新增 output_dir export |
|
||||
| 2026-03-18 | `src/core/db/redis_client.rs` | 新增 Redis 客戶端(Hash + Pub/Sub) |
|
||||
| 2026-03-18 | `src/core/db/mod.rs` | 新增 redis_client export |
|
||||
|
||||
### 新增檔案
|
||||
|
||||
| 日期 | 檔案 | 說明 |
|
||||
|------|------|------|
|
||||
| 2026-03-18 | `docs/MOMENTRY_CORE_REDIS_KEYS.md` | Redis Key 設計規範 |
|
||||
| 2026-03-18 | `docs/MOMENTRY_CORE_MONITORING.md` | 監控規範(暫定) |
|
||||
| 2026-03-18 | `scripts/redis_publisher.py` | Redis 訊息發布模組 |
|
||||
|
||||
### 更新檔案
|
||||
|
||||
| 日期 | 檔案 | 說明 |
|
||||
|------|------|------|
|
||||
| 2026-03-17 | `Cargo.toml` | 新增 player feature |
|
||||
| 2026-03-17 | `src/lib.rs` | 新增 ui module exports |
|
||||
| 2026-03-18 | `docs/PENDING_ISSUES.md` | 新增問題 #2, #3 |
|
||||
| 2026-03-18 | `src/core/storage/output_dir.rs` | 預設改為 `./output` |
|
||||
| 2026-03-18 | `scripts/yolo_processor.py` | 新增 --uuid 參數 + Redis |
|
||||
| 2026-03-18 | `scripts/cut_processor.py` | 新增 Redis |
|
||||
| 2026-03-18 | `scripts/ocr_processor.py` | 新增 Redis |
|
||||
| 2026-03-18 | `scripts/face_processor.py` | 新增 --uuid 參數 + Redis |
|
||||
| 2026-03-18 | `scripts/pose_processor.py` | 新增 --uuid 參數 + Redis |
|
||||
| 2026-03-18 | `scripts/asr_processor.py` | 新增 --uuid 參數 + Redis |
|
||||
| 2026-03-18 | `scripts/asrx_processor.py` | 新增 --uuid 參數 + Redis |
|
||||
| 2026-03-18 | `requirements.txt` | 新增 redis>=5.0.0 |
|
||||
| 2026-03-18 | `src/core/processor/yolo.rs` | 新增 uuid 參數 |
|
||||
| 2026-03-18 | `src/core/processor/cut.rs` | 新增 uuid 參數 |
|
||||
| 2026-03-18 | `src/core/processor/ocr.rs` | 新增 uuid 參數 |
|
||||
| 2026-03-18 | `src/core/processor/face.rs` | 新增 uuid 參數 |
|
||||
| 2026-03-18 | `src/core/processor/pose.rs` | 新增 uuid 參數 |
|
||||
| 2026-03-18 | `src/core/processor/asr.rs` | 新增 uuid 參數 |
|
||||
| 2026-03-18 | `src/core/processor/asrx.rs` | 新增 uuid 參數 |
|
||||
| 2026-03-18 | `src/main.rs` | 更新所有 processor 調用傳入 uuid |
|
||||
| 2026-03-18 | `Cargo.toml` | 新增 futures-util 依賴 |
|
||||
| 2026-03-18 | `src/core/db/redis_client.rs` | 新增 subscribe_and_callback 方法,密碼認證 |
|
||||
| 2026-03-18 | `src/ui/progress/mod.rs` | 新增 update_from_redis 方法 |
|
||||
| 2026-03-18 | `scripts/redis_publisher.py` | 新增密碼認證支援 |
|
||||
| 2026-03-18 | 測試 | Redis Pub/Sub 成功運作 |
|
||||
|
||||
---
|
||||
|
||||
## 待解決問題
|
||||
|
||||
### 問題 #1: sqlx async INSERT 不會實際寫入數據庫
|
||||
- 狀態:待解決
|
||||
- 影響:`store_vector` 函數,PVector 存儲
|
||||
|
||||
### 問題 #2: TUI 與 stdout 輸出混合
|
||||
- 狀態:已解決
|
||||
- 解決方案:使用 Redis Message Bus
|
||||
- 進度:
|
||||
- ✅ Redis 客戶端 (`src/core/db/redis_client.rs`)
|
||||
- ✅ Python redis_publisher.py
|
||||
- ✅ 所有 Python processors 更新完成
|
||||
- ✅ 所有 Rust processor 函數更新完成
|
||||
- ✅ main.rs 調用更新完成
|
||||
- ✅ Rust TUI Redis 訂閱已完成
|
||||
|
||||
### 問題 #3: Redis Message Bus 尚未實作
|
||||
- 狀態:已解決
|
||||
- 詳細設計:參考 `docs/MOMENTRY_CORE_REDIS_KEYS.md`
|
||||
- 進度:Python 端 + Rust 端均已完成
|
||||
|
||||
---
|
||||
|
||||
## 環境變數
|
||||
|
||||
```bash
|
||||
# 輸出目錄
|
||||
MOMENTRY_OUTPUT_DIR=./output # 預設
|
||||
|
||||
# 備份
|
||||
MOMENTRY_BACKUP_ENABLED=false # 預設
|
||||
MOMENTRY_BACKUP_DIR=/Users/accusys/momentry/backup/momentry
|
||||
|
||||
# Redis(未來實作)
|
||||
REDIS_URL=redis://localhost:6379
|
||||
REDIS_PASSWORD=accusys
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 數據庫
|
||||
|
||||
- PostgreSQL: `postgres://accusys@localhost:5432/momentry`
|
||||
- Redis: `localhost:6379`(待實作)
|
||||
- Qdrant: `localhost:6333`
|
||||
|
||||
---
|
||||
|
||||
## 指令範例
|
||||
|
||||
```bash
|
||||
# 註冊視頻
|
||||
cargo run -- register /path/to/video.mp4
|
||||
|
||||
# 處理視頻
|
||||
cargo run -- process <uuid>
|
||||
|
||||
# 列出備份
|
||||
cargo run -- backup list
|
||||
|
||||
# 清理備份
|
||||
cargo run -- backup cleanup
|
||||
|
||||
# 驗證備份
|
||||
cargo run -- backup verify
|
||||
|
||||
# 查看狀態
|
||||
cargo run -- status
|
||||
|
||||
# API Server
|
||||
cargo run -- server --host 0.0.0.0 --port 3000
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2026-03-18 (進行中)
|
||||
|
||||
### Redis Message Bus 實作
|
||||
|
||||
**問題**:TUI 與 Python stdout 輸出混合,導致 TUI 顯示混亂
|
||||
|
||||
**解決方案**:使用 Redis Pub/Sub 作為訊息匯流排
|
||||
|
||||
**實作內容**:
|
||||
|
||||
| 元件 | 檔案 | 狀態 |
|
||||
|------|------|------|
|
||||
| Redis 客戶端 | `src/core/db/redis_client.rs` | ✅ |
|
||||
| Progress 訂閱 | `src/main.rs` | ✅ |
|
||||
| UI 更新 | `src/ui/progress/mod.rs` | ✅ |
|
||||
| Python Publisher | `scripts/redis_publisher.py` | ✅ |
|
||||
| Python Processors | 7 個 `scripts/*_processor.py` | ✅ |
|
||||
| Rust 函數 | `src/core/processor/*.rs` | ✅ |
|
||||
|
||||
**流程**:
|
||||
```
|
||||
Python Processor ──(Redis Pub)──> Redis ──(Subscribe)──> Rust TUI
|
||||
```
|
||||
|
||||
**測試結果**:
|
||||
- Redis 連線 ✅
|
||||
- 密碼認證 ✅
|
||||
- 即時進度發布 ✅
|
||||
- TUI 即時更新 ✅
|
||||
|
||||
**新增依賴**:
|
||||
- `futures-util = "0.3"` (Cargo.toml)
|
||||
- `redis >= 5.0.0` (requirements.txt)
|
||||
|
||||
---
|
||||
|
||||
## 2026-03-18 (HTTP API)
|
||||
|
||||
### HTTP API 實作
|
||||
|
||||
**問題**:TUI 運作正常但使用者偏好 HTTP API 來查詢進度
|
||||
|
||||
**解決方案**:建立 HTTP 端點 + Redis Hash 儲存
|
||||
|
||||
**實作內容**:
|
||||
|
||||
| 元件 | 檔案 | 變更 |
|
||||
|------|------|------|
|
||||
| HTTP 端點 | `src/api/server.rs` | 新增 `/api/v1/progress/:uuid` |
|
||||
| Redis Hash 查詢 | `src/core/db/redis_client.rs` | 新增 `get_processor_status` 方法 |
|
||||
| Progress 儲存 | `src/main.rs` | 新增 Redis HSET 儲存進度 |
|
||||
|
||||
**API 端點**:
|
||||
```
|
||||
GET /api/v1/progress/:uuid
|
||||
|
||||
Response:
|
||||
{
|
||||
"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": "complete", "current": 14300, "total": 14315, "message": "..."},
|
||||
...
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**流程**:
|
||||
```
|
||||
Python Processor ──(Redis Pub)──> Redis ──(Subscribe)──> Rust TUI
|
||||
└──(HSET)──> Redis Hash
|
||||
│
|
||||
HTTP Client ──(GET /progress/:uuid)──> Rust API ─(HGETALL)──> Redis Hash
|
||||
```
|
||||
|
||||
**測試結果**:
|
||||
- ✅ 編譯成功
|
||||
- ✅ API 伺服器啟動 (port 3002)
|
||||
- ✅ 即時進度查詢
|
||||
- ✅ 完整流程測試 (BigBuckBunny_320x180.mp4)
|
||||
|
||||
**除錯記錄**:
|
||||
1. 語法錯誤:main.rs 有重複程式碼區塊 (lines 297-322),已移除
|
||||
2. DB 連線池:從 5 增加到 10 個連線
|
||||
3. PostgreSQL 狀態:處理 shutdown 狀態,殺掉 stale 連線
|
||||
|
||||
**新增變更**:
|
||||
- `src/api/server.rs` - 新增進度端點
|
||||
- `src/core/db/redis_client.rs` - 新增 `get_processor_status` 方法
|
||||
- `src/core/db/postgres_db.rs` - 連線池 5→10
|
||||
- `src/main.rs` - Redis Hash 儲存 + 語法修復
|
||||
|
||||
**使用方式**:
|
||||
```bash
|
||||
# 啟動 API 伺服器
|
||||
cargo run --bin momentry -- server --host 127.0.0.1 --port 3002
|
||||
|
||||
# 註冊影片
|
||||
cargo run --bin momentry -- register ~/test_video/BigBuckBunny_320x180.mp4
|
||||
|
||||
# 處理影片
|
||||
cargo run --bin momentry -- process <uuid>
|
||||
|
||||
# 查詢進度
|
||||
curl http://127.0.0.1:3002/api/v1/progress/<uuid>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 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 實時推送
|
||||
- [ ] 移動端響應式設計
|
||||
@@ -1,474 +0,0 @@
|
||||
# 文件創建規範
|
||||
|
||||
| 項目 | 內容 |
|
||||
|------|------|
|
||||
| 建立者 | Warren |
|
||||
| 建立時間 | 2026-03-18 |
|
||||
| 文件版本 | V1.0 |
|
||||
|
||||
---
|
||||
|
||||
## 版本歷史
|
||||
|
||||
| 版本 | 日期 | 目的 | 操作人 | 工具/模型 |
|
||||
|------|------|------|--------|-----------|
|
||||
| V1.0 | 2026-03-18 | 創建文件規範 | Warren | OpenCode / MiniMax M2.5 |
|
||||
|
||||
---
|
||||
|
||||
本文檔定義 Momentry Core 專案中文件的命名規範、格式標準和結構要求。
|
||||
|
||||
---
|
||||
|
||||
## 1. 檔案命名規範
|
||||
|
||||
### 命名模式
|
||||
|
||||
所有文件必須使用以下命名模式:
|
||||
|
||||
| 文件類型 | 模式 | 範例 |
|
||||
|----------|------|------|
|
||||
| 安裝指南 | `INSTALL_<NAME>.md` | `INSTALL_POSTGRESQL.md` |
|
||||
| 開發指南 | `DEVELOP_<NAME>.md` | `DEVELOP_API.md` |
|
||||
| API 參考 | `API_REFERENCE.md` | `API_REFERENCE.md` |
|
||||
| 規格文件 | `<NAME>_SPEC.md` | `CHUNK_SPEC.md` |
|
||||
| 設計文件 | `<NAME>_DESIGN.md` | `CHUNK_DESIGN.md` |
|
||||
| 服務總覽 | `SERVICES.md` | `SERVICES.md` |
|
||||
| 其他文件 | `<NAME>.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
|
||||
| 項目 | 內容 |
|
||||
|------|------|
|
||||
| 建立者 | <姓名> |
|
||||
| 建立時間 | <YYYY-MM-DD> |
|
||||
| 文件版本 | V1.0 |
|
||||
```
|
||||
|
||||
版本歷史表:
|
||||
|
||||
```markdown
|
||||
---
|
||||
|
||||
## 版本歷史
|
||||
|
||||
| 版本 | 日期 | 目的 | 操作人 | 工具/模型 |
|
||||
|------|------|------|--------|-----------|
|
||||
| V1.0 | 2026-03-18 | 創建文件 | Warren | OpenCode / MiniMax M2.5 |
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 版本資訊章節格式
|
||||
|
||||
```markdown
|
||||
---
|
||||
|
||||
## 版本資訊
|
||||
|
||||
- 版本: <版本號>
|
||||
- 安裝日期: <YYYY-MM-DD>
|
||||
- 文件更新: <YYYY-MM-DD>
|
||||
```
|
||||
|
||||
### 狀態標記
|
||||
|
||||
| 狀態 | 標記 |
|
||||
|------|------|
|
||||
| 已安裝 | ✅ 已安裝 v<x.x.x> |
|
||||
| 未安裝 | ❌ 未安裝 |
|
||||
| 可選 | ⚙️ 可選 |
|
||||
| 進行中 | 🔄 進行中 |
|
||||
|
||||
---
|
||||
|
||||
## 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` | 變更日誌 | `/` |
|
||||
@@ -1,167 +0,0 @@
|
||||
# Document Embedding Strategy - Parent-Child Chunks
|
||||
|
||||
| Item | Content |
|
||||
|------|---------|
|
||||
| Author | Warren |
|
||||
| Created | 2026-03-23 |
|
||||
| Document Version | V1.0 |
|
||||
|
||||
---
|
||||
|
||||
## Version History
|
||||
|
||||
| Version | Date | Purpose | Operator | Tool/Model |
|
||||
|---------|------|---------|----------|------------|
|
||||
| V1.0 | 2026-03-23 | Create document embedding strategy | Warren | OpenCode |
|
||||
|
||||
---
|
||||
|
||||
## 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
|
||||
@@ -1,323 +0,0 @@
|
||||
# 文件修改管理規範 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 <file_path>
|
||||
|
||||
# 3. 查看現有版本歷史
|
||||
cat docs/<file>.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
|
||||
## 檔案
|
||||
`<file_path>`
|
||||
|
||||
## 修改原因
|
||||
<說明修改的目的>
|
||||
|
||||
## 變更內容
|
||||
```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 <file_path>
|
||||
```
|
||||
|
||||
### 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 <files>
|
||||
ruff format --check <files>
|
||||
|
||||
# Markdown 文件
|
||||
markdownlint <files>
|
||||
|
||||
# Shell 文件
|
||||
shellcheck -S error <files>
|
||||
```
|
||||
|
||||
### 5.2 手動驗證清單
|
||||
|
||||
```
|
||||
□ 1. 文件語法正確
|
||||
□ 2. 連結有效
|
||||
□ 3. 格式一致
|
||||
□ 4. 術語一致
|
||||
□ 5. 版本歷史完整
|
||||
□ 6. 變更記錄清晰
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. 提交規範
|
||||
|
||||
### 6.1 Commit Message 格式
|
||||
|
||||
```
|
||||
<type>: <subject>
|
||||
|
||||
<body>
|
||||
|
||||
<footer>
|
||||
```
|
||||
|
||||
### 6.2 Type 類型
|
||||
|
||||
| Type | 說明 |
|
||||
|------|------|
|
||||
| `feat` | 新功能 |
|
||||
| `fix` | 錯誤修復 |
|
||||
| `refactor` | 重構 |
|
||||
| `docs` | 文檔更新 |
|
||||
| `style` | 格式變更 |
|
||||
| `test` | 測試相關 |
|
||||
| `chore` | 維護工作 |
|
||||
|
||||
### 6.3 Commit Message 範例
|
||||
|
||||
```bash
|
||||
# 文檔更新
|
||||
git commit -m "docs: update INSTALL_MONGODB.md with LaunchAgent instructions
|
||||
|
||||
- Add LaunchDaemon plist installation steps
|
||||
- Update management commands section
|
||||
- Add version history entry
|
||||
|
||||
Closes: #xxx"
|
||||
|
||||
# 配置文件更新
|
||||
git commit -m "fix: remove duplicate mongodb entry in monitor_config.yaml
|
||||
|
||||
The backup section had two mongodb entries which caused confusion.
|
||||
Removed the duplicate config entry at line 408-414."
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. AI 工具修改額外規範
|
||||
|
||||
### 7.1 修改前確認
|
||||
|
||||
AI 工具修改文件前,必須:
|
||||
|
||||
1. **完整閱讀**:閱讀完整文件(不可只讀取部分)
|
||||
2. **理解語境**:理解文件的用途和目標讀者
|
||||
3. **查閱相關**:查閱相關聯文件確保一致性
|
||||
4. **提出計畫**:修改前先提出變更計畫供確認
|
||||
|
||||
### 7.2 修改後確認
|
||||
|
||||
AI 工具修改文件後,必須:
|
||||
|
||||
1. **展示差異**:顯示修改的 diff 內容
|
||||
2. **說明變更**:解釋每項變更的目的
|
||||
3. **執行驗證**:執行相關的 lint/test 命令
|
||||
4. **更新歷史**:更新版本歷史表
|
||||
|
||||
### 7.3 禁止事項
|
||||
|
||||
AI 工具**禁止**以下行為:
|
||||
|
||||
```
|
||||
□ 未閱讀完整文件即進行修改
|
||||
□ 未經確認直接執行大規模修改
|
||||
□ 未提供 diff 即提交變更
|
||||
□ 未更新版本歷史
|
||||
□ 未執行驗證即聲稱完成
|
||||
□ 擅自刪除其他工具/模型添加的內容
|
||||
□ 無視現有文件規範
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. 審查清單
|
||||
|
||||
### 8.1 提交前審查
|
||||
|
||||
```
|
||||
□ 1. 修改內容已完整閱讀
|
||||
□ 2. 預檢問題清單已回答
|
||||
□ 3. diff 對照已確認
|
||||
□ 4. 版本歷史已更新
|
||||
□ 5. 自動化驗證已通過
|
||||
□ 6. 手動驗證清單已完成
|
||||
□ 7. Commit message 符合規範
|
||||
□ 8. 無敏感資訊泄露
|
||||
```
|
||||
|
||||
### 8.2 Code Review 關注點
|
||||
|
||||
- [ ] 修改是否影響現有功能?
|
||||
- [ ] 修改是否與專案規範一致?
|
||||
- [ ] 是否有遺漏的關聯修改?
|
||||
- [ ] 測試是否足夠?
|
||||
- [ ] 文檔是否同步更新?
|
||||
|
||||
---
|
||||
|
||||
## 9. 版本歷史
|
||||
|
||||
| 版本 | 日期 | 目的 | 操作人 | 工具/模型 |
|
||||
|------|------|------|--------|-----------|
|
||||
| V1.0 | 2026-03-22 | 創建文件 | Warren | OpenCode / big-pickle |
|
||||
@@ -1,707 +0,0 @@
|
||||
# Momentry 系統全新 Mac 安裝指南
|
||||
|
||||
| 項目 | 內容 |
|
||||
|------|------|
|
||||
| 建立時間 | 2026-03-23 |
|
||||
| 文件版本 | V1.0 |
|
||||
| 適用對象 | 全新 Mac (Intel 或 Apple Silicon) |
|
||||
|
||||
---
|
||||
|
||||
## 版本歷史
|
||||
|
||||
| 版本 | 日期 | 目的 | 操作人 |
|
||||
|------|------|------|--------|
|
||||
| V1.0 | 2026-03-23 | 創建文件 | OpenCode |
|
||||
|
||||
---
|
||||
|
||||
## 快速概覽
|
||||
|
||||
### 安裝時間估算
|
||||
|
||||
| 項目 | 時間 |
|
||||
|------|------|
|
||||
| 系統準備 | 30 分鐘 |
|
||||
| Homebrew 安裝 | 15 分鐘 |
|
||||
| 資料庫服務 | 30 分鐘 |
|
||||
| 應用服務 | 60 分鐘 |
|
||||
| Momentry Core | 20 分鐘 |
|
||||
| **總計** | **~2.5 小時** |
|
||||
|
||||
### 系統需求
|
||||
|
||||
| 項目 | 最低需求 | 推薦需求 |
|
||||
|------|----------|----------|
|
||||
| macOS | 13.0 Ventura | 14.0+ Sonoma |
|
||||
| 記憶體 | 8GB | 16GB+ |
|
||||
| 儲存空間 | 100GB | 500GB+ |
|
||||
| CPU | Apple Silicon M1 或 Intel i5 | M2/M3 或 Intel i7+ |
|
||||
|
||||
---
|
||||
|
||||
## 第一部分:系統準備
|
||||
|
||||
### 1.1 macOS 初始設定
|
||||
|
||||
首次開機後,執行以下設定:
|
||||
|
||||
```bash
|
||||
# 1. 設定電腦名稱
|
||||
sudo scutil --set ComputerName "Momentry"
|
||||
sudo scutil --set LocalHostName "momentry"
|
||||
|
||||
# 2. 啟用 SSH 遠端登入
|
||||
sudo systemsetup -setremotelogin on
|
||||
|
||||
# 3. 關閉休眠 (防止遠端斷線)
|
||||
sudo pmset -a sleep 0
|
||||
sudo pmset -a hibernatemode 0
|
||||
|
||||
# 4. 允許任何來源 App (安裝開發工具用)
|
||||
sudo spctl --master-disable
|
||||
|
||||
# 5. 設定時區
|
||||
sudo systemsetup -settimezone "Asia/Taipei"
|
||||
```
|
||||
|
||||
### 1.2 建立管理員帳戶
|
||||
|
||||
```bash
|
||||
# 建立 Momentry 管理員帳戶 (可選)
|
||||
sudo dscl . -create /Users/momentry
|
||||
sudo dscl . -create /Users/momentry UserShell /bin/zsh
|
||||
sudo dscl . -create /Users/momentry RealName "Momentry Admin"
|
||||
sudo dscl . -create /Users/momentry PrimaryGroupID 80
|
||||
sudo dscl . -create /Users/momentry UniqueID 1001
|
||||
sudo dscl . -passwd /Users/momentry "momentry_password"
|
||||
sudo dscl . -append /Groups/admin GroupMembership momentry
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 第二部分:Homebrew 安裝
|
||||
|
||||
### 2.1 安裝 Homebrew
|
||||
|
||||
```bash
|
||||
# 安裝 Homebrew
|
||||
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
|
||||
|
||||
# Apple Silicon 設定
|
||||
echo 'eval "$(/opt/homebrew/bin/brew shellenv)"' >> ~/.zprofile
|
||||
eval "$(/opt/homebrew/bin/brew shellenv)"
|
||||
|
||||
# Intel Mac 設定
|
||||
# echo 'eval "$(/usr/local/bin/brew shellenv)"' >> ~/.zprofile
|
||||
# eval "$(/usr/local/bin/brew shellenv)"
|
||||
|
||||
# 驗證
|
||||
brew --version
|
||||
```
|
||||
|
||||
### 2.2 安裝基礎工具
|
||||
|
||||
```bash
|
||||
# 開發工具
|
||||
brew install \
|
||||
git \
|
||||
curl \
|
||||
wget \
|
||||
jq \
|
||||
yq \
|
||||
tree \
|
||||
htop \
|
||||
tmux \
|
||||
zsh \
|
||||
zsh-completions \
|
||||
ncdu \
|
||||
httpie \
|
||||
openssl \
|
||||
readline \
|
||||
sqlite \
|
||||
python@3.11 \
|
||||
rustup-init
|
||||
|
||||
# Rust 初始化
|
||||
rustup-init -y
|
||||
source "$HOME/.cargo/env"
|
||||
|
||||
# Python 虛擬環境
|
||||
python3.11 -m venv ~/.venv
|
||||
source ~/.venv/bin/activate
|
||||
pip install --upgrade pip
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 第三部分:建立目錄結構
|
||||
|
||||
```bash
|
||||
# 建立目錄結構
|
||||
mkdir -p /Users/accusys/momentry/{var,etc,log,scripts,backup}
|
||||
mkdir -p /Users/accusys/momentry/var/{postgresql,mongodb,mariadb,redis,qdrant,n8n,ollama,sftpgo}
|
||||
mkdir -p /Users/accusys/momentry/etc/{sftpgo,caddy,gitea,php}
|
||||
mkdir -p /Users/accusys/momentry/scripts
|
||||
mkdir -p /Users/accusys/momentry/backup/{daily,weekly,monthly}
|
||||
mkdir -p /Users/accusys/workspace/sftpgo
|
||||
mkdir -p /Users/accusys/sftpgo_test/{demo,uploads}
|
||||
|
||||
# 設定權限
|
||||
chown -R accusys:staff /Users/accusys/momentry
|
||||
chown -R accusys:staff /Users/accusys/workspace
|
||||
chown -R accusys:staff /Users/accusys/sftpgo_test
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 第四部分:資料庫服務安裝
|
||||
|
||||
### 4.1 PostgreSQL
|
||||
|
||||
```bash
|
||||
# 安裝
|
||||
brew install postgresql@18
|
||||
|
||||
# 啟動服務
|
||||
brew services start postgresql@18
|
||||
|
||||
# 建立資料庫
|
||||
createdb momentry
|
||||
createdb video_register
|
||||
createdb n8n
|
||||
createdb sftpgo
|
||||
|
||||
# 建立用戶
|
||||
psql -U accusys -d postgres << 'EOF'
|
||||
CREATE USER sftpgo WITH PASSWORD 'sftpgo_pass_2026';
|
||||
GRANT ALL PRIVILEGES ON DATABASE sftpgo TO sftpgo;
|
||||
GRANT ALL PRIVILEGES ON DATABASE momentry TO accusys;
|
||||
GRANT ALL PRIVILEGES ON DATABASE video_register TO accusys;
|
||||
GRANT ALL PRIVILEGES ON DATABASE n8n TO accusys;
|
||||
EOF
|
||||
|
||||
# 驗證
|
||||
pg_isready -h 127.0.0.1 -p 5432
|
||||
```
|
||||
|
||||
### 4.2 MongoDB
|
||||
|
||||
```bash
|
||||
# 安裝
|
||||
brew tap mongodb/brew
|
||||
brew install mongodb-community
|
||||
|
||||
# 建立資料目錄
|
||||
mkdir -p /Users/accusys/momentry/var/mongodb
|
||||
chown -R accusys:staff /Users/accusys/momentry/var/mongodb
|
||||
|
||||
# 啟動服務
|
||||
brew services start mongodb-community
|
||||
|
||||
# 建立用戶
|
||||
mongosh admin --eval "
|
||||
db.createUser({
|
||||
user: 'accusys',
|
||||
pwd: 'Test3200Test3200',
|
||||
roles: [
|
||||
{ role: 'readWriteAnyDatabase', db: 'admin' },
|
||||
{ role: 'dbAdminAnyDatabase', db: 'admin' }
|
||||
]
|
||||
});
|
||||
"
|
||||
|
||||
# 驗證
|
||||
mongosh --quiet --eval "db.adminCommand('ping')"
|
||||
```
|
||||
|
||||
### 4.3 Redis
|
||||
|
||||
```bash
|
||||
# 安裝
|
||||
brew install redis
|
||||
|
||||
# 建立配置目錄
|
||||
mkdir -p /Users/accusys/momentry/etc/redis
|
||||
|
||||
# 建立配置文件
|
||||
cat > /Users/accusys/momentry/etc/redis/redis.conf << 'EOF'
|
||||
requirepass accusys
|
||||
dir /Users/accusys/momentry/var/redis
|
||||
logfile /Users/accusys/momentry/log/redis.log
|
||||
daemonize no
|
||||
port 6379
|
||||
bind 127.0.0.1
|
||||
EOF
|
||||
|
||||
# 啟動服務
|
||||
redis-server /Users/accusys/momentry/etc/redis/redis.conf &
|
||||
sleep 2
|
||||
|
||||
# 驗證
|
||||
redis-cli -a accusys ping
|
||||
```
|
||||
|
||||
### 4.4 MariaDB (WordPress)
|
||||
|
||||
```bash
|
||||
# 安裝
|
||||
brew install mariadb
|
||||
|
||||
# 啟動服務
|
||||
brew services start mariadb
|
||||
|
||||
# 安全設定
|
||||
mysql_secure_installation
|
||||
|
||||
# 建立資料庫
|
||||
mysql -u root << 'EOF'
|
||||
CREATE DATABASE wordpress CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
CREATE USER 'wordpress'@'localhost' IDENTIFIED BY 'wordpress_password';
|
||||
GRANT ALL PRIVILEGES ON wordpress.* TO 'wordpress'@'localhost';
|
||||
FLUSH PRIVILEGES;
|
||||
EOF
|
||||
|
||||
# 驗證
|
||||
mysql -u root -e "SHOW DATABASES;"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 第五部分:應用服務安裝
|
||||
|
||||
### 5.1 Ollama (LLM)
|
||||
|
||||
```bash
|
||||
# 安裝
|
||||
brew install ollama
|
||||
|
||||
# 建立資料目錄
|
||||
mkdir -p /Users/accusys/momentry/var/ollama
|
||||
mkdir -p ~/.ollama/models
|
||||
|
||||
# 啟動服務
|
||||
brew services start ollama
|
||||
|
||||
# 下載模型
|
||||
ollama pull llama3.2
|
||||
ollama pull nomic-embed-text
|
||||
|
||||
# 驗證
|
||||
curl -s http://localhost:11434/api/tags | jq '.models[].name'
|
||||
```
|
||||
|
||||
### 5.2 Caddy (反向代理)
|
||||
|
||||
```bash
|
||||
# 安裝
|
||||
brew install caddy
|
||||
|
||||
# 建立配置目錄
|
||||
mkdir -p /Users/accusys/momentry/etc/caddy
|
||||
|
||||
# 建立 Caddyfile
|
||||
cat > /Users/accusys/momentry/etc/caddy/Caddyfile << 'EOF'
|
||||
{
|
||||
admin off
|
||||
auto_https off
|
||||
}
|
||||
|
||||
# 範例:本地開發
|
||||
:8080 {
|
||||
respond "Momentry Server" 200
|
||||
}
|
||||
EOF
|
||||
|
||||
# 啟動服務
|
||||
brew services start caddy
|
||||
|
||||
# 驗證
|
||||
curl -s http://localhost:8080
|
||||
```
|
||||
|
||||
### 5.3 Gitea (Git 服務)
|
||||
|
||||
```bash
|
||||
# 安裝
|
||||
brew install gitea
|
||||
|
||||
# 建立資料目錄
|
||||
mkdir -p /Users/accusys/momentry/var/gitea
|
||||
mkdir -p /Users/accusys/momentry/etc/gitea
|
||||
|
||||
# 複製設定檔
|
||||
cp /opt/homebrew/etc/gitea/conf/app.ini /Users/accusys/momentry/etc/gitea/
|
||||
|
||||
# 啟動服務
|
||||
brew services start gitea
|
||||
|
||||
# 驗證
|
||||
curl -s http://localhost:3000
|
||||
```
|
||||
|
||||
### 5.4 n8n (工作流程自動化)
|
||||
|
||||
```bash
|
||||
# 安裝 Node.js (如果尚未安裝)
|
||||
brew install node@22
|
||||
|
||||
# 全域安裝 n8n
|
||||
npm install -g n8n
|
||||
|
||||
# 建立資料目錄
|
||||
mkdir -p /Users/accusys/momentry/var/n8n
|
||||
|
||||
# 設定環境變數
|
||||
export N8N_DATA_DIR=/Users/accusys/momentry/var/n8n
|
||||
export DB_TYPE=postgresdb
|
||||
export DB_POSTGRES_HOST=127.0.0.1
|
||||
export DB_POSTGRES_PORT=5432
|
||||
export DB_POSTGRES_DATABASE=n8n
|
||||
export DB_POSTGRES_USER=accusys
|
||||
export DB_POSTGRES_PASSWORD=accusys
|
||||
export N8N_ENCRYPTION_KEY=Test3200Test3200Test3200
|
||||
|
||||
# 啟動服務
|
||||
n8n start &
|
||||
|
||||
# 驗證
|
||||
curl -s http://localhost:5678
|
||||
```
|
||||
|
||||
### 5.5 SFTPGo (SFTP 服務)
|
||||
|
||||
```bash
|
||||
# 安裝
|
||||
brew install sftpgo
|
||||
|
||||
# 建立配置目錄
|
||||
mkdir -p /Users/accusys/momentry/etc/sftpgo
|
||||
|
||||
# 建立配置文件
|
||||
cat > /Users/accusys/momentry/etc/sftpgo/sftpgo.json << 'EOF'
|
||||
{
|
||||
"data_provider": {
|
||||
"driver": "postgresql",
|
||||
"host": "127.0.0.1",
|
||||
"port": 5432,
|
||||
"username": "sftpgo",
|
||||
"password": "sftpgo_pass_2026",
|
||||
"name": "sftpgo",
|
||||
"create_default_admin": true
|
||||
},
|
||||
"httpd": {
|
||||
"bind_port": 8080,
|
||||
"bind_address": "127.0.0.1"
|
||||
},
|
||||
"sftpd": {
|
||||
"bindings": [
|
||||
{"port": 2022}
|
||||
]
|
||||
},
|
||||
"ftpd": {
|
||||
"bindings": [
|
||||
{"port": 0}
|
||||
]
|
||||
}
|
||||
}
|
||||
EOF
|
||||
|
||||
# 建立 launchd plist
|
||||
cat > /Library/LaunchDaemons/com.momentry.sftpgo.plist << 'EOF'
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>Label</key>
|
||||
<string>com.momentry.sftpgo</string>
|
||||
<key>UserName</key>
|
||||
<string>accusys</string>
|
||||
<key>WorkingDirectory</key>
|
||||
<string>/Users/accusys/workspace/sftpgo</string>
|
||||
<key>ProgramArguments</key>
|
||||
<array>
|
||||
<string>/opt/homebrew/bin/sftpgo</string>
|
||||
<string>serve</string>
|
||||
<string>--config-file</string>
|
||||
<string>/Users/accusys/momentry/etc/sftpgo/sftpgo.json</string>
|
||||
</array>
|
||||
<key>EnvironmentVariables</key>
|
||||
<dict>
|
||||
<key>PATH</key>
|
||||
<string>/opt/homebrew/bin:/opt/homebrew/sbin:/usr/bin:/bin</string>
|
||||
<key>HOME</key>
|
||||
<string>/Users/accusys</string>
|
||||
<key>SFTPGO_DEFAULT_ADMIN_USERNAME</key>
|
||||
<string>admin</string>
|
||||
<key>SFTPGO_DEFAULT_ADMIN_PASSWORD</key>
|
||||
<string>Test3200Test3200</string>
|
||||
</dict>
|
||||
<key>RunAtLoad</key>
|
||||
<true/>
|
||||
<key>KeepAlive</key>
|
||||
<true/>
|
||||
<key>StandardOutPath</key>
|
||||
<string>/Users/accusys/momentry/log/sftpgo.log</string>
|
||||
<key>StandardErrorPath</key>
|
||||
<string>/Users/accusys/momentry/log/sftpgo.error.log</string>
|
||||
</dict>
|
||||
</plist>
|
||||
EOF
|
||||
|
||||
# 載入服務
|
||||
sudo launchctl load /Library/LaunchDaemons/com.momentry.sftpgo.plist
|
||||
|
||||
# 驗證
|
||||
sleep 3
|
||||
curl -s -X POST http://localhost:8080/api/v2/token -u "admin:Test3200Test3200"
|
||||
```
|
||||
|
||||
### 5.6 Qdrant (向量資料庫)
|
||||
|
||||
```bash
|
||||
# 安裝 Rust (如果尚未安裝)
|
||||
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
|
||||
source "$HOME/.cargo/env"
|
||||
|
||||
# 安裝 Qdrant
|
||||
cargo install qdrant
|
||||
|
||||
# 建立資料目錄
|
||||
mkdir -p /Users/accusys/momentry/var/qdrant/storage
|
||||
|
||||
# 啟動服務
|
||||
qdrant --data-path /Users/accusys/momentry/var/qdrant/storage --api-key Test3200Test3200Test3200 &
|
||||
|
||||
# 驗證
|
||||
sleep 3
|
||||
curl -s http://localhost:6333/collections -H "api-key: Test3200Test3200Test3200"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 第六部分:Momentry Core 安裝
|
||||
|
||||
### 6.1 安裝依賴
|
||||
|
||||
```bash
|
||||
# 安裝 Python 依賴
|
||||
pip install \
|
||||
openai-whisper \
|
||||
ultralytics \
|
||||
opencv-python \
|
||||
pillow \
|
||||
python-dotenv \
|
||||
requests \
|
||||
httpx
|
||||
|
||||
# 安裝 FFmpeg
|
||||
brew install ffmpeg
|
||||
```
|
||||
|
||||
### 6.2 安裝 Momentry Core
|
||||
|
||||
```bash
|
||||
# 克隆或進入專案目錄
|
||||
cd /Users/accusys/momentry_core_0.1
|
||||
|
||||
# 編譯專案
|
||||
cargo build --release
|
||||
|
||||
# 複製執行檔
|
||||
cp target/release/momentry /usr/local/bin/
|
||||
|
||||
# 建立 launchd plist
|
||||
cat > /Library/LaunchDaemons/com.momentry.api.plist << 'EOF'
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>Label</key>
|
||||
<string>com.momentry.api</string>
|
||||
<key>UserName</key>
|
||||
<string>accusys</string>
|
||||
<key>ProgramArguments</key>
|
||||
<array>
|
||||
<string>/usr/local/bin/momentry</string>
|
||||
<string>server</string>
|
||||
<string>--host</string>
|
||||
<string>0.0.0.0</string>
|
||||
<string>--port</string>
|
||||
<string>3002</string>
|
||||
</array>
|
||||
<key>EnvironmentVariables</key>
|
||||
<dict>
|
||||
<key>DATABASE_URL</key>
|
||||
<string>postgres://accusys:accusys@127.0.0.1:5432/momentry</string>
|
||||
<key>RUST_LOG</key>
|
||||
<string>info</string>
|
||||
</dict>
|
||||
<key>RunAtLoad</key>
|
||||
<true/>
|
||||
<key>KeepAlive</key>
|
||||
<true/>
|
||||
<key>StandardOutPath</key>
|
||||
<string>/Users/accusys/momentry/log/momentry.log</string>
|
||||
<key>StandardErrorPath</key>
|
||||
<string>/Users/accusys/momentry/log/momentry.error.log</string>
|
||||
</dict>
|
||||
</plist>
|
||||
EOF
|
||||
|
||||
# 載入服務
|
||||
sudo launchctl load /Library/LaunchDaemons/com.momentry.api.plist
|
||||
|
||||
# 驗證
|
||||
sleep 3
|
||||
curl -s http://localhost:3002/health
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 第七部分:服務驗證
|
||||
|
||||
### 7.1 健康檢查腳本
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# health_check.sh
|
||||
|
||||
echo "=========================================="
|
||||
echo "Momentry System Health Check"
|
||||
echo "=========================================="
|
||||
echo ""
|
||||
|
||||
# PostgreSQL
|
||||
pg_isready -h 127.0.0.1 -p 5432 > /dev/null 2>&1
|
||||
[ $? -eq 0 ] && echo "✅ PostgreSQL" || echo "❌ PostgreSQL"
|
||||
|
||||
# MongoDB
|
||||
mongosh --quiet --eval "db.adminCommand('ping')" > /dev/null 2>&1
|
||||
[ $? -eq 0 ] && echo "✅ MongoDB" || echo "❌ MongoDB"
|
||||
|
||||
# Redis
|
||||
redis-cli -a accusys ping > /dev/null 2>&1
|
||||
[ $? -eq 0 ] && echo "✅ Redis" || echo "❌ Redis"
|
||||
|
||||
# Ollama
|
||||
curl -s http://localhost:11434/api/tags > /dev/null 2>&1
|
||||
[ $? -eq 0 ] && echo "✅ Ollama" || echo "❌ Ollama"
|
||||
|
||||
# n8n
|
||||
curl -s http://localhost:5678 > /dev/null 2>&1
|
||||
[ $? -eq 0 ] && echo "✅ n8n" || echo "❌ n8n"
|
||||
|
||||
# SFTPGo
|
||||
curl -s http://localhost:8080 > /dev/null 2>&1
|
||||
[ $? -eq 0 ] && echo "✅ SFTPGo" || echo "❌ SFTPGo"
|
||||
|
||||
# Qdrant
|
||||
curl -s http://localhost:6333/ > /dev/null 2>&1
|
||||
[ $? -eq 0 ] && echo "✅ Qdrant" || echo "❌ Qdrant"
|
||||
|
||||
# Momentry API
|
||||
curl -s http://localhost:3002/health > /dev/null 2>&1
|
||||
[ $? -eq 0 ] && echo "✅ Momentry API" || echo "❌ Momentry API"
|
||||
|
||||
# Caddy
|
||||
curl -sI http://localhost:8080 > /dev/null 2>&1
|
||||
[ $? -eq 0 ] && echo "✅ Caddy" || echo "❌ Caddy"
|
||||
|
||||
echo ""
|
||||
echo "=========================================="
|
||||
```
|
||||
|
||||
### 7.2 執行驗證
|
||||
|
||||
```bash
|
||||
# 儲存並執行
|
||||
chmod +x health_check.sh
|
||||
./health_check.sh
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 第八部分:重要憑證總覽
|
||||
|
||||
### 8.1 服務密碼
|
||||
|
||||
| 服務 | 用戶名 | 密碼 | 用途 |
|
||||
|------|--------|------|------|
|
||||
| **PostgreSQL** | accusys | `accusys` | 主要資料庫 |
|
||||
| **PostgreSQL** | sftpgo | `sftpgo_pass_2026` | SFTPGo 資料庫 |
|
||||
| **MongoDB** | accusys | `Test3200Test3200` | 文件資料庫 |
|
||||
| **Redis** | - | `accusys` | 快取服務 |
|
||||
| **n8n** | - | `Test3200Test3200Test3200` | n8n 加密金鑰 |
|
||||
| **SFTPGo** | admin | `Test3200Test3200` | SFTPGo 管理員 |
|
||||
| **Qdrant** | - | `Test3200Test3200Test3200` | Qdrant API Key |
|
||||
| **MariaDB** | root | (設定時指定) | WordPress 資料庫 |
|
||||
| **MariaDB** | wordpress | `wordpress_password` | WordPress 資料庫用戶 |
|
||||
|
||||
### 8.2 服務連接埠
|
||||
|
||||
| 服務 | 連接埠 | 協定 |
|
||||
|------|--------|------|
|
||||
| PostgreSQL | 5432 | TCP |
|
||||
| MongoDB | 27017 | TCP |
|
||||
| Redis | 6379 | TCP |
|
||||
| Ollama | 11434 | HTTP |
|
||||
| n8n | 5678 | HTTP |
|
||||
| SFTPGo | 8080 | HTTP |
|
||||
| SFTPGo | 2022 | SFTP |
|
||||
| Qdrant | 6333 | HTTP |
|
||||
| Momentry API | 3002 | HTTP |
|
||||
| Caddy | 80/443 | HTTP/HTTPS |
|
||||
| Gitea | 3000 | HTTP |
|
||||
| PHP-FPM | 9000 | TCP |
|
||||
|
||||
---
|
||||
|
||||
## 第九部分:相關文檔
|
||||
|
||||
| 文檔 | 說明 |
|
||||
|------|------|
|
||||
| `SERVICES.md` | 服務詳細說明 |
|
||||
| `INSTALL_POSTGRESQL.md` | PostgreSQL 安裝指南 |
|
||||
| `INSTALL_MONGODB.md` | MongoDB 安裝指南 |
|
||||
| `INSTALL_REDIS.md` | Redis 安裝指南 |
|
||||
| `INSTALL_QDRANT.md` | Qdrant 安裝指南 |
|
||||
| `INSTALL_N8N.md` | n8n 安裝指南 |
|
||||
| `INSTALL_SFTPGO.md` | SFTPGo 安裝指南 |
|
||||
| `SFTPGO_DEMO_USER.md` | SFTPGo Demo 用戶指南 |
|
||||
| `MOMENTRY_CORE_MONITORING.md` | 監控規範 |
|
||||
| `API_INDEX.md` | API 文件索引 |
|
||||
|
||||
---
|
||||
|
||||
## 附錄:常見問題
|
||||
|
||||
### Q1: n8n 無法連接 PostgreSQL
|
||||
|
||||
確保資料庫用戶有登入權限:
|
||||
```bash
|
||||
psql -U accusys -d postgres -c "ALTER USER accusys WITH LOGIN;"
|
||||
```
|
||||
|
||||
### Q2: SFTPGo 無法啟動
|
||||
|
||||
檢查 PostgreSQL 是否運行:
|
||||
```bash
|
||||
pg_isready -h 127.0.0.1 -p 5432
|
||||
```
|
||||
|
||||
### Q3: Qdrant API Key 無效
|
||||
|
||||
使用正確的 API Key:
|
||||
```bash
|
||||
curl -H "api-key: Test3200Test3200Test3200" http://localhost:6333/collections
|
||||
```
|
||||
|
||||
### Q4: Momentry API 無法啟動
|
||||
|
||||
檢查環境變數:
|
||||
```bash
|
||||
export DATABASE_URL="postgres://accusys:accusys@127.0.0.1:5432/momentry"
|
||||
export RUST_LOG=debug
|
||||
momentry server --host 0.0.0.0 --port 3002
|
||||
```
|
||||
@@ -1,467 +0,0 @@
|
||||
# Caddy 安裝指南 (本地部署)
|
||||
|
||||
| 項目 | 內容 |
|
||||
|------|------|
|
||||
| 建立者 | Warren |
|
||||
| 建立時間 | 2026-03-16 |
|
||||
| 文件版本 | V1.0 |
|
||||
|
||||
---
|
||||
|
||||
## 版本歷史
|
||||
|
||||
| 版本 | 日期 | 目的 | 操作人 | 工具/模型 |
|
||||
|------|------|------|--------|-----------|
|
||||
| V1.0 | 2026-03-16 | 創建文件 | Warren | OpenCode / MiniMax M2.5 |
|
||||
|
||||
---
|
||||
|
||||
## 概述
|
||||
|
||||
本文檔說明如何在 macOS 上安裝 Caddy Web Server,配置為本地部署,作為反向代理伺服器。
|
||||
|
||||
---
|
||||
|
||||
## 當前狀態
|
||||
|
||||
| 項目 | 狀態 |
|
||||
|------|------|
|
||||
| Caddy | ✅ 已安裝 v2.10.2 |
|
||||
| 設定檔 | /Users/accusys/momentry/etc/Caddyfile |
|
||||
| 日誌目錄 | /Users/accusys/momentry/log/ |
|
||||
| Plist | /Library/LaunchDaemons/com.momentry.caddy.plist |
|
||||
|
||||
---
|
||||
|
||||
## 安裝步驟
|
||||
|
||||
### Step 1: 安裝 Caddy (使用 brew)
|
||||
|
||||
```bash
|
||||
# 安裝 Caddy
|
||||
brew install caddy
|
||||
```
|
||||
|
||||
**驗證**:
|
||||
```bash
|
||||
caddy --version
|
||||
# v2.10.2
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Step 2: 建立目錄
|
||||
|
||||
```bash
|
||||
# 建立配置目錄
|
||||
mkdir -p /Users/accusys/momentry/etc/caddy
|
||||
|
||||
# 建立日誌目錄
|
||||
mkdir -p /Users/accusys/momentry/log
|
||||
|
||||
# 建立數據目錄
|
||||
mkdir -p /Users/accusys/momentry/var/caddy
|
||||
|
||||
# 建立日誌文件
|
||||
touch /Users/accusys/momentry/log/caddy.log
|
||||
touch /Users/accusys/momentry/log/caddy.error.log
|
||||
|
||||
# 設定權限
|
||||
# 注意: Caddy 使用 ports 80/443,必須以 root 身份運行
|
||||
# 因此 var/caddy 目錄需要 root:admin 權限
|
||||
chown -R accusys:staff /Users/accusys/momentry/etc/caddy
|
||||
chown -R accusys:staff /Users/accusys/momentry/log
|
||||
sudo chown -R root:admin /Users/accusys/momentry/var/caddy
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Step 3: 建立設定檔
|
||||
|
||||
建立 `/Users/accusys/momentry/etc/Caddyfile`:
|
||||
|
||||
```Caddyfile
|
||||
{
|
||||
email admin@accusys.com.tw
|
||||
metrics
|
||||
}
|
||||
|
||||
# 定義日誌 Snippet
|
||||
(common_log) {
|
||||
log {
|
||||
output file /Users/accusys/momentry/log/{args[0]}.log {
|
||||
roll_size 100mb
|
||||
roll_keep 5
|
||||
roll_keep_for 720h
|
||||
}
|
||||
format json
|
||||
}
|
||||
}
|
||||
|
||||
# Example: 反向代理到本地服務
|
||||
example.momentry.ddns.net {
|
||||
reverse_proxy localhost:8080 {
|
||||
header_up Host {upstream_hostport}
|
||||
}
|
||||
import common_log example_access
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Step 4: 使用 plist 開機自動啟動
|
||||
|
||||
**注意**: Caddy 需要使用 ports 80 和 443,必須以 root 身份運行。
|
||||
|
||||
```bash
|
||||
# 複製 plist 到 LaunchDaemons 目錄
|
||||
sudo cp /Users/accusys/momentry_core_0.1/momentry_runtime/plist/com.momentry.caddy.plist /Library/LaunchDaemons/
|
||||
|
||||
# 載入並啟動
|
||||
sudo launchctl load /Library/LaunchDaemons/com.momentry.caddy.plist
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 監控配置
|
||||
|
||||
### 添加到監控配置
|
||||
|
||||
在 `monitor/config/monitor_config.yaml` 中添加:
|
||||
|
||||
```yaml
|
||||
service:
|
||||
services:
|
||||
- name: "caddy"
|
||||
type: "http"
|
||||
port: 80
|
||||
host: "localhost"
|
||||
check_url: "http://localhost:2019/config/"
|
||||
timeout: 5
|
||||
enabled: true
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 卸載步驟
|
||||
|
||||
### 重要: 路徑說明
|
||||
|
||||
| 路徑 | 類型 | 說明 |
|
||||
|------|------|------|
|
||||
| `/Users/accusys/momentry/etc/caddy/` | 配置 | **不要刪除** - Caddy 配置 |
|
||||
| `/Users/accusys/momentry/log/` | 日誌 | **不要刪除** - 日誌目錄 |
|
||||
| `/Users/accusys/momentry/var/caddy/` | 數據 | **不要刪除** - Caddy 數據 |
|
||||
| `/opt/homebrew/opt/caddy/` | 安裝 | **刪除** - Caddy 安裝目錄 |
|
||||
| `/opt/homebrew/opt/caddy/` | 安裝 | **刪除** - Caddy 安裝目錄 |
|
||||
|
||||
### Step 1: 停止 Caddy
|
||||
|
||||
```bash
|
||||
# 找到 Caddy 進程
|
||||
ps aux | grep caddy | grep -v grep
|
||||
|
||||
# 停止 Caddy
|
||||
pkill caddy
|
||||
|
||||
# 確認停止
|
||||
ps aux | grep caddy | grep -v grep || echo "Caddy 已停止"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Step 2: 卸載 Caddy
|
||||
|
||||
```bash
|
||||
# 卸載 Caddy
|
||||
brew uninstall caddy
|
||||
|
||||
# 移除 plist
|
||||
sudo launchctl unload /Library/LaunchDaemons/com.momentry.caddy.plist
|
||||
sudo rm /Library/LaunchDaemons/com.momentry.caddy.plist
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Step 3: 刪除專屬檔案
|
||||
|
||||
```bash
|
||||
# 刪除配置目錄 (可選)
|
||||
rm -rf /Users/accusys/momentry/etc/Caddyfile
|
||||
|
||||
# 刪除日誌 (可選)
|
||||
rm -f /Users/accusys/momentry/log/caddy.log
|
||||
rm -f /Users/accusys/momentry/log/caddy.error.log
|
||||
|
||||
# 刪除數據目錄 (可選)
|
||||
rm -rf /Users/accusys/momentry/var/caddy
|
||||
```
|
||||
|
||||
**注意: 不要刪除以下共用目錄**:
|
||||
```bash
|
||||
# 這些是共用的,不要刪除!
|
||||
# /Users/accusys/momentry/etc
|
||||
# /Users/accusys/momentry/log
|
||||
# /Users/accusys/momentry/var
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Step 4: 卸載後檢查清單
|
||||
|
||||
```bash
|
||||
echo "=== Caddy 卸載後檢查 ==="
|
||||
|
||||
# 1. 檢查 Caddy 進程
|
||||
echo "1. Caddy 進程:"
|
||||
ps aux | grep caddy | grep -v grep && echo " ✗ 仍在運行" || echo " ✓ 已停止"
|
||||
|
||||
# 2. Port 80/443
|
||||
echo "2. Port 80/443:"
|
||||
(lsof -i :80 > /dev/null 2>&1 || lsof -i :443 > /dev/null 2>&1) && echo " ✗ 仍被佔用" || echo " ✓ 已釋放"
|
||||
|
||||
# 3. caddy 命令
|
||||
echo "3. caddy 命令:"
|
||||
which caddy > /dev/null 2>&1 && echo " ✗ 仍存在" || echo " ✓ 已移除"
|
||||
|
||||
# 4. brew 安裝
|
||||
echo "4. brew 安裝:"
|
||||
brew list caddy > /dev/null 2>&1 && echo " ✗ 仍存在" || echo " ✓ 已移除"
|
||||
|
||||
# 5. launchctl 服務
|
||||
echo "5. launchctl 服務:"
|
||||
sudo launchctl list | grep caddy > /dev/null 2>&1 && echo " ✗ 仍存在" || echo " ✓ 已移除"
|
||||
|
||||
# 6. 配置目錄 (可選刪除)
|
||||
echo "6. 配置目錄:"
|
||||
[ -d "/Users/accusys/momentry/etc" ] && echo " ✓ 保留" || echo " ✗ 已刪除"
|
||||
|
||||
# 7. 日誌目錄 (可選刪除)
|
||||
echo "7. 日誌目錄:"
|
||||
[ -d "/Users/accusys/momentry/log" ] && echo " ✓ 保留" || echo " ✗ 已刪除"
|
||||
```
|
||||
|
||||
**預期結果**:
|
||||
```
|
||||
=== Caddy 卸載後檢查 ===
|
||||
1. Caddy 進程:
|
||||
✓ 已停止
|
||||
2. Port 80/443:
|
||||
✓ 已釋放
|
||||
3. caddy 命令:
|
||||
✓ 已移除
|
||||
4. brew 安裝:
|
||||
✓ 已移除
|
||||
5. launchctl 服務:
|
||||
✓ 已移除
|
||||
6. 配置目錄:
|
||||
✓ 保留 (或 ✗ 已刪除)
|
||||
7. 日誌目錄:
|
||||
✓ 保留 (或 ✗ 已刪除)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 手動檢查命令
|
||||
|
||||
```bash
|
||||
# 1. 檢查進程
|
||||
ps aux | grep caddy | grep -v grep
|
||||
|
||||
# 2. 檢查 Port
|
||||
lsof -i :80
|
||||
lsof -i :443
|
||||
lsof -i :2019
|
||||
|
||||
# 3. 測試配置語法
|
||||
caddy validate --config /Users/accusys/momentry/etc/Caddyfile
|
||||
|
||||
# 4. 查看 Caddy 版本
|
||||
caddy version
|
||||
|
||||
# 5. 重新載入配置
|
||||
caddy reload --config /Users/accusys/momentry/etc/Caddyfile
|
||||
|
||||
# 6. 查看日誌
|
||||
tail -20 /Users/accusys/momentry/log/caddy.log
|
||||
|
||||
# 7. 查看 Caddy 適配的網站
|
||||
curl -I http://localhost:2019/config/
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Caddyfile 範例
|
||||
|
||||
### 基本反向代理
|
||||
|
||||
```Caddyfile
|
||||
{
|
||||
email admin@accusys.com.tw
|
||||
}
|
||||
|
||||
# 反向代理到本地服務
|
||||
example.local {
|
||||
reverse_proxy localhost:8080
|
||||
}
|
||||
```
|
||||
|
||||
### 帶 SSL 的反向代理
|
||||
|
||||
```Caddyfile
|
||||
{
|
||||
email admin@accusys.com.tw
|
||||
}
|
||||
|
||||
# 使用 Let's Encrypt 自動 SSL
|
||||
example.momentry.ddns.net {
|
||||
reverse_proxy localhost:8080 {
|
||||
header_up Host {upstream_hostport}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 多站點配置
|
||||
|
||||
```Caddyfile
|
||||
{
|
||||
email admin@accusys.com.tw
|
||||
}
|
||||
|
||||
# 站點 1
|
||||
site1.example.com {
|
||||
reverse_proxy localhost:8080
|
||||
}
|
||||
|
||||
# 站點 2
|
||||
site2.example.com {
|
||||
reverse_proxy localhost:8081
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 環境變數
|
||||
|
||||
在 `.env` 中:
|
||||
|
||||
```env
|
||||
CADDY_CONFIG=/Users/accusys/momentry/etc/Caddyfile
|
||||
CADDY_HOME=/Users/accusys/.local/share/caddy
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 故障排除
|
||||
|
||||
### Caddy 無法啟動
|
||||
|
||||
```bash
|
||||
# 檢查日誌
|
||||
tail -f /Users/accusys/momentry/log/caddy.log
|
||||
|
||||
# 檢查配置語法
|
||||
caddy validate --config /Users/accusys/momentry/etc/Caddyfile
|
||||
|
||||
# 檢查目錄權限
|
||||
ls -la /Users/accusys/momentry/etc/
|
||||
|
||||
# 重新設定權限
|
||||
chown -R $(whoami):staff /Users/accusys/momentry/etc
|
||||
```
|
||||
|
||||
### Port 被佔用
|
||||
|
||||
```bash
|
||||
# 檢查哪個程序佔用 port 80
|
||||
lsof -i :80
|
||||
|
||||
# 終止佔用程序
|
||||
kill <PID>
|
||||
```
|
||||
|
||||
### 需要重新載入配置
|
||||
|
||||
```bash
|
||||
# 重新載入配置 (熱重載)
|
||||
caddy reload --config /Users/accusys/momentry/etc/Caddyfile
|
||||
|
||||
# 或者停止後重新啟動
|
||||
sudo launchctl unload /Library/LaunchDaemons/com.momentry.caddy.plist
|
||||
sudo launchctl load /Library/LaunchDaemons/com.momentry.caddy.plist
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 檔案位置
|
||||
|
||||
| 類型 | 路徑 | 說明 |
|
||||
|------|------|------|
|
||||
| 安裝 | `/opt/homebrew/opt/caddy/` | Caddy 安裝目錄 |
|
||||
| 執行檔 | `/opt/homebrew/opt/caddy/bin/caddy` | Caddy 執行檔 |
|
||||
| 配置 | `/Users/accusys/momentry/etc/Caddyfile` | 設定檔 |
|
||||
| 日誌 | `/Users/accusys/momentry/log/caddy.log` | 執行日誌 |
|
||||
| 錯誤日誌 | `/Users/accusys/momentry/log/caddy.error.log` | 錯誤日誌 |
|
||||
| 數據 | `/Users/accusys/momentry/var/caddy/` | Caddy 數據目錄 |
|
||||
| plist | `/Library/LaunchDaemons/com.momentry.caddy.plist` | 開機啟動 |
|
||||
| 備份 | `/Users/accusys/momentry/var/caddy_backup/Caddyfile` | 配置備份 |
|
||||
|
||||
---
|
||||
|
||||
## 常用指令
|
||||
|
||||
```bash
|
||||
# 驗證配置
|
||||
caddy validate --config /Users/accusys/momentry/etc/Caddyfile
|
||||
|
||||
# 熱重載配置
|
||||
caddy reload --config /Users/accusys/momentry/etc/Caddyfile
|
||||
|
||||
# 停止 Caddy
|
||||
caddy stop
|
||||
|
||||
# 啟動 Caddy
|
||||
caddy start --config /Users/accusys/momentry/etc/Caddyfile
|
||||
|
||||
# 適配所有網站
|
||||
caddy adapt --config /Users/accusys/momentry/etc/Caddyfile --adapter caddyfile
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 備份與恢復
|
||||
|
||||
### 備份
|
||||
|
||||
```bash
|
||||
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
|
||||
BACKUP_DIR="/Users/accusys/momentry/backup/daily/caddy"
|
||||
|
||||
mkdir -p "$BACKUP_DIR"
|
||||
|
||||
# 備份配置
|
||||
tar -czf "$BACKUP_DIR/caddy_cfg_${TIMESTAMP}.tar.gz" \
|
||||
/Users/accusys/momentry/etc/Caddyfile
|
||||
|
||||
# 驗證
|
||||
sha256sum "$BACKUP_DIR/caddy_cfg_${TIMESTAMP}.tar.gz" > "$BACKUP_DIR/caddy_${TIMESTAMP}.sha256"
|
||||
```
|
||||
|
||||
### 恢復
|
||||
|
||||
```bash
|
||||
# 解壓配置
|
||||
tar -xzf /Users/accusys/momentry/backup/daily/caddy/caddy_cfg_20260316_102416.tar.gz -C /
|
||||
|
||||
# 驗證並重載
|
||||
caddy validate --config /Users/accusys/momentry/etc/Caddyfile
|
||||
caddy reload --config /Users/accusys/momentry/etc/Caddyfile
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 版本資訊
|
||||
|
||||
- 版本: 2.10.2
|
||||
- 配置: /Users/accusys/momentry/etc/Caddyfile
|
||||
- 日誌目錄: /Users/accusys/momentry/log/
|
||||
@@ -1,410 +0,0 @@
|
||||
# Gitea 安裝指南 (本地部署)
|
||||
|
||||
| 項目 | 內容 |
|
||||
|------|------|
|
||||
| 建立者 | Warren |
|
||||
| 建立時間 | 2026-03-15 |
|
||||
| 文件版本 | V1.0 |
|
||||
|
||||
---
|
||||
|
||||
## 版本歷史
|
||||
|
||||
| 版本 | 日期 | 目的 | 操作人 | 工具/模型 |
|
||||
|------|------|------|--------|-----------|
|
||||
| V1.0 | 2026-03-15 | 創建文件 | Warren | OpenCode / MiniMax M2.5 |
|
||||
|
||||
---
|
||||
|
||||
## 概述
|
||||
|
||||
本文檔說明如何在 macOS 上安裝 Gitea Git 服務,配置為本地部署。
|
||||
|
||||
---
|
||||
|
||||
## 當前狀態
|
||||
|
||||
| 項目 | 狀態 |
|
||||
|------|------|
|
||||
| Gitea | ✅ 已安裝 v1.25.3 |
|
||||
| 數據目錄 | /Users/accusys/momentry/var/gitea/ |
|
||||
| 配置目錄 | /Users/accusys/momentry/etc/gitea/ |
|
||||
| 日誌目錄 | /Users/accusys/momentry/log/ |
|
||||
| Plist | /Library/LaunchDaemons/com.momentry.gitea.plist |
|
||||
|
||||
---
|
||||
|
||||
## 安裝步驟
|
||||
|
||||
### Step 1: 安裝 Gitea (使用 brew)
|
||||
|
||||
```bash
|
||||
# 安裝 Gitea
|
||||
brew install gitea
|
||||
```
|
||||
|
||||
**驗證**:
|
||||
```bash
|
||||
gitea --version
|
||||
# gitea version 1.25.3
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Step 2: 建立目錄
|
||||
|
||||
```bash
|
||||
# 建立數據目錄
|
||||
mkdir -p /Users/accusys/momentry/var/gitea/data
|
||||
mkdir -p /Users/accusys/momentry/var/gitea/log
|
||||
|
||||
# 建立配置目錄
|
||||
mkdir -p /Users/accusys/momentry/etc/gitea
|
||||
|
||||
# 建立日誌目錄
|
||||
mkdir -p /Users/accusys/momentry/log
|
||||
|
||||
# 建立日誌文件
|
||||
touch /Users/accusys/momentry/log/gitea.log
|
||||
touch /Users/accusys/momentry/log/gitea.error.log
|
||||
|
||||
# 設定權限
|
||||
chown -R accusys:staff /Users/accusys/momentry/var/gitea
|
||||
chown -R accusys:staff /Users/accusys/momentry/etc/gitea
|
||||
chown -R accusys:staff /Users/accusys/momentry/log
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Step 3: 建立設定檔
|
||||
|
||||
建立 `/Users/accusys/momentry/etc/gitea/app.ini`:
|
||||
|
||||
```ini
|
||||
APP_NAME = Gitea: Git with a cup of tea
|
||||
RUN_USER = accusys
|
||||
WORK_PATH = /Users/accusys/momentry/var/gitea
|
||||
RUN_MODE = prod
|
||||
|
||||
[database]
|
||||
DB_TYPE = postgres
|
||||
HOST = 127.0.0.1:5432
|
||||
NAME = gitea
|
||||
USER = gitea
|
||||
PASSWD = gitea_pass
|
||||
SSL_MODE = disable
|
||||
|
||||
[repository]
|
||||
ROOT = /Users/accusys/momentry/var/gitea/data/gitea-repositories
|
||||
|
||||
[server]
|
||||
SSH_DOMAIN = gitea.momentry.ddns.net
|
||||
DOMAIN = gitea.momentry.ddns.net
|
||||
HTTP_PORT = 3000
|
||||
ROOT_URL = http://gitea.momentry.ddns.net:3000/
|
||||
APP_DATA_PATH = /Users/accusys/momentry/var/gitea/data
|
||||
DISABLE_SSH = false
|
||||
SSH_PORT = 2222
|
||||
LFS_START_SERVER = true
|
||||
OFFLINE_MODE = true
|
||||
|
||||
[lfs]
|
||||
PATH = /Users/accusys/momentry/var/gitea/data/lfs
|
||||
|
||||
[log]
|
||||
MODE = console, file
|
||||
ROOT_PATH = /Users/accusys/momentry/log
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Step 4: 使用 plist 開機自動啟動
|
||||
|
||||
```bash
|
||||
# 複製 plist 到 LaunchDaemons 目錄
|
||||
sudo cp /Users/accusys/momentry_core_0.1/momentry_runtime/plist/com.momentry.gitea.plist /Library/LaunchDaemons/
|
||||
|
||||
# 載入並啟動
|
||||
sudo launchctl load /Library/LaunchDaemons/com.momentry.gitea.plist
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 監控配置
|
||||
|
||||
### 添加到監控配置
|
||||
|
||||
在 `monitor/config/monitor_config.yaml` 中添加:
|
||||
|
||||
```yaml
|
||||
service:
|
||||
services:
|
||||
- name: "gitea"
|
||||
type: "http"
|
||||
port: 3000
|
||||
host: "localhost"
|
||||
check_url: "http://localhost:3000/"
|
||||
timeout: 5
|
||||
enabled: true
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 卸載步驟
|
||||
|
||||
### 重要: 路徑說明
|
||||
|
||||
| 路徑 | 類型 | 說明 |
|
||||
|------|------|------|
|
||||
| `/Users/accusys/momentry/var/gitea/` | 數據 | **不要刪除** - Gitea 數據 |
|
||||
| `/Users/accusys/momentry/etc/gitea/` | 配置 | **不要刪除** - Gitea 配置 |
|
||||
| `/Users/accusys/momentry/log/` | 日誌 | **不要刪除** - 日誌目錄 |
|
||||
| `/opt/homebrew/opt/gitea/` | 安裝 | **刪除** - Gitea 安裝目錄 |
|
||||
|
||||
### Step 1: 停止 Gitea
|
||||
|
||||
```bash
|
||||
# 找到 Gitea 進程
|
||||
ps aux | grep gitea | grep -v grep
|
||||
|
||||
# 停止 Gitea
|
||||
pkill gitea
|
||||
|
||||
# 確認停止
|
||||
ps aux | grep gitea | grep -v grep || echo "Gitea 已停止"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Step 2: 卸載 Gitea
|
||||
|
||||
```bash
|
||||
# 卸載 Gitea
|
||||
brew uninstall gitea
|
||||
|
||||
# 移除 plist
|
||||
sudo launchctl unload /Library/LaunchDaemons/com.momentry.gitea.plist
|
||||
sudo rm /Library/LaunchDaemons/com.momentry.gitea.plist
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Step 3: 刪除專屬檔案
|
||||
|
||||
```bash
|
||||
# 刪除數據目錄 (可選)
|
||||
rm -rf /Users/accusys/momentry/var/gitea
|
||||
|
||||
# 刪除配置目錄 (可選)
|
||||
rm -rf /Users/accusys/momentry/etc/gitea
|
||||
|
||||
# 刪除日誌 (可選)
|
||||
rm -f /Users/accusys/momentry/log/gitea.log
|
||||
rm -f /Users/accusys/momentry/log/gitea.error.log
|
||||
```
|
||||
|
||||
**注意: 不要刪除以下共用目錄**:
|
||||
```bash
|
||||
# 這些是共用的,不要刪除!
|
||||
# /Users/accusys/momentry/var
|
||||
# /Users/accusys/momentry/etc
|
||||
# /Users/accusys/momentry/log
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Step 4: 卸載後檢查清單
|
||||
|
||||
```bash
|
||||
echo "=== Gitea 卸載後檢查 ==="
|
||||
|
||||
# 1. 檢查 Gitea 進程
|
||||
echo "1. Gitea 進程:"
|
||||
ps aux | grep gitea | grep -v grep && echo " ✗ 仍在運行" || echo " ✓ 已停止"
|
||||
|
||||
# 2. Port 3000
|
||||
echo "2. Port 3000:"
|
||||
lsof -i :3000 > /dev/null 2>&1 && echo " ✗ 仍被佔用" || echo " ✓ 已釋放"
|
||||
|
||||
# 3. gitea 命令
|
||||
echo "3. gitea 命令:"
|
||||
which gitea > /dev/null 2>&1 && echo " ✗ 仍存在" || echo " ✓ 已移除"
|
||||
|
||||
# 4. brew 安裝
|
||||
echo "4. brew 安裝:"
|
||||
brew list gitea > /dev/null 2>&1 && echo " ✗ 仍存在" || echo " ✓ 已移除"
|
||||
|
||||
# 5. launchctl 服務
|
||||
echo "5. launchctl 服務:"
|
||||
sudo launchctl list | grep gitea > /dev/null 2>&1 && echo " ✗ 仍存在" || echo " ✓ 已移除"
|
||||
|
||||
# 6. 數據目錄 (可選刪除)
|
||||
echo "6. 數據目錄:"
|
||||
[ -d "/Users/accusys/momentry/var/gitea" ] && echo " ✓ 保留" || echo " ✗ 已刪除"
|
||||
|
||||
# 7. 配置目錄 (可選刪除)
|
||||
echo "7. 配置目錄:"
|
||||
[ -d "/Users/accusys/momentry/etc/gitea" ] && echo " ✓ 保留" || echo " ✗ 已刪除"
|
||||
```
|
||||
|
||||
**預期結果**:
|
||||
```
|
||||
=== Gitea 卸載後檢查 ===
|
||||
1. Gitea 進程:
|
||||
✓ 已停止
|
||||
2. Port 3000:
|
||||
✓ 已釋放
|
||||
3. gitea 命令:
|
||||
✓ 已移除
|
||||
4. brew 安裝:
|
||||
✓ 已移除
|
||||
5. launchctl 服務:
|
||||
✓ 已移除
|
||||
6. 數據目錄:
|
||||
✓ 保留 (或 ✗ 已刪除)
|
||||
7. 配置目錄:
|
||||
✓ 保留 (或 ✗ 已刪除)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 手動檢查命令
|
||||
|
||||
```bash
|
||||
# 1. 檢查進程
|
||||
ps aux | grep gitea | grep -v grep
|
||||
|
||||
# 2. 檢查 Port
|
||||
lsof -i :3000
|
||||
|
||||
# 3. 測試連線
|
||||
curl http://localhost:3000/
|
||||
|
||||
# 4. 查看版本
|
||||
gitea --version
|
||||
|
||||
# 5. 驗證配置
|
||||
gitea doctor --config /Users/accusys/momentry/etc/gitea/app.ini
|
||||
|
||||
# 6. 查看日誌
|
||||
tail -20 /Users/accusys/momentry/log/gitea.log
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 連線資訊
|
||||
|
||||
| 項目 | 值 |
|
||||
|------|-----|
|
||||
| URL | http://localhost:3000 |
|
||||
| Domain | gitea.momentry.ddns.net |
|
||||
| SSH Port | 2222 |
|
||||
| Database | PostgreSQL (gitea) |
|
||||
|
||||
---
|
||||
|
||||
## 環境變數
|
||||
|
||||
在 `.env` 中:
|
||||
|
||||
```env
|
||||
GITEA_URL=http://localhost:3000
|
||||
GITEA_ROOT=/Users/accusys/momentry/var/gitea/data/gitea-repositories
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 故障排除
|
||||
|
||||
### Gitea 無法啟動
|
||||
|
||||
```bash
|
||||
# 檢查日誌
|
||||
tail -f /Users/accusys/momentry/log/gitea.log
|
||||
|
||||
# 檢查配置語法
|
||||
gitea doctor --config /Users/accusys/momentry/etc/gitea/app.ini
|
||||
|
||||
# 檢查目錄權限
|
||||
ls -la /Users/accusys/momentry/var/gitea/
|
||||
|
||||
# 重新設定權限
|
||||
chown -R $(whoami):staff /Users/accusys/momentry/var/gitea
|
||||
```
|
||||
|
||||
### Port 被佔用
|
||||
|
||||
```bash
|
||||
# 檢查哪個程序佔用 port 3000
|
||||
lsof -i :3000
|
||||
|
||||
# 終止佔用程序
|
||||
kill <PID>
|
||||
```
|
||||
|
||||
### 需要重新載入 plist
|
||||
|
||||
```bash
|
||||
# 卸載舊服務 (如果存在)
|
||||
sudo launchctl unload /Library/LaunchDaemons/com.momentry.gitea.plist 2>/dev/null
|
||||
|
||||
# 載入新服務
|
||||
sudo launchctl load /Library/LaunchDaemons/com.momentry.gitea.plist
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 檔案位置
|
||||
|
||||
| 類型 | 路徑 | 說明 |
|
||||
|------|------|------|
|
||||
| 安裝 | `/opt/homebrew/opt/gitea/` | Gitea 安裝目錄 |
|
||||
| 執行檔 | `/opt/homebrew/opt/gitea/bin/gitea` | Gitea 執行檔 |
|
||||
| 數據目錄 | `/Users/accusys/momentry/var/gitea/data/` | 數據儲存 |
|
||||
| 配置 | `/Users/accusys/momentry/etc/gitea/app.ini` | 設定檔 |
|
||||
| 日誌 | `/Users/accusys/momentry/log/gitea.log` | 執行日誌 |
|
||||
| 錯誤日誌 | `/Users/accusys/momentry/log/gitea.error.log` | 錯誤日誌 |
|
||||
| plist | `/Library/LaunchDaemons/com.momentry.gitea.plist` | 開機啟動 |
|
||||
| 備份 | `/Users/accusys/momentry/var/gitea_backup/app.ini` | 配置備份 |
|
||||
|
||||
---
|
||||
|
||||
## 資料庫資訊
|
||||
|
||||
Gitea 使用 PostgreSQL 作為資料庫:
|
||||
|
||||
| 項目 | 值 |
|
||||
|------|-----|
|
||||
| Database | gitea |
|
||||
| User | gitea |
|
||||
| Host | 127.0.0.1:5432 |
|
||||
| Password | gitea_pass |
|
||||
|
||||
---
|
||||
|
||||
## 常用指令
|
||||
|
||||
```bash
|
||||
# 啟動 Gitea
|
||||
gitea web --config /Users/accusys/momentry/etc/gitea/app.ini
|
||||
|
||||
# 驗證配置
|
||||
gitea doctor --config /Users/accusys/momentry/etc/gitea/app.ini
|
||||
|
||||
# 查看版本
|
||||
gitea --version
|
||||
|
||||
# 備份數據
|
||||
gitea dump --config /Users/accusys/momentry/etc/gitea/app.ini --zipfile /Users/accusys/momentry/var/gitea_backup.zip
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 版本資訊
|
||||
|
||||
- 版本: 1.25.3
|
||||
- HTTP Port: 3000
|
||||
- SSH Port: 2222
|
||||
- 數據目錄: /Users/accusys/momentry/var/gitea/
|
||||
- 配置: /Users/accusys/momentry/etc/gitea/app.ini
|
||||
- 日誌目錄: /Users/accusys/momentry/log/
|
||||
@@ -1,393 +0,0 @@
|
||||
# Gitea MCP Server 安裝指南 (本地部署)
|
||||
|
||||
| 項目 | 內容 |
|
||||
|------|------|
|
||||
| 建立者 | OpenCode |
|
||||
| 建立時間 | 2026-03-24 |
|
||||
| 文件版本 | V1.0 |
|
||||
|
||||
---
|
||||
|
||||
## 版本歷史
|
||||
|
||||
| 版本 | 日期 | 目的 | 操作人 | 工具/模型 |
|
||||
|------|------|------|--------|-----------|
|
||||
| V1.0 | 2026-03-24 | 創建文件 | OpenCode | OpenCode / big-pickle |
|
||||
|
||||
---
|
||||
|
||||
## 概述
|
||||
|
||||
本文檔說明如何在 macOS 上安裝 Gitea MCP Server,配置為透過 OpenCode MCP 整合存取 Gitea API。
|
||||
|
||||
Gitea MCP Server 是一個 MCP (Model Context Protocol) 伺服器,提供對 Gitea API 的完整存取能力,包括 repos、issues、pull requests、workflows 等資源管理。
|
||||
|
||||
---
|
||||
|
||||
## 當前狀態
|
||||
|
||||
| 項目 | 狀態 |
|
||||
|------|------|
|
||||
| Gitea MCP Server | ✅ 已安裝 |
|
||||
| 安裝方式 | Homebrew (`gitea-mcp-server`) |
|
||||
| Plist | /Library/LaunchDaemons/com.momentry.gitea-mcp-server.plist |
|
||||
| 執行身份 | accusys |
|
||||
| 監聽端口 | 8787 |
|
||||
| Gitea 主機 | http://localhost:3000 |
|
||||
| Launchd 狀態 | ✅ 已註冊 |
|
||||
| RunAtLoad | ✅ 已設定 |
|
||||
| KeepAlive | ✅ 已設定 |
|
||||
|
||||
---
|
||||
|
||||
## 前置條件
|
||||
|
||||
- Gitea 服務已運行(端口 3000)
|
||||
- Homebrew 已安裝
|
||||
- 管理員權限
|
||||
|
||||
---
|
||||
|
||||
## 安裝步驟
|
||||
|
||||
### Step 1: 安裝 Gitea MCP Server
|
||||
|
||||
```bash
|
||||
brew install gitea-mcp-server
|
||||
```
|
||||
|
||||
**驗證**:
|
||||
|
||||
```bash
|
||||
which gitea-mcp-server
|
||||
# /opt/homebrew/bin/gitea-mcp-server
|
||||
|
||||
/opt/homebrew/bin/gitea-mcp-server --help
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Step 2: 創建日誌目錄
|
||||
|
||||
```bash
|
||||
mkdir -p /Users/accusys/momentry/log
|
||||
|
||||
touch /Users/accusys/momentry/log/gitea-mcp-server.log
|
||||
touch /Users/accusys/momentry/log/gitea-mcp-server.error.log
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Step 3: 獲取 Gitea API Token
|
||||
|
||||
#### 步驟 3.1: 登入 Gitea
|
||||
|
||||
1. 開啟瀏覽器,訪問 http://localhost:3000
|
||||
2. 使用管理員帳號登入
|
||||
|
||||
#### 步驟 3.2: 生成 Personal Access Token
|
||||
|
||||
1. 點擊右上角頭像 → **Settings**
|
||||
2. 左側選單選擇 **Applications**
|
||||
3. 在 **Access Tokens** 區塊:
|
||||
- **Token Name**: `opencode-mcp`
|
||||
- **Expiration**: 選擇過期時間(如 365 days)
|
||||
- **Scopes**: 勾選需要的權限
|
||||
- `repo` - 倉庫操作
|
||||
- `read:user` - 讀取用戶資訊
|
||||
- `read:org` - 讀取組織資訊
|
||||
4. 點擊 **Generate Token**
|
||||
5. **立即複製** 生成的 Token
|
||||
|
||||
#### 步驟 3.3: 驗證 Token
|
||||
|
||||
```bash
|
||||
curl -H "Authorization: token <YOUR_TOKEN>" http://localhost:3000/api/v1/user
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Step 4: 創建 Plist 檔案
|
||||
|
||||
```bash
|
||||
sudo tee /Library/LaunchDaemons/com.momentry.gitea-mcp-server.plist << 'EOF'
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>Label</key>
|
||||
<string>com.momentry.gitea-mcp-server</string>
|
||||
<key>UserName</key>
|
||||
<string>accusys</string>
|
||||
<key>ProgramArguments</key>
|
||||
<array>
|
||||
<string>/opt/homebrew/bin/gitea-mcp-server</string>
|
||||
<string>-transport</string>
|
||||
<string>http</string>
|
||||
<string>-port</string>
|
||||
<string>8787</string>
|
||||
<string>-host</string>
|
||||
<string>http://localhost:3000</string>
|
||||
<string>-token</string>
|
||||
<string><GITEA_TOKEN></string>
|
||||
</array>
|
||||
<key>RunAtLoad</key>
|
||||
<true/>
|
||||
<key>KeepAlive</key>
|
||||
<true/>
|
||||
<key>StandardOutPath</key>
|
||||
<string>/Users/accusys/momentry/log/gitea-mcp-server.log</string>
|
||||
<key>StandardErrorPath</key>
|
||||
<string>/Users/accusys/momentry/log/gitea-mcp-server.error.log</string>
|
||||
</dict>
|
||||
</plist>
|
||||
EOF
|
||||
```
|
||||
|
||||
**注意**: 將 `<GITEA_TOKEN>` 替換為實際的 Token 值。
|
||||
|
||||
---
|
||||
|
||||
### Step 5: 設定權限
|
||||
|
||||
```bash
|
||||
sudo chown root:wheel /Library/LaunchDaemons/com.momentry.gitea-mcp-server.plist
|
||||
sudo chmod 644 /Library/LaunchDaemons/com.momentry.gitea-mcp-server.plist
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Step 6: 載入服務
|
||||
|
||||
```bash
|
||||
sudo launchctl bootstrap system /Library/LaunchDaemons/com.momentry.gitea-mcp-server.plist
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Step 7: 驗證服務
|
||||
|
||||
```bash
|
||||
# 檢查服務狀態
|
||||
sudo launchctl list | grep gitea-mcp-server
|
||||
|
||||
# 檢查進程
|
||||
ps aux | grep gitea-mcp-server | grep -v grep
|
||||
|
||||
# 測試端點
|
||||
curl -s http://localhost:8787/
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Step 8: 整合 OpenCode MCP
|
||||
|
||||
#### 步驟 8.1: 更新 OpenCode 配置
|
||||
|
||||
編輯 `~/.config/opencode/opencode.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"$schema": "https://opencode.ai/config.json",
|
||||
"mcp": {
|
||||
"gitea": {
|
||||
"type": "local",
|
||||
"enabled": true,
|
||||
"command": [
|
||||
"/opt/homebrew/bin/gitea-mcp-server",
|
||||
"-token", "<GITEA_TOKEN>",
|
||||
"-host", "http://localhost:3000"
|
||||
]
|
||||
},
|
||||
"n8n": {
|
||||
"type": "local",
|
||||
"enabled": true,
|
||||
"command": ["/opt/homebrew/bin/mcp-n8n"],
|
||||
"environment": {
|
||||
"N8N_BASE_URL": "http://localhost:5678",
|
||||
"N8N_API_KEY": "<N8N_API_KEY>"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 步驟 8.2: 重啟 OpenCode
|
||||
|
||||
```bash
|
||||
# 停止現有 OpenCode
|
||||
pkill -f opencode
|
||||
|
||||
# 重新啟動
|
||||
opencode
|
||||
```
|
||||
|
||||
#### 步驟 8.3: 驗證 MCP 連接
|
||||
|
||||
在 OpenCode 中執行:
|
||||
|
||||
```
|
||||
/mcps
|
||||
```
|
||||
|
||||
確認 gitea 顯示為 `connected`。
|
||||
|
||||
---
|
||||
|
||||
## Plist 參數說明
|
||||
|
||||
| 參數 | 說明 | 值 |
|
||||
|------|------|-----|
|
||||
| `-transport` | 傳輸類型 | `http` |
|
||||
| `-port` | HTTP 監聽端口 | `8787` |
|
||||
| `-host` | Gitea 主機 URL | `http://localhost:3000` |
|
||||
| `-token` | Gitea API Token | 見 Step 3 |
|
||||
|
||||
---
|
||||
|
||||
## 管理指令
|
||||
|
||||
### 啟動服務
|
||||
|
||||
```bash
|
||||
sudo launchctl bootstrap system /Library/LaunchDaemons/com.momentry.gitea-mcp-server.plist
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 停止服務
|
||||
|
||||
```bash
|
||||
sudo launchctl bootout system/com.momentry.gitea-mcp-server
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 重啟服務
|
||||
|
||||
```bash
|
||||
sudo launchctl bootout system/com.momentry.gitea-mcp-server
|
||||
sudo launchctl bootstrap system /Library/LaunchDaemons/com.momentry.gitea-mcp-server.plist
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 查看日誌
|
||||
|
||||
```bash
|
||||
# 即時查看日誌
|
||||
tail -f /Users/accusys/momentry/log/gitea-mcp-server.log
|
||||
|
||||
# 錯誤日誌
|
||||
tail -f /Users/accusys/momentry/log/gitea-mcp-server.error.log
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 卸載步驟
|
||||
|
||||
### Step 1: 停止服務
|
||||
|
||||
```bash
|
||||
sudo launchctl bootout system/com.momentry.gitea-mcp-server
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Step 2: 移除 Plist
|
||||
|
||||
```bash
|
||||
sudo rm /Library/LaunchDaemons/com.momentry.gitea-mcp-server.plist
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Step 3: 從 OpenCode 配置移除
|
||||
|
||||
編輯 `~/.config/opencode/opencode.json`,移除 `mcp.gitea` 區塊。
|
||||
|
||||
---
|
||||
|
||||
## 故障排除
|
||||
|
||||
### 服務無法啟動
|
||||
|
||||
1. 檢查 Token 是否正確
|
||||
2. 檢查 Gitea 是否運行:`curl -s http://localhost:3000/`
|
||||
3. 檢查日誌:`/Users/accusys/momentry/log/gitea-mcp-server.error.log`
|
||||
|
||||
---
|
||||
|
||||
### Token 無效
|
||||
|
||||
1. 確認 Token 未過期
|
||||
2. 確認 Token 有足夠的權限
|
||||
3. 重新生成 Token
|
||||
|
||||
---
|
||||
|
||||
### 端口被佔用
|
||||
|
||||
```bash
|
||||
# 檢查端口占用
|
||||
lsof -i :8787
|
||||
|
||||
# 修改 plist 中的端口後重新載入
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### OpenCode MCP 未顯示
|
||||
|
||||
1. 確認 OpenCode 已重啟
|
||||
2. 檢查 `~/.config/opencode/opencode.json` 格式正確
|
||||
3. 確認 gitea-mcp-server 程序正在運行
|
||||
|
||||
---
|
||||
|
||||
## 檔案位置
|
||||
|
||||
| 類型 | 路徑 | 說明 |
|
||||
|------|------|------|
|
||||
| Plist | /Library/LaunchDaemons/com.momentry.gitea-mcp-server.plist | Launchd 服務配置 |
|
||||
| 日誌 | /Users/accusys/momentry/log/gitea-mcp-server.log | 標準輸出日誌 |
|
||||
| 錯誤日誌 | /Users/accusys/momentry/log/gitea-mcp-server.error.log | 錯誤日誌 |
|
||||
| OpenCode 配置 | ~/.config/opencode/opencode.json | MCP 設定檔 |
|
||||
| Gitea | http://localhost:3000 | Gitea Web UI |
|
||||
|
||||
---
|
||||
|
||||
## 常用指令
|
||||
|
||||
```bash
|
||||
# 驗證服務狀態
|
||||
sudo launchctl list | grep gitea-mcp-server
|
||||
|
||||
# 查看服務 PID
|
||||
ps aux | grep gitea-mcp-server | grep -v grep
|
||||
|
||||
# 測試端點
|
||||
curl -s http://localhost:8787/
|
||||
|
||||
# 驗證 Gitea 連接
|
||||
curl -H "Authorization: token <TOKEN>" http://localhost:3000/api/v1/user
|
||||
|
||||
# 查看即時日誌
|
||||
tail -f /Users/accusys/momentry/log/gitea-mcp-server.log
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 版本資訊
|
||||
|
||||
- 版本: 1.0
|
||||
- 安裝日期: 2026-03-24
|
||||
- 文件更新: 2026-03-24
|
||||
|
||||
---
|
||||
|
||||
## 相關文件
|
||||
|
||||
- [OpenCode MCP 整合](./N8N_MCP_SETUP.md) - n8n MCP 設定說明
|
||||
- [服務總覽](./SERVICES.md) - 所有服務狀態總覽
|
||||
- [待解決問題](./PENDING_ISSUES.md) - MCP 安裝狀態追蹤
|
||||
@@ -1,396 +0,0 @@
|
||||
# MariaDB 安裝指南 (本地部署)
|
||||
|
||||
| 項目 | 內容 |
|
||||
|------|------|
|
||||
| 建立者 | Warren |
|
||||
| 建立時間 | 2026-03-16 |
|
||||
| 文件版本 | V1.0 |
|
||||
|
||||
---
|
||||
|
||||
## 版本歷史
|
||||
|
||||
| 版本 | 日期 | 目的 | 操作人 | 工具/模型 |
|
||||
|------|------|------|--------|-----------|
|
||||
| V1.0 | 2026-03-16 | 創建文件 | Warren | OpenCode / MiniMax M2.5 |
|
||||
|
||||
---
|
||||
|
||||
## 概述
|
||||
|
||||
本文檔說明如何在 macOS 上安裝 MariaDB,配置為本地部署,支援遠端訪問。
|
||||
|
||||
---
|
||||
|
||||
## 當前狀態
|
||||
|
||||
| 項目 | 狀態 |
|
||||
|------|------|
|
||||
| MariaDB | ✅ 已安裝 v12.1.2 |
|
||||
| 數據目錄 | /Users/accusys/momentry/var/mariadb/ |
|
||||
| 日誌目錄 | /Users/accusys/momentry/log/ |
|
||||
| Plist | /Library/LaunchDaemons/com.momentry.mariadb.plist |
|
||||
|
||||
---
|
||||
|
||||
## 安裝步驟
|
||||
|
||||
### Step 1: 安裝 MariaDB (使用 brew)
|
||||
|
||||
```bash
|
||||
# 安裝 MariaDB
|
||||
brew install mariadb
|
||||
```
|
||||
|
||||
**驗證**:
|
||||
```bash
|
||||
mariadb --version
|
||||
# mariadb from 12.1.2-MariaDB
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Step 2: 建立目錄結構
|
||||
|
||||
```bash
|
||||
# 建立數據目錄
|
||||
mkdir -p /Users/accusys/momentry/var/mariadb
|
||||
|
||||
# 建立配置目錄
|
||||
mkdir -p /Users/accusys/momentry/etc/mariadb
|
||||
|
||||
# 建立日誌目錄
|
||||
mkdir -p /Users/accusys/momentry/log
|
||||
|
||||
# 建立日誌文件
|
||||
touch /Users/accusys/momentry/log/mariadb.log
|
||||
touch /Users/accusys/momentry/log/mariadb.error.log
|
||||
|
||||
# 設定權限
|
||||
chown -R accusys:staff /Users/accusys/momentry/var/mariadb
|
||||
chown -R accusys:staff /Users/accusys/momentry/etc/mariadb
|
||||
chown -R accusys:staff /Users/accusys/momentry/log
|
||||
```
|
||||
|
||||
**注意**: 如果需要從舊數據遷移,需要先初始化新目錄:
|
||||
```bash
|
||||
# 初始化新數據目錄
|
||||
mysql_install_db --datadir=/Users/accusys/momentry/var/mariadb
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Step 3: 使用 plist 開機自動啟動
|
||||
|
||||
```bash
|
||||
# 複製 plist 到 LaunchDaemons 目錄
|
||||
sudo cp /Users/accusys/momentry_core_0.1/momentry_runtime/plist/com.momentry.mariadb.plist /Library/LaunchDaemons/
|
||||
|
||||
# 載入並啟動
|
||||
sudo launchctl load /Library/LaunchDaemons/com.momentry.mariadb.plist
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 監控配置
|
||||
|
||||
### 添加到監控配置
|
||||
|
||||
在 `monitor/config/monitor_config.yaml` 中添加:
|
||||
|
||||
```yaml
|
||||
database:
|
||||
mariadb:
|
||||
enabled: true
|
||||
host: "localhost"
|
||||
port: 3306
|
||||
user: "root"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 卸載步驟
|
||||
|
||||
### 重要: 路徑說明
|
||||
|
||||
| 路徑 | 類型 | 說明 |
|
||||
|------|------|------|
|
||||
| `/Users/accusys/momentry/var/mariadb/` | 數據 | **不要刪除** - 數據目錄 |
|
||||
| `/Users/accusys/momentry/etc/mariadb/` | 配置 | **不要刪除** - 配置目錄 |
|
||||
| `/Users/accusys/momentry/log/` | 日誌 | **不要刪除** - 日誌目錄 |
|
||||
| `/opt/homebrew/opt/mariadb/` | 安裝 | **刪除** - MariaDB 安裝目錄 |
|
||||
|
||||
### Step 1: 停止 MariaDB
|
||||
|
||||
```bash
|
||||
# 找到 MariaDB 進程
|
||||
ps aux | grep mariadb | grep -v grep
|
||||
|
||||
# 停止 MariaDB
|
||||
mysqladmin -u root -p shutdown
|
||||
# 或
|
||||
pkill mariadbd
|
||||
|
||||
# 確認停止
|
||||
ps aux | grep mariadb | grep -v grep || echo "MariaDB 已停止"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Step 2: 卸載 MariaDB
|
||||
|
||||
```bash
|
||||
# 卸載 MariaDB
|
||||
brew uninstall mariadb
|
||||
|
||||
# 移除 plist
|
||||
sudo launchctl unload /Library/LaunchDaemons/com.momentry.mariadb.plist
|
||||
sudo rm /Library/LaunchDaemons/com.momentry.mariadb.plist
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Step 3: 刪除專屬檔案
|
||||
|
||||
```bash
|
||||
# 刪除數據目錄 (可選)
|
||||
rm -rf /Users/accusys/momentry/var/mariadb
|
||||
|
||||
# 刪除日誌 (可選)
|
||||
rm -f /Users/accusys/momentry/log/mariadb.log
|
||||
rm -f /Users/accusys/momentry/log/mariadb.error.log
|
||||
```
|
||||
|
||||
**注意: 不要刪除以下共用目錄**:
|
||||
```bash
|
||||
# 這些是共用的,不要刪除!
|
||||
# /Users/accusys/momentry/var
|
||||
# /Users/accusys/momentry/log
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Step 4: 卸載後檢查清單
|
||||
|
||||
```bash
|
||||
echo "=== MariaDB 卸載後檢查 ==="
|
||||
|
||||
# 1. 檢查 MariaDB 進程
|
||||
echo "1. MariaDB 進程:"
|
||||
ps aux | grep mariadb | grep -v grep && echo " ✗ 仍在運行" || echo " ✓ 已停止"
|
||||
|
||||
# 2. Port 3306
|
||||
echo "2. Port 3306:"
|
||||
lsof -i :3306 > /dev/null 2>&1 && echo " ✗ 仍被佔用" || echo " ✓ 已釋放"
|
||||
|
||||
# 3. mariadb 命令
|
||||
echo "3. mariadb 命令:"
|
||||
which mariadb > /dev/null 2>&1 && echo " ✗ 仍存在" || echo " ✓ 已移除"
|
||||
|
||||
# 4. brew 安裝
|
||||
echo "4. brew 安裝:"
|
||||
brew list mariadb > /dev/null 2>&1 && echo " ✗ 仍存在" || echo " ✓ 已移除"
|
||||
|
||||
# 5. launchctl 服務
|
||||
echo "5. launchctl 服務:"
|
||||
sudo launchctl list | grep mariadb > /dev/null 2>&1 && echo " ✗ 仍存在" || echo " ✓ 已移除"
|
||||
|
||||
# 6. 數據目錄 (可選刪除)
|
||||
echo "6. 數據目錄:"
|
||||
[ -d "/Users/accusys/momentry/var/mariadb" ] && echo " ✓ 保留" || echo " ✗ 已刪除"
|
||||
|
||||
# 7. 日誌目錄 (可選刪除)
|
||||
echo "7. 日誌目錄:"
|
||||
[ -d "/Users/accusys/momentry/log" ] && echo " ✓ 保留" || echo " ✗ 已刪除"
|
||||
```
|
||||
|
||||
**預期結果**:
|
||||
```
|
||||
=== MariaDB 卸載後檢查 ===
|
||||
1. MariaDB 進程:
|
||||
✓ 已停止
|
||||
2. Port 3306:
|
||||
✓ 已釋放
|
||||
3. mariadb 命令:
|
||||
✓ 已移除
|
||||
4. brew 安裝:
|
||||
✓ 已移除
|
||||
5. launchctl 服務:
|
||||
✓ 已移除
|
||||
6. 數據目錄:
|
||||
✓ 保留 (或 ✗ 已刪除)
|
||||
7. 日誌目錄:
|
||||
✓ 保留 (或 ✗ 已刪除)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 手動檢查命令
|
||||
|
||||
```bash
|
||||
# 1. 檢查進程
|
||||
ps aux | grep mariadb | grep -v grep
|
||||
|
||||
# 2. 檢查 Port
|
||||
lsof -i :3306
|
||||
|
||||
# 3. 測試連線
|
||||
mariadb -u root -e "SELECT 1;"
|
||||
|
||||
# 4. 查看所有數據庫
|
||||
mariadb -u root -e "SHOW DATABASES;"
|
||||
|
||||
# 5. 查看用戶
|
||||
mariadb -u root -e "SELECT User, Host FROM mysql.user;"
|
||||
|
||||
# 6. 查看表
|
||||
mariadb -u root -e "USE mysql; SHOW TABLES;"
|
||||
|
||||
# 7. 查看日誌
|
||||
tail -20 /Users/accusys/momentry/log/mariadb.log
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 連線資訊
|
||||
|
||||
| 項目 | 值 |
|
||||
|------|-----|
|
||||
| Host | localhost |
|
||||
| Port | 3306 |
|
||||
| User | root |
|
||||
|
||||
---
|
||||
|
||||
## 環境變數
|
||||
|
||||
在 `.env` 中:
|
||||
|
||||
```env
|
||||
MARIADB_URL=mariadb://root@localhost:3306
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 遠端訪問
|
||||
|
||||
- MariaDB 綁定到所有網路介面 (0.0.0.0)
|
||||
- 本地網路其他機器可透過 IP 訪問
|
||||
- 請設定用戶權限限制訪問
|
||||
|
||||
---
|
||||
|
||||
## 故障排除
|
||||
|
||||
### MariaDB 無法啟動
|
||||
|
||||
```bash
|
||||
# 檢查日誌
|
||||
tail -f /Users/accusys/momentry/log/mariadb.log
|
||||
|
||||
# 檢查目錄權限
|
||||
ls -la /Users/accusys/momentry/var/mariadb/
|
||||
|
||||
# 重新設定權限
|
||||
chown -R $(whoami):staff /Users/accusys/momentry/var/mariadb
|
||||
```
|
||||
|
||||
### Port 被佔用
|
||||
|
||||
```bash
|
||||
# 檢查哪個程序佔用 port 3306
|
||||
lsof -i :3306
|
||||
|
||||
# 終止佔用程序
|
||||
kill <PID>
|
||||
```
|
||||
|
||||
### 需要重新載入 plist
|
||||
|
||||
```bash
|
||||
# 卸載舊服務 (如果存在)
|
||||
sudo launchctl unload /Library/LaunchDaemons/com.momentry.mariadb.plist 2>/dev/null
|
||||
|
||||
# 載入新服務
|
||||
sudo launchctl load /Library/LaunchDaemons/com.momentry.mariadb.plist
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 檔案位置
|
||||
|
||||
| 類型 | 路徑 | 說明 |
|
||||
|------|------|------|
|
||||
| 安裝 | `/opt/homebrew/opt/mariadb/` | MariaDB 安裝目錄 |
|
||||
| 執行檔 | `/opt/homebrew/opt/mariadb/bin/mariadbd` | MariaDB 執行檔 |
|
||||
| 數據目錄 | `/Users/accusys/momentry/var/mariadb/` | 數據儲存 |
|
||||
| 日誌 | `/Users/accusys/momentry/log/mariadb.log` | 執行日誌 |
|
||||
| 錯誤日誌 | `/Users/accusys/momentry/log/mariadb.error.log` | 錯誤日誌 |
|
||||
| plist | `/Library/LaunchDaemons/com.momentry.mariadb.plist` | 開機啟動 |
|
||||
| 備份 | `/Users/accusys/momentry/var/mariadb_backup/` | 數據備份 |
|
||||
|
||||
---
|
||||
|
||||
## 備份與恢復
|
||||
|
||||
### 備份用戶配置
|
||||
|
||||
已創建專用備份用戶:
|
||||
- 用戶名:`momentry_backup`
|
||||
- 密碼:`momentry_backup_pwd_2026`
|
||||
- 權限:SELECT, LOCK TABLES, SHOW VIEW, EVENT, TRIGGER
|
||||
|
||||
### 備份 (mysqldump)
|
||||
|
||||
```bash
|
||||
# 備份所有數據庫
|
||||
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
|
||||
mysqldump -u momentry_backup -pmomentry_backup_pwd_2026 --all-databases | gzip > \
|
||||
/Users/accusys/momentry/backup/daily/mariadb/mariadb_db_all_${TIMESTAMP}.sql.gz
|
||||
|
||||
# 備份指定數據庫 (WordPress)
|
||||
mysqldump -u momentry_backup -pmomentry_backup_pwd_2026 wordpress | gzip > \
|
||||
/Users/accusys/momentry/backup/daily/mariadb/mariadb_db_wordpress_${TIMESTAMP}.sql.gz
|
||||
|
||||
# 驗證
|
||||
sha256sum /Users/accusys/momentry/backup/daily/mariadb/mariadb_db_*.sql.gz > \
|
||||
/Users/accusys/momentry/backup/daily/mariadb/mariadb_db_${TIMESTAMP}.sha256
|
||||
```
|
||||
|
||||
### 恢復 (mysql)
|
||||
|
||||
```bash
|
||||
# 恢復所有數據庫
|
||||
gunzip < /Users/accusys/momentry/backup/daily/mariadb/mariadb_db_all_20260316_101802.sql.gz | \
|
||||
mysql -u momentry_backup -pmomentry_backup_pwd_2026
|
||||
|
||||
# 恢復指定數據庫
|
||||
gunzip < /Users/accusys/momentry/backup/daily/mariadb/mariadb_db_wordpress_20260316_101802.sql.gz | \
|
||||
mysql -u momentry_backup -pmomentry_backup_pwd_2026 wordpress
|
||||
```
|
||||
|
||||
### 數據目錄複製 (完整遷移) - 離線
|
||||
|
||||
```bash
|
||||
# 1. 停止 MariaDB
|
||||
mysqladmin -u momentry_backup -pmomentry_backup_pwd_2026 shutdown
|
||||
|
||||
# 2. 複製數據目錄
|
||||
cp -r /opt/homebrew/var/mysql/* /Users/accusys/momentry/var/mariadb/
|
||||
|
||||
# 3. 設定權限
|
||||
chown -R $(whoami):staff /Users/accusys/momentry/var/mariadb
|
||||
|
||||
# 4. 啟動 MariaDB
|
||||
sudo launchctl load /Library/LaunchDaemons/com.momentry.mariadb.plist
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 版本資訊
|
||||
|
||||
- 版本: 12.1.2
|
||||
- Port: 3306
|
||||
- User: root
|
||||
- 數據目錄: /Users/accusys/momentry/var/mariadb/
|
||||
- 日誌目錄: /Users/accusys/momentry/log/
|
||||
@@ -1,464 +0,0 @@
|
||||
# Momentry Core API 安裝指南 (本地部署)
|
||||
|
||||
| 項目 | 內容 |
|
||||
|------|------|
|
||||
| 建立者 | OpenCode |
|
||||
| 建立時間 | 2026-03-23 |
|
||||
| 文件版本 | V1.0 |
|
||||
|
||||
---
|
||||
|
||||
## 版本歷史
|
||||
|
||||
| 版本 | 日期 | 目的 | 操作人 | 工具/模型 |
|
||||
|------|------|------|--------|-----------|
|
||||
| V1.0 | 2026-03-23 | 創建文件 | OpenCode | - |
|
||||
|
||||
---
|
||||
|
||||
## 概述
|
||||
|
||||
本文檔說明如何在 macOS 上安裝 Momentry Core API 服務,配置為本地部署,並設定開機自動啟動。
|
||||
|
||||
Momentry Core API 是一個 Rust 編寫的數位資產管理 API 服務,提供:
|
||||
- 影片搜尋 API (`/api/v1/search`)
|
||||
- n8n 整合 API (`/api/v1/n8n/search`)
|
||||
- 健康檢查端點 (`/health`)
|
||||
- 影片註冊與處理功能
|
||||
|
||||
---
|
||||
|
||||
## 當前狀態
|
||||
|
||||
| 項目 | 狀態 |
|
||||
|------|------|
|
||||
| Momentry Core API | ✅ 已安裝 v0.1.0 |
|
||||
| Binary | `/Users/accusys/momentry_core_0.1/target/release/momentry` |
|
||||
| Port | 3002 |
|
||||
| 反向代理 | Caddy (`api.momentry.ddns.net`) |
|
||||
| 數據庫 | PostgreSQL (momentry) |
|
||||
| 向量庫 | Qdrant |
|
||||
| Cache | Redis |
|
||||
| launchd plist | ✅ 已建立 (/Library/LaunchDaemons/com.momentry.api.plist) |
|
||||
|
||||
---
|
||||
|
||||
## 系統需求
|
||||
|
||||
### 必要服務
|
||||
|
||||
| 服務 | 版本 | 用途 |
|
||||
|------|------|------|
|
||||
| PostgreSQL | 16+ | 主數據庫 |
|
||||
| Redis | 1.0+ | 快取與佇列 |
|
||||
| Qdrant | 1.7+ | 向量搜尋 |
|
||||
| Ollama | 最新 | LLM 與 Embedding |
|
||||
|
||||
### Rust 環境
|
||||
|
||||
```bash
|
||||
# 檢查 Rust 版本
|
||||
rustc --version
|
||||
cargo --version
|
||||
|
||||
# 如需安裝 Rust
|
||||
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 安裝步驟
|
||||
|
||||
### Step 1: 編譯 Momentry Core
|
||||
|
||||
```bash
|
||||
# 進入專案目錄
|
||||
cd /Users/accusys/momentry_core_0.1
|
||||
|
||||
# 編譯 release 版本
|
||||
cargo build --release
|
||||
|
||||
# 驗證編譯結果
|
||||
ls -la target/release/momentry
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Step 2: 設定環境變數
|
||||
|
||||
建立環境變數檔案:
|
||||
|
||||
```bash
|
||||
# 建立執行目錄
|
||||
mkdir -p /Users/accusys/momentry_core_0.1/momentry_runtime/env
|
||||
|
||||
# 建立環境變數檔案
|
||||
cat > /Users/accusys/momentry_core_0.1/momentry_runtime/env/momentry.env << 'EOF'
|
||||
# Database Configuration
|
||||
DATABASE_URL=postgres://accusys@localhost:5432/momentry
|
||||
|
||||
# Redis Configuration
|
||||
REDIS_URL=redis://:accusys@localhost:6379
|
||||
REDIS_PASSWORD=accusys
|
||||
|
||||
# API Server
|
||||
API_HOST=127.0.0.1
|
||||
API_PORT=3002
|
||||
|
||||
# Ollama (LLM)
|
||||
OLLAMA_HOST=http://localhost:11434
|
||||
|
||||
# Qdrant (Vector Database)
|
||||
QDRANT_URL=http://localhost:6333
|
||||
QDRANT_COLLECTION=momentry_chunks
|
||||
EOF
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Step 3: 手動啟動服務
|
||||
|
||||
```bash
|
||||
# 啟動 API 服務
|
||||
cd /Users/accusys/momentry_core_0.1
|
||||
./target/release/momentry server --port 3002
|
||||
|
||||
# 驗證服務
|
||||
curl http://localhost:3002/health
|
||||
# {"status":"ok","version":"0.1.0","uptime_ms":1234}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Step 4: 設定 Caddy 反向代理
|
||||
|
||||
在 `/Users/accusys/momentry/etc/Caddyfile` 中新增:
|
||||
|
||||
```caddy
|
||||
# Momentry Core API
|
||||
api.momentry.ddns.net {
|
||||
reverse_proxy localhost:3002
|
||||
import common_log momentry_api_access
|
||||
}
|
||||
```
|
||||
|
||||
重新載入 Caddy:
|
||||
|
||||
```bash
|
||||
# 重新載入配置
|
||||
caddy reload --config /Users/accusys/momentry/etc/Caddyfile
|
||||
|
||||
# 驗證
|
||||
curl -sk https://api.momentry.ddns.net/health
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Step 5: 建立 launchd plist (開機自動啟動)
|
||||
|
||||
建立 plist 檔案:
|
||||
|
||||
```bash
|
||||
sudo tee /Library/LaunchDaemons/com.momentry.api.plist << 'EOF'
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>Label</key>
|
||||
<string>com.momentry.api</string>
|
||||
|
||||
<key>UserName</key>
|
||||
<string>accusys</string>
|
||||
|
||||
<key>GroupName</key>
|
||||
<string>staff</string>
|
||||
|
||||
<key>WorkingDirectory</key>
|
||||
<string>/Users/accusys/momentry_core_0.1</string>
|
||||
|
||||
<key>ProgramArguments</key>
|
||||
<array>
|
||||
<string>/Users/accusys/momentry_core_0.1/target/release/momentry</string>
|
||||
<string>server</string>
|
||||
<string>--port</string>
|
||||
<string>3002</string>
|
||||
</array>
|
||||
|
||||
<key>EnvironmentVariables</key>
|
||||
<dict>
|
||||
<key>PATH</key>
|
||||
<string>/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin</string>
|
||||
|
||||
<key>DATABASE_URL</key>
|
||||
<string>postgres://accusys@localhost:5432/momentry</string>
|
||||
|
||||
<key>REDIS_URL</key>
|
||||
<string>redis://:accusys@localhost:6379</string>
|
||||
|
||||
<key>REDIS_PASSWORD</key>
|
||||
<string>accusys</string>
|
||||
|
||||
<key>OLLAMA_HOST</key>
|
||||
<string>http://localhost:11434</string>
|
||||
|
||||
<key>QDRANT_URL</key>
|
||||
<string>http://localhost:6333</string>
|
||||
</dict>
|
||||
|
||||
<key>RunAtLoad</key>
|
||||
<true/>
|
||||
|
||||
<key>KeepAlive</key>
|
||||
<true/>
|
||||
|
||||
<key>StandardOutPath</key>
|
||||
<string>/Users/accusys/momentry/log/momentry_api.log</string>
|
||||
|
||||
<key>StandardErrorPath</key>
|
||||
<string>/Users/accusys/momentry/log/momentry_api.error.log</string>
|
||||
</dict>
|
||||
</plist>
|
||||
EOF
|
||||
```
|
||||
|
||||
建立日誌檔案:
|
||||
|
||||
```bash
|
||||
# 建立日誌目錄(如不存在)
|
||||
mkdir -p /Users/accusys/momentry/log
|
||||
|
||||
# 建立日誌檔案
|
||||
touch /Users/accusys/momentry/log/momentry_api.log
|
||||
touch /Users/accusys/momentry/log/momentry_api.error.log
|
||||
|
||||
# 設定權限
|
||||
chown accusys:staff /Users/accusys/momentry/log/momentry_api.log
|
||||
chown accusys:staff /Users/accusys/momentry/log/momentry_api.error.log
|
||||
```
|
||||
|
||||
載入服務:
|
||||
|
||||
```bash
|
||||
# 載入服務
|
||||
sudo launchctl load /Library/LaunchDaemons/com.momentry.api.plist
|
||||
|
||||
# 驗證服務
|
||||
launchctl list | grep momentry.api
|
||||
|
||||
# 檢查服務狀態
|
||||
curl http://localhost:3002/health
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 卸載步驟
|
||||
|
||||
### Step 1: 停止並移除服務
|
||||
|
||||
```bash
|
||||
# 停止服務
|
||||
sudo launchctl unload /Library/LaunchDaemons/com.momentry.api.plist
|
||||
|
||||
# 移除 plist
|
||||
sudo rm /Library/LaunchDaemons/com.momentry.api.plist
|
||||
```
|
||||
|
||||
### Step 2: 移除 Caddy 配置
|
||||
|
||||
從 `/Users/accusys/momentry/etc/Caddyfile` 中移除 `api.momentry.ddns.net` 區塊。
|
||||
|
||||
```bash
|
||||
# 重新載入 Caddy
|
||||
caddy reload --config /Users/accusys/momentry/etc/Caddyfile
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 故障排除
|
||||
|
||||
### API 返回 502 Bad Gateway
|
||||
|
||||
**問題**: `api.momentry.ddns.net` 返回 502 錯誤
|
||||
|
||||
**原因**: Momentry Core API 服務未啟動
|
||||
|
||||
**解決方案**:
|
||||
|
||||
```bash
|
||||
# 檢查服務狀態
|
||||
launchctl list | grep momentry.api
|
||||
|
||||
# 如服務未啟動,手動啟動
|
||||
sudo launchctl load /Library/LaunchDaemons/com.momentry.api.plist
|
||||
|
||||
# 或手動啟動測試
|
||||
cd /Users/accusys/momentry_core_0.1
|
||||
./target/release/momentry server --port 3002
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### API 返回 404 Not Found
|
||||
|
||||
**問題**: 端點返回 404
|
||||
|
||||
**原因**: Binary 過舊,缺少該端點
|
||||
|
||||
**解決方案**:
|
||||
|
||||
```bash
|
||||
# 重新編譯
|
||||
cd /Users/accusys/momentry_core_0.1
|
||||
cargo build --release
|
||||
|
||||
# 重啟服務
|
||||
sudo launchctl unload /Library/LaunchDaemons/com.momentry.api.plist
|
||||
sudo launchctl load /Library/LaunchDaemons/com.momentry.api.plist
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 服務無法啟動
|
||||
|
||||
**問題**: launchd 無法啟動服務
|
||||
|
||||
**檢查步驟**:
|
||||
|
||||
```bash
|
||||
# 檢查日誌
|
||||
tail -50 /Users/accusys/momentry/log/momentry_api.error.log
|
||||
|
||||
# 檢查 plist 語法
|
||||
plutil -lint /Library/LaunchDaemons/com.momentry.api.plist
|
||||
|
||||
# 檢查權限
|
||||
ls -la /Users/accusys/momentry_core_0.1/target/release/momentry
|
||||
|
||||
# 手動測試
|
||||
/Users/accusys/momentry_core_0.1/target/release/momentry server --port 3002
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 檔案位置
|
||||
|
||||
| 類型 | 路徑 | 說明 |
|
||||
|------|------|------|
|
||||
| Binary | `/Users/accusys/momentry_core_0.1/target/release/momentry` | 執行檔 |
|
||||
| 環境變數 | `/Users/accusys/momentry_core_0.1/momentry_runtime/env/momentry.env` | 環境設定 |
|
||||
| launchd plist | `/Library/LaunchDaemons/com.momentry.api.plist` | 開機啟動配置 |
|
||||
| 日誌 | `/Users/accusys/momentry/log/momentry_api.log` | 標準輸出 |
|
||||
| 錯誤日誌 | `/Users/accusys/momentry/log/momentry_api.error.log` | 錯誤輸出 |
|
||||
| Caddy 配置 | `/Users/accusys/momentry/etc/Caddyfile` | 反向代理配置 |
|
||||
|
||||
---
|
||||
|
||||
## 常用指令
|
||||
|
||||
### 服務管理
|
||||
|
||||
```bash
|
||||
# 啟動服務
|
||||
sudo launchctl load /Library/LaunchDaemons/com.momentry.api.plist
|
||||
|
||||
# 停止服務
|
||||
sudo launchctl unload /Library/LaunchDaemons/com.momentry.api.plist
|
||||
|
||||
# 重啟服務
|
||||
sudo launchctl unload /Library/LaunchDaemons/com.momentry.api.plist
|
||||
sudo launchctl load /Library/LaunchDaemons/com.momentry.api.plist
|
||||
|
||||
# 檢查服務狀態
|
||||
launchctl list | grep momentry.api
|
||||
```
|
||||
|
||||
### 健康檢查
|
||||
|
||||
```bash
|
||||
# 本地健康檢查
|
||||
curl http://localhost:3002/health
|
||||
|
||||
# 詳細健康檢查
|
||||
curl http://localhost:3002/health/detailed
|
||||
|
||||
# 外部健康檢查
|
||||
curl -sk https://api.momentry.ddns.net/health
|
||||
```
|
||||
|
||||
### API 測試
|
||||
|
||||
```bash
|
||||
# 搜尋 API
|
||||
curl -X POST http://localhost:3002/api/v1/search \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"query":"test"}'
|
||||
|
||||
# n8n 搜尋 API
|
||||
curl -X POST http://localhost:3002/api/v1/n8n/search \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"query":"test"}'
|
||||
|
||||
# 列出影片
|
||||
curl http://localhost:3002/api/v1/videos
|
||||
```
|
||||
|
||||
### 日誌查看
|
||||
|
||||
```bash
|
||||
# 查看最近的日誌
|
||||
tail -50 /Users/accusys/momentry/log/momentry_api.log
|
||||
|
||||
# 即時監控日誌
|
||||
tail -f /Users/accusys/momentry/log/momentry_api.log
|
||||
|
||||
# 查看錯誤日誌
|
||||
tail -50 /Users/accusys/momentry/log/momentry_api.error.log
|
||||
```
|
||||
|
||||
### 重新編譯
|
||||
|
||||
```bash
|
||||
# 編譯 release 版本
|
||||
cd /Users/accusys/momentry_core_0.1
|
||||
cargo build --release
|
||||
|
||||
# 編譯後重啟服務
|
||||
sudo launchctl unload /Library/LaunchDaemons/com.momentry.api.plist
|
||||
sudo launchctl load /Library/LaunchDaemons/com.momentry.api.plist
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## API 端點
|
||||
|
||||
| 端點 | 方法 | 說明 |
|
||||
|------|------|------|
|
||||
| `/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 | 查詢 UUID |
|
||||
| `/api/v1/videos` | GET | 列出所有影片 |
|
||||
| `/api/v1/progress/:uuid` | GET | 查詢處理進度 |
|
||||
|
||||
---
|
||||
|
||||
## 版本資訊
|
||||
|
||||
- 版本: 0.1.0
|
||||
- 安裝日期: 2026-03-23
|
||||
- Rust 版本: 1.xx
|
||||
- 文件更新: 2026-03-23
|
||||
|
||||
---
|
||||
|
||||
## 相關文件
|
||||
|
||||
- `docs/SERVICES.md` - 服務總覽
|
||||
- `docs/API_REFERENCE.md` - API 參考
|
||||
- `docs/INSTALL_POSTGRESQL.md` - PostgreSQL 安裝
|
||||
- `docs/INSTALL_REDIS.md` - Redis 安裝
|
||||
- `docs/INSTALL_QDRANT.md` - Qdrant 安裝
|
||||
- `docs/PENDING_ISSUES.md` - 待解決問題
|
||||
@@ -1,392 +0,0 @@
|
||||
# MongoDB 安裝指南 (本地部署)
|
||||
|
||||
| 項目 | 內容 |
|
||||
|------|------|
|
||||
| 建立者 | Warren |
|
||||
| 建立時間 | 2026-03-15 |
|
||||
| 文件版本 | V1.0 |
|
||||
|
||||
---
|
||||
|
||||
## 版本歷史
|
||||
|
||||
| 版本 | 日期 | 目的 | 操作人 | 工具/模型 |
|
||||
|------|------|------|--------|-----------|
|
||||
| V1.0 | 2026-03-15 | 創建文件 | Warren | OpenCode / MiniMax M2.5 |
|
||||
|
||||
---
|
||||
|
||||
## 概述
|
||||
|
||||
本文檔說明如何在 macOS 上安裝 MongoDB Community Edition,配置為本地部署,支援遠端訪問。
|
||||
|
||||
---
|
||||
|
||||
## 當前狀態
|
||||
|
||||
| 項目 | 狀態 |
|
||||
|------|------|
|
||||
| MongoDB (mongodb-community) | ✅ 已安裝 v8.2.6 |
|
||||
| 數據目錄 | /opt/homebrew/var/mongodb |
|
||||
| 日誌目錄 | /Users/accusys/momentry/log |
|
||||
|
||||
---
|
||||
|
||||
## 安裝步驟
|
||||
|
||||
### Step 1: 安裝 MongoDB Community
|
||||
|
||||
```bash
|
||||
# 安裝 MongoDB Community
|
||||
brew tap mongodb/brew
|
||||
brew install mongodb-community
|
||||
```
|
||||
|
||||
**驗證**:
|
||||
```bash
|
||||
mongod --version
|
||||
# db version v8.x.x
|
||||
mongosh --version
|
||||
# 2.7.x
|
||||
sudo launchctl list | grep mongo
|
||||
# 確認 MongoDB 服務已載入
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Step 2: 數據目錄 (已存在 - 共用)
|
||||
|
||||
數據目錄使用 homebrew 預設位置:
|
||||
- 數據目錄: `/opt/homebrew/var/mongodb`
|
||||
- 配置目錄: `/opt/homebrew/etc/mongod.conf`
|
||||
- 日誌目錄: `/Users/accusys/momentry/log`
|
||||
|
||||
**建立配置目錄和日誌文件**:
|
||||
```bash
|
||||
# 建立配置目錄
|
||||
mkdir -p /Users/accusys/momentry/etc/mongodb
|
||||
|
||||
# 建立日誌文件
|
||||
touch /Users/accusys/momentry/log/mongodb.log
|
||||
touch /Users/accusys/momentry/log/mongodb.error.log
|
||||
|
||||
# 確認權限:
|
||||
ls -la /Users/accusys/momentry/
|
||||
chown -R accusys:staff /Users/accusys/momentry
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Step 3: 使用 LaunchAgent 啟動 (開機自動)
|
||||
|
||||
```bash
|
||||
# 複製 plist 到 LaunchDaemons 目錄 (開機自動需要 root 權限)
|
||||
sudo cp /Users/accusys/momentry_core_0.1/momentry_runtime/plist/com.momentry.mongodb.plist \
|
||||
/Library/LaunchDaemons/
|
||||
|
||||
# 載入並啟動
|
||||
sudo launchctl load /Library/LaunchDaemons/com.momentry.mongodb.plist
|
||||
|
||||
# 驗證
|
||||
launchctl list | grep mongodb
|
||||
pgrep -a mongod
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Step 4: 建立資料庫用戶
|
||||
|
||||
```bash
|
||||
mongosh --eval '
|
||||
use admin
|
||||
db.createUser({
|
||||
user: "accusys",
|
||||
pwd: "Test3200Test3200",
|
||||
roles: [
|
||||
{ role: "readWrite", db: "momentry" },
|
||||
{ role: "dbAdmin", db: "momentry" }
|
||||
]
|
||||
})
|
||||
|
||||
---
|
||||
|
||||
### Step 4: 驗證安裝
|
||||
|
||||
```bash
|
||||
# 檢查進程
|
||||
pgrep -a mongod
|
||||
|
||||
# 檢查端口
|
||||
lsof -i :27017
|
||||
|
||||
# 測試連線
|
||||
mongosh --eval "db.adminCommand('ping')"
|
||||
|
||||
# 檢查 LaunchAgent
|
||||
launchctl list | grep mongodb
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 監控配置
|
||||
|
||||
### 添加到監控配置
|
||||
|
||||
在 `monitor/config/monitor_config.yaml` 中添加:
|
||||
|
||||
```yaml
|
||||
database:
|
||||
mongodb:
|
||||
enabled: true
|
||||
host: "localhost"
|
||||
port: 27017
|
||||
user: "accusys"
|
||||
database: "momentry"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 卸載步驟
|
||||
|
||||
### 重要: 路徑說明
|
||||
|
||||
| 路徑 | 類型 | 說明 |
|
||||
|------|------|------|
|
||||
| `/Users/accusys/momentry/` | 共用 | **不要刪除** - 多個系統共用 |
|
||||
| `/Users/accusys/momentry/var` | 共用 | **不要刪除** - 數據目錄 |
|
||||
| `/Users/accusys/momentry/etc/mongodb/` | 配置 | **不要刪除** - MongoDB 配置 |
|
||||
| `/Users/accusys/momentry/log` | 共用 | **不要刪除** - 日誌目錄 |
|
||||
| `~/.mongosh_history` | 專屬 | 可選刪除 - Mongo Shell 歷史 |
|
||||
|
||||
### Step 1: 停止 MongoDB
|
||||
|
||||
```bash
|
||||
# 找到 MongoDB 進程
|
||||
ps aux | grep mongod | grep -v grep
|
||||
|
||||
# 停止 MongoDB
|
||||
pkill mongod
|
||||
# 或
|
||||
kill <PID>
|
||||
|
||||
# 確認停止
|
||||
ps aux | grep mongod | grep -v grep || echo "MongoDB 已停止"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Step 2: 卸載 MongoDB
|
||||
|
||||
```bash
|
||||
# 完全卸載 (保留數據)
|
||||
brew uninstall mongodb-community
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Step 3: 刪除專屬檔案
|
||||
|
||||
```bash
|
||||
# 刪除 MongoDB 專屬配置 (如果有)
|
||||
rm -f ~/.mongosh_history
|
||||
|
||||
# 刪除臨時文件 (可選)
|
||||
rm -rf /tmp/mongodb-*
|
||||
```
|
||||
|
||||
**注意: 不要刪除以下共用目錄**:
|
||||
```bash
|
||||
# 這些是共用的,不要刪除!
|
||||
# /Users/accusys/momentry/var
|
||||
# /Users/accusys/momentry/log
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Step 4: 卸載後檢查清單
|
||||
|
||||
```bash
|
||||
echo "=== MongoDB 卸載後檢查 ==="
|
||||
|
||||
# 1. 檢查 MongoDB 進程
|
||||
echo "1. MongoDB 進程:"
|
||||
ps aux | grep mongod | grep -v grep && echo " ✗ 仍在運行" || echo " ✓ 已停止"
|
||||
|
||||
# 2. 檢查 Port 27017
|
||||
echo "2. Port 27017:"
|
||||
lsof -i :27017 > /dev/null 2>&1 && echo " ✗ 仍被佔用" || echo " ✓ 已釋放"
|
||||
|
||||
# 3. 檢查 mongod 命令
|
||||
echo "3. mongod 命令:"
|
||||
which mongod > /dev/null 2>&1 && echo " ✗ 仍存在" || echo " ✓ 已移除"
|
||||
|
||||
# 4. 檢查 launchctl
|
||||
echo "4. launchctl 服務:"
|
||||
sudo launchctl list | grep mongo > /dev/null 2>&1 && echo " ✗ 仍存在" || echo " ✓ 已移除"
|
||||
|
||||
# 5. 檢查 Homebrew
|
||||
echo "5. Homebrew 移除:"
|
||||
brew list mongo > /dev/null 2>&1 && echo " ✗ 仍存在" || echo " ✓ 已移除"
|
||||
|
||||
# 6. 檢查數據目錄 (應該存在)
|
||||
echo "6. 數據目錄:"
|
||||
[ -d "/Users/accusys/momentry/var" ] && echo " ✓ 保留" || echo " ✗ 已刪除"
|
||||
|
||||
# 7. 檢查日誌目錄 (應該存在)
|
||||
echo "7. 日誌目錄:"
|
||||
[ -d "/Users/accusys/momentry/log" ] && echo " ✓ 保留" || echo " ✗ 已刪除"
|
||||
```
|
||||
|
||||
**預期結果**:
|
||||
```
|
||||
=== MongoDB 卸載後檢查 ===
|
||||
1. MongoDB 進程:
|
||||
✓ 已停止
|
||||
2. Port 27017:
|
||||
✓ 已釋放
|
||||
3. mongod 命令:
|
||||
✓ 已移除
|
||||
4. launchctl 服務:
|
||||
✓ 已移除
|
||||
5. Homebrew 移除:
|
||||
✓ 已移除
|
||||
6. 數據目錄:
|
||||
✓ 保留
|
||||
7. 日誌目錄:
|
||||
✓ 保留
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 手動檢查命令
|
||||
|
||||
```bash
|
||||
# 1. 檢查 Process 是否運行
|
||||
ps aux | grep mongo | grep -v grep
|
||||
|
||||
# 2. 檢查 Port 27017
|
||||
lsof -i :27017
|
||||
|
||||
# 3. 測試連線 (無認證)
|
||||
mongosh --eval "db.adminCommand('ping')"
|
||||
|
||||
# 4. 測試連線 (有認證)
|
||||
mongosh "mongodb://accusys:Test3200Test3200@localhost:27017/admin" --eval "db.adminCommand('ping')"
|
||||
|
||||
# 5. 查看所有資料庫
|
||||
mongosh "mongodb://accusys:Test3200Test3200@localhost:27017/admin" --quiet --eval "db.adminCommand({listDatabases:1}).databases"
|
||||
|
||||
# 6. 查看用戶
|
||||
mongosh "mongodb://accusys:Test3200Test3200@localhost:27017/admin" --quiet --eval "db.getUser('accusys')"
|
||||
|
||||
# 7. 查看日誌
|
||||
tail -20 /Users/accusys/momentry/log/mongodb.log
|
||||
tail -20 /Users/accusys/momentry/log/mongodb.error.log
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 管理命令
|
||||
|
||||
### 啟動/停止
|
||||
|
||||
```bash
|
||||
# 使用 LaunchAgent (開機自動 - LaunchDaemons 目錄)
|
||||
sudo launchctl load /Library/LaunchDaemons/com.momentry.mongodb.plist # 啟動
|
||||
sudo launchctl unload /Library/LaunchDaemons/com.momentry.mongodb.plist # 停止
|
||||
|
||||
# 手動啟動 (僅除錯用)
|
||||
nohup /opt/homebrew/bin/mongod \
|
||||
--dbpath /Users/accusys/momentry/var \
|
||||
--logpath /Users/accusys/momentry/log/mongodb.log \
|
||||
--port 27017 \
|
||||
--bind_ip 0.0.0.0 \
|
||||
> /Users/accusys/momentry/log/mongodb.log 2>&1 &
|
||||
|
||||
# 強制停止
|
||||
pkill mongod
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 連線字串
|
||||
|
||||
```bash
|
||||
# 無認證 (本地)
|
||||
mongodb://localhost:27017
|
||||
|
||||
# 有認證 (admin 資料庫)
|
||||
mongodb://accusys:Test3200Test3200@localhost:27017/admin
|
||||
|
||||
# 有認證 (momentry 資料庫)
|
||||
mongodb://accusys:Test3200Test3200@localhost:27017/momentry?authSource=admin
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 環境變數
|
||||
|
||||
在 `.env` 中:
|
||||
|
||||
```env
|
||||
MONGODB_URL=mongodb://accusys:Test3200Test3200@localhost:27017/admin
|
||||
MONGODB_DATABASE=momentry
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 遠端訪問
|
||||
|
||||
- MongoDB 綁定到 `0.0.0.0` (所有網路介面)
|
||||
- 本地網路其他機器可透過 IP 訪問
|
||||
- 建議設定防火牆規則限制訪問 IP
|
||||
|
||||
---
|
||||
|
||||
## 故障排除
|
||||
|
||||
### MongoDB 無法啟動
|
||||
|
||||
```bash
|
||||
# 檢查日誌
|
||||
tail -f /Users/accusys/momentry/log/mongodb.log
|
||||
|
||||
# 檢查目錄權限
|
||||
ls -la /Users/accusys/momentry/
|
||||
|
||||
# 重新設定權限
|
||||
chown -R $(whoami):staff /Users/accusys/momentry
|
||||
```
|
||||
|
||||
### Port 被佔用
|
||||
|
||||
```bash
|
||||
# 檢查哪個程序佔用 port 27017
|
||||
lsof -i :27017
|
||||
|
||||
# 終止佔用程序
|
||||
kill <PID>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 檔案位置
|
||||
|
||||
| 類型 | 路徑 | 說明 |
|
||||
|------|------|------|
|
||||
| 數據目錄 | `/Users/accusys/momentry/var` | **共用 - 不要刪除** |
|
||||
| 日誌目錄 | `/Users/accusys/momentry/log` | **共用 - 不要刪除** |
|
||||
| mongod | `/opt/homebrew/bin/mongod` | 安裝後存在 |
|
||||
| Homebrew | `/opt/homebrew/Cellar/mongodb-community/` | 卸載時刪除 |
|
||||
| Homebrew | `/opt/homebrew/Cellar/mongodb-database-tools/` | 卸載時刪除 |
|
||||
| Homebrew | `/opt/homebrew/Cellar/mongosh/` | 卸載時刪除 |
|
||||
| 配置檔 | `/opt/homebrew/etc/mongod.conf` | 卸載時刪除 |
|
||||
|
||||
---
|
||||
|
||||
## 版本資訊
|
||||
|
||||
- 用戶: accusys
|
||||
- 密碼: Test3200Test3200
|
||||
- 數據目錄: /Users/accusys/momentry/var (共用 - 不要刪除!)
|
||||
- 日誌目錄: /Users/accusys/momentry/log (共用 - 不要刪除!)
|
||||
@@ -1,489 +0,0 @@
|
||||
# n8n 安裝指南 (本地部署)
|
||||
|
||||
| 項目 | 內容 |
|
||||
|------|------|
|
||||
| 建立者 | Warren |
|
||||
| 建立時間 | 2026-03-16 |
|
||||
| 文件版本 | V1.0 |
|
||||
|
||||
---
|
||||
|
||||
## 版本歷史
|
||||
|
||||
| 版本 | 日期 | 目的 | 操作人 | 工具/模型 |
|
||||
|------|------|------|--------|-----------|
|
||||
| V1.0 | 2026-03-16 | 創建文件 | Warren | OpenCode / MiniMax M2.5 |
|
||||
|
||||
---
|
||||
|
||||
## 概述
|
||||
|
||||
本文檔說明如何在 macOS 上安裝 n8n 工作流自動化平台,配置為本地部署,使用 Queue 模式。
|
||||
|
||||
---
|
||||
|
||||
## 當前狀態
|
||||
|
||||
| 項目 | 狀態 |
|
||||
|------|------|
|
||||
| n8n | ✅ 已安裝 v2.12.3 |
|
||||
| 數據目錄 | /Users/accusys/momentry/var/n8n/ |
|
||||
| 日誌目錄 | /Users/accusys/momentry/log/ |
|
||||
| Main Plist | /Library/LaunchDaemons/com.momentry.n8n.main.plist |
|
||||
| Worker Plist | /Library/LaunchDaemons/com.momentry.n8n.worker.plist |
|
||||
| 數據庫 | PostgreSQL (n8n) |
|
||||
| 隊列 | Redis |
|
||||
| Launchd 狀態 | ✅ Main + Worker 已註冊 |
|
||||
| RunAtLoad | ✅ 已設定 |
|
||||
| KeepAlive | ✅ 已設定 |
|
||||
|
||||
### 重要更新 (2026-03-24)
|
||||
|
||||
1. **n8n Main + Worker**: 兩個服務都使用自定義 plist
|
||||
2. **Runner 禁用**: 為避免端口衝突,Main 服務設定 `N8N_RUNNERS_ENABLED=false`
|
||||
3. **Worker 端口**: Worker 使用 5681, 5682, 5690, 5691 端口
|
||||
|
||||
---
|
||||
|
||||
## 安裝步驟
|
||||
|
||||
### Step 1: 安裝 n8n (使用 brew)
|
||||
|
||||
```bash
|
||||
# 安裝 n8n
|
||||
brew install n8n
|
||||
```
|
||||
|
||||
**驗證**:
|
||||
```bash
|
||||
n8n --version
|
||||
# 2.12.3
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Step 2: 建立目錄
|
||||
|
||||
```bash
|
||||
# 建立數據目錄
|
||||
mkdir -p /Users/accusys/momentry/var/n8n
|
||||
|
||||
# 建立配置目錄
|
||||
mkdir -p /Users/accusys/momentry/etc/n8n
|
||||
|
||||
# 建立日誌目錄
|
||||
mkdir -p /Users/accusys/momentry/log
|
||||
|
||||
# 建立日誌文件
|
||||
touch /Users/accusys/momentry/log/n8n.main.log
|
||||
touch /Users/accusys/momentry/log/n8n.main.error.log
|
||||
touch /Users/accusys/momentry/log/n8n.worker.log
|
||||
touch /Users/accusys/momentry/log/n8n.worker.error.log
|
||||
|
||||
# 設定權限
|
||||
chown -R accusys:staff /Users/accusys/momentry/var/n8n
|
||||
chown -R accusys:staff /Users/accusys/momentry/etc/n8n
|
||||
chown -R accusys:staff /Users/accusys/momentry/log
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Step 3: 數據遷移 (如果從舊位置遷移)
|
||||
|
||||
```bash
|
||||
# 停止舊服務
|
||||
sudo launchctl unload /Library/LaunchDaemons/com.n8n.main.plist
|
||||
sudo launchctl unload /Library/LaunchDaemons/com.n8n.worker.plist
|
||||
|
||||
# 複製數據
|
||||
cp -r /Users/accusys/.n8n/* /Users/accusys/momentry/var/n8n/
|
||||
|
||||
# 設定權限
|
||||
chown -R accusys:staff /Users/accusys/momentry/var/n8n
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Step 4: 使用 plist 開機自動啟動
|
||||
|
||||
```bash
|
||||
# 複製 plist 到 LaunchDaemons 目錄
|
||||
sudo cp /Users/accusys/momentry_core_0.1/momentry_runtime/plist/com.momentry.n8n.main.plist /Library/LaunchDaemons/
|
||||
sudo cp /Users/accusys/momentry_core_0.1/momentry_runtime/plist/com.momentry.n8n.worker.plist /Library/LaunchDaemons/
|
||||
|
||||
# 移除舊 plist (如果存在)
|
||||
sudo launchctl unload /Library/LaunchDaemons/com.n8n.main.plist 2>/dev/null
|
||||
sudo launchctl unload /Library/LaunchDaemons/com.n8n.worker.plist 2>/dev/null
|
||||
sudo rm /Library/LaunchDaemons/com.n8n.main.plist 2>/dev/null
|
||||
sudo rm /Library/LaunchDaemons/com.n8n.worker.plist 2>/dev/null
|
||||
|
||||
# 載入並啟動
|
||||
sudo launchctl load /Library/LaunchDaemons/com.momentry.n8n.main.plist
|
||||
sudo launchctl load /Library/LaunchDaemons/com.momentry.n8n.worker.plist
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 監控配置
|
||||
|
||||
### 添加到監控配置
|
||||
|
||||
在 `monitor/config/monitor_config.yaml` 中添加:
|
||||
|
||||
```yaml
|
||||
service:
|
||||
services:
|
||||
- name: "n8n"
|
||||
type: "http"
|
||||
port: 5678
|
||||
host: "localhost"
|
||||
check_url: "http://localhost:5678/"
|
||||
timeout: 5
|
||||
enabled: true
|
||||
```
|
||||
|
||||
### 添加健康檢查函數
|
||||
|
||||
在 `monitor/service/health_check.sh` 中添加:
|
||||
|
||||
```bash
|
||||
check_n8n() {
|
||||
local start=$(date +%s%N)
|
||||
if curl -s http://localhost:5678/ > /dev/null 2>&1; then
|
||||
local end=$(date +%s%N)
|
||||
local ms=$(( (end - start) / 1000000 ))
|
||||
echo -e "${GREEN}✓${NC} n8n (5678) - ${ms}ms"
|
||||
record_service "n8n" "up" "$ms" ""
|
||||
return 0
|
||||
else
|
||||
echo -e "${RED}✗${NC} n8n (5678) - Down"
|
||||
record_service "n8n" "down" "0" "Connection failed"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
```
|
||||
|
||||
### n8n Workflow 監控
|
||||
|
||||
n8n 有專門的工作流監控腳本,請參考 `monitor/workflow/n8n_workflow_monitor.sh`。
|
||||
|
||||
---
|
||||
|
||||
## 卸載步驟
|
||||
|
||||
### 重要: 路徑說明
|
||||
|
||||
| 路徑 | 類型 | 說明 |
|
||||
|------|------|------|
|
||||
| `/Users/accusys/momentry/var/n8n/` | 數據 | **不要刪除** - n8n 數據 |
|
||||
| `/Users/accusys/momentry/etc/n8n/` | 配置 | **不要刪除** - n8n 配置 |
|
||||
| `/Users/accusys/momentry/log/` | 日誌 | **不要刪除** - n8n 日誌 |
|
||||
| `/opt/homebrew/lib/node_modules/n8n/` | 安裝 | **刪除** - n8n 安裝目錄 |
|
||||
|
||||
### Step 1: 停止 n8n
|
||||
|
||||
```bash
|
||||
# 停止 n8n 服務
|
||||
sudo launchctl unload /Library/LaunchDaemons/com.momentry.n8n.main.plist
|
||||
sudo launchctl unload /Library/LaunchDaemons/com.momentry.n8n.worker.plist
|
||||
|
||||
# 確認停止
|
||||
ps aux | grep n8n | grep -v grep || echo "n8n 已停止"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Step 2: 卸載 n8n
|
||||
|
||||
```bash
|
||||
# 卸載 n8n
|
||||
brew uninstall n8n
|
||||
|
||||
# 移除 plist
|
||||
sudo rm /Library/LaunchDaemons/com.momentry.n8n.main.plist
|
||||
sudo rm /Library/LaunchDaemons/com.momentry.n8n.worker.plist
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Step 3: 刪除專屬檔案
|
||||
|
||||
```bash
|
||||
# 刪除數據目錄 (可選)
|
||||
rm -rf /Users/accusys/momentry/var/n8n
|
||||
|
||||
# 刪除日誌 (可選)
|
||||
rm -f /Users/accusys/momentry/log/n8n-*.log
|
||||
```
|
||||
|
||||
**注意: 不要刪除以下共用目錄**:
|
||||
```bash
|
||||
# 這些是共用的,不要刪除!
|
||||
# /Users/accusys/momentry/var
|
||||
# /Users/accusys/momentry/log
|
||||
# PostgreSQL n8n 數據庫 (如需保留)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Step 4: 卸載後檢查清單
|
||||
|
||||
```bash
|
||||
echo "=== n8n 卸載後檢查 ==="
|
||||
|
||||
# 1. 檢查 n8n 進程
|
||||
echo "1. n8n Main 進程:"
|
||||
ps aux | grep "n8n.*start" | grep -v grep && echo " ✗ 仍在運行" || echo " ✓ 已停止"
|
||||
|
||||
echo "2. n8n Worker 進程:"
|
||||
ps aux | grep "n8n.*worker" | grep -v grep && echo " ✗ 仍在運行" || echo " ✓ 已停止"
|
||||
|
||||
# 2. Port 8085
|
||||
echo "3. Port 8085:"
|
||||
lsof -i :8085 > /dev/null 2>&1 && echo " ✗ 仍被佔用" || echo " ✓ 已釋放"
|
||||
|
||||
# 3. Port 5679 (Worker)
|
||||
echo "4. Port 5679 (Worker):"
|
||||
lsof -i :5679 > /dev/null 2>&1 && echo " ✗ 仍被佔用" || echo " ✓ 已釋放"
|
||||
|
||||
# 4. n8n 命令
|
||||
echo "5. n8n 命令:"
|
||||
which n8n > /dev/null 2>&1 && echo " ✗ 仍存在" || echo " ✓ 已移除"
|
||||
|
||||
# 5. brew 安裝
|
||||
echo "6. brew 安裝:"
|
||||
brew list n8n > /dev/null 2>&1 && echo " ✗ 仍存在" || echo " ✓ 已移除"
|
||||
|
||||
# 6. launchctl 服務
|
||||
echo "7. launchctl 服務:"
|
||||
sudo launchctl list | grep n8n > /dev/null 2>&1 && echo " ✗ 仍存在" || echo " ✓ 已移除"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 備份步驟
|
||||
|
||||
### 備份 n8n 數據
|
||||
|
||||
```bash
|
||||
# 建立備份目錄
|
||||
mkdir -p /Users/accusys/momentry/var/n8n_backup
|
||||
|
||||
# 1. 備份 PostgreSQL 數據庫
|
||||
PGPASSWORD=accusys pg_dump -U accusys -d n8n > /Users/accusys/momentry/var/n8n_backup/n8n_database_$(date +%Y%m%d).sql
|
||||
|
||||
# 2. 備份用戶數據夾
|
||||
cp -r /Users/accusys/momentry/var/n8n /Users/accusys/momentry/var/n8n_backup/
|
||||
|
||||
# 3. 壓縮備份
|
||||
cd /Users/accusys/momentry/var && tar -czvf n8n_backup_$(date +%Y%m%d).tar.gz n8n_backup/
|
||||
|
||||
# 4. 清理臨時備份
|
||||
rm -rf /Users/accusys/momentry/var/n8n_backup
|
||||
```
|
||||
|
||||
### 還原數據
|
||||
|
||||
```bash
|
||||
# 1. 停止 n8n
|
||||
sudo launchctl unload /Library/LaunchDaemons/com.momentry.n8n.main.plist
|
||||
sudo launchctl unload /Library/LaunchDaemons/com.momentry.n8n.worker.plist
|
||||
|
||||
# 2. 還原 PostgreSQL 數據庫
|
||||
PGPASSWORD=accusys psql -U accusys -d n8n < /Users/accusys/momentry/var/n8n_backup/n8n_database_20260314.sql
|
||||
|
||||
# 3. 還原用戶數據夾
|
||||
rm -rf /Users/accusys/momentry/var/n8n
|
||||
cp -r /Users/accusys/momentry/var/n8n_backup/n8n /Users/accusys/momentry/var/n8n
|
||||
chown -R accusys:staff /Users/accusys/momentry/var/n8n
|
||||
|
||||
# 4. 啟動 n8n
|
||||
sudo launchctl load /Library/LaunchDaemons/com.momentry.n8n.main.plist
|
||||
sudo launchctl load /Library/LaunchDaemons/com.momentry.n8n.worker.plist
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 手動檢查命令
|
||||
|
||||
```bash
|
||||
# 1. 檢查進程
|
||||
ps aux | grep n8n | grep -v grep
|
||||
|
||||
# 2. 檢查 Port
|
||||
lsof -i :5678
|
||||
lsof -i :5679
|
||||
|
||||
# 3. 測試連線
|
||||
curl http://localhost:5678/
|
||||
|
||||
# 4. 查看版本
|
||||
n8n --version
|
||||
|
||||
# 5. 查看日誌
|
||||
tail -20 /Users/accusys/momentry/log/n8n-main.log
|
||||
tail -20 /Users/accusys/momentry/log/n8n-worker.log
|
||||
|
||||
# 6. 查看錯誤日誌
|
||||
tail -20 /Users/accusys/momentry/log/n8n-main.error.log
|
||||
tail -20 /Users/accusys/momentry/log/n8n-worker.error.log
|
||||
|
||||
# 7. 檢查 launchctl 狀態
|
||||
sudo launchctl list | grep n8n
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 連線資訊
|
||||
|
||||
| 項目 | 值 |
|
||||
|------|-----|
|
||||
| URL | http://localhost:5678 |
|
||||
| Domain | n8n.momentry.ddns.net |
|
||||
| 數據庫 | PostgreSQL (n8n) |
|
||||
| 隊列 | Redis |
|
||||
| Encryption Key | Test3200Test3200Test3200 |
|
||||
|
||||
### Queue 模式端口
|
||||
|
||||
| 服務 | Port |
|
||||
|------|------|
|
||||
| Main | 5678 |
|
||||
| Task Broker (Worker 連接) | 5679 |
|
||||
|
||||
---
|
||||
|
||||
## 環境變數
|
||||
|
||||
在 `.env` 中:
|
||||
|
||||
```env
|
||||
N8N_URL=http://localhost:5678
|
||||
N8N_USER_FOLDER=/Users/accusys/momentry/var/n8n
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 故障排除
|
||||
|
||||
### n8n 無法啟動
|
||||
|
||||
```bash
|
||||
# 檢查日誌
|
||||
tail -f /Users/accusys/momentry/log/n8n-main.log
|
||||
tail -f /Users/accusys/momentry/log/n8n-worker.log
|
||||
|
||||
# 檢查環境變數
|
||||
launchctl list | grep n8n
|
||||
|
||||
# 檢查數據庫連線
|
||||
PGPASSWORD=accusys psql -U accusys -d n8n -c "SELECT 1"
|
||||
|
||||
# 檢查 Redis 連線
|
||||
redis-cli -a accusys ping
|
||||
```
|
||||
|
||||
### Port 被佔用
|
||||
|
||||
```bash
|
||||
# 檢查哪個程序佔用 port
|
||||
lsof -i :8085
|
||||
|
||||
# 終止佔用程序
|
||||
kill <PID>
|
||||
```
|
||||
|
||||
### 數據庫連線失敗
|
||||
|
||||
```bash
|
||||
# 檢查 PostgreSQL
|
||||
pg_isready -h 127.0.0.1 -p 5432 -U n8n
|
||||
|
||||
# 測試連線
|
||||
PGPASSWORD=accusys psql -h 127.0.0.1 -U n8n -d n8n -c "SELECT version();"
|
||||
```
|
||||
|
||||
### 需要重新載入 plist
|
||||
|
||||
```bash
|
||||
# 卸載舊服務
|
||||
sudo launchctl unload /Library/LaunchDaemons/com.momentry.n8n.main.plist
|
||||
sudo launchctl unload /Library/LaunchDaemons/com.momentry.n8n.worker.plist
|
||||
|
||||
# 載入新服務
|
||||
sudo launchctl load /Library/LaunchDaemons/com.momentry.n8n.main.plist
|
||||
sudo launchctl load /Library/LaunchDaemons/com.momentry.n8n.worker.plist
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 檔案位置
|
||||
|
||||
| 類型 | 路徑 | 說明 |
|
||||
|------|------|------|
|
||||
| 安裝 | `/opt/homebrew/lib/node_modules/n8n/` | n8n 安裝目錄 |
|
||||
| 執行檔 | `/opt/homebrew/bin/n8n` | n8n 執行檔 |
|
||||
| 數據目錄 | `/Users/accusys/momentry/var/n8n/` | 數據儲存 |
|
||||
| 配置目錄 | `/Users/accusys/momentry/etc/n8n/` | 配置儲存 |
|
||||
| Main 日誌 | `/Users/accusys/momentry/log/n8n.main.log` | 主服務日誌 |
|
||||
| Main 錯誤日誌 | `/Users/accusys/momentry/log/n8n.main.error.log` | 主服務錯誤日誌 |
|
||||
| Worker 日誌 | `/Users/accusys/momentry/log/n8n.worker.log` | Worker 日誌 |
|
||||
| Worker 錯誤日誌 | `/Users/accusys/momentry/log/n8n.worker.error.log` | Worker 錯誤日誌 |
|
||||
| Main Plist | `/Library/LaunchDaemons/com.momentry.n8n.main.plist` | 主服務開機啟動 |
|
||||
| Worker Plist | `/Library/LaunchDaemons/com.momentry.n8n.worker.plist` | Worker 開機啟動 |
|
||||
| 備份 | `/Users/accusys/momentry/var/n8n_backup/` | 數據備份 |
|
||||
|
||||
---
|
||||
|
||||
## 數據庫資訊
|
||||
|
||||
n8n 使用 PostgreSQL 作為數據庫:
|
||||
|
||||
| 項目 | 值 |
|
||||
|------|-----|
|
||||
| Database | n8n |
|
||||
| User | n8n |
|
||||
| Host | 127.0.0.1:5432 |
|
||||
| Password | accusys |
|
||||
|
||||
### Redis 隊列資訊
|
||||
|
||||
| 項目 | 值 |
|
||||
|------|-----|
|
||||
| Host | 127.0.0.1:6379 |
|
||||
| Password | accusys |
|
||||
| Mode | Queue (Bull) |
|
||||
|
||||
---
|
||||
|
||||
## 常用指令
|
||||
|
||||
```bash
|
||||
# 啟動 n8n Main
|
||||
n8n start
|
||||
|
||||
# 啟動 n8n Worker
|
||||
n8n worker
|
||||
|
||||
# 查看版本
|
||||
n8n --version
|
||||
|
||||
# 備份數據
|
||||
PGPASSWORD=accusys pg_dump -U accusys -d n8n > n8n_backup.sql
|
||||
|
||||
# 重新載入服務
|
||||
sudo launchctl unload /Library/LaunchDaemons/com.momentry.n8n.main.plist
|
||||
sudo launchctl load /Library/LaunchDaemons/com.momentry.n8n.main.plist
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 版本資訊
|
||||
|
||||
- 版本: 2.3.5
|
||||
- Main Port: 5678
|
||||
- Task Broker (Worker): 5679
|
||||
- 數據目錄: /Users/accusys/momentry/var/n8n/
|
||||
- 日誌目錄: /Users/accusys/momentry/log/
|
||||
- 數據庫: PostgreSQL n8n
|
||||
- 隊列: Redis
|
||||
@@ -1,375 +0,0 @@
|
||||
# Ollama 安裝指南 (本地部署)
|
||||
|
||||
| 項目 | 內容 |
|
||||
|------|------|
|
||||
| 建立者 | Warren |
|
||||
| 建立時間 | 2026-03-15 |
|
||||
| 文件版本 | V1.0 |
|
||||
|
||||
---
|
||||
|
||||
## 版本歷史
|
||||
|
||||
| 版本 | 日期 | 目的 | 操作人 | 工具/模型 |
|
||||
|------|------|------|--------|-----------|
|
||||
| V1.0 | 2026-03-15 | 創建文件 | Warren | OpenCode / MiniMax M2.5 |
|
||||
|
||||
---
|
||||
|
||||
## 概述
|
||||
|
||||
本文檔說明如何在 macOS 上安裝 Ollama,配置為本地部署,用於運行大型語言模型 (LLM)。
|
||||
|
||||
---
|
||||
|
||||
## 當前狀態
|
||||
|
||||
| 項目 | 狀態 |
|
||||
|------|------|
|
||||
| Ollama | ✅ 已安裝 v0.13.5 |
|
||||
| Port | 11434 |
|
||||
| Models 目錄 | /Users/accusys/momentry/var/ollama/models |
|
||||
| 日誌目錄 | /Users/accusys/momentry/log/ |
|
||||
| Plist | /Library/LaunchDaemons/com.momentry.ollama.plist |
|
||||
|
||||
---
|
||||
|
||||
## 安裝步驟
|
||||
|
||||
### Step 1: 安裝 Ollama (使用 brew)
|
||||
|
||||
```bash
|
||||
# 安裝 Ollama
|
||||
brew install ollama
|
||||
```
|
||||
|
||||
**驗證**:
|
||||
```bash
|
||||
ollama --version
|
||||
# ollama version is 0.13.5
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Step 2: 建立目錄
|
||||
|
||||
```bash
|
||||
# 建立數據目錄
|
||||
mkdir -p /Users/accusys/momentry/var/ollama
|
||||
|
||||
# 建立配置目錄
|
||||
mkdir -p /Users/accusys/momentry/etc/ollama
|
||||
|
||||
# 建立日誌目錄
|
||||
mkdir -p /Users/accusys/momentry/log
|
||||
|
||||
# 建立日誌文件
|
||||
touch /Users/accusys/momentry/log/ollama.log
|
||||
touch /Users/accusys/momentry/log/ollama.error.log
|
||||
|
||||
# 設定權限
|
||||
chown -R accusys:staff /Users/accusys/momentry/var/ollama
|
||||
chown -R accusys:staff /Users/accusys/momentry/etc/ollama
|
||||
chown -R accusys:staff /Users/accusys/momentry/log
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Step 3: 使用 plist 開機自動啟動
|
||||
|
||||
```bash
|
||||
# 複製 plist 到 LaunchDaemons 目錄
|
||||
sudo cp /Users/accusys/momentry_core_0.1/momentry_runtime/plist/com.momentry.ollama.plist /Library/LaunchDaemons/
|
||||
|
||||
# 載入並啟動
|
||||
sudo launchctl load /Library/LaunchDaemons/com.momentry.ollama.plist
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 監控配置
|
||||
|
||||
### 添加到監控配置
|
||||
|
||||
在 `monitor/config/monitor_config.yaml` 中添加:
|
||||
|
||||
```yaml
|
||||
service:
|
||||
services:
|
||||
- name: "ollama"
|
||||
type: "http"
|
||||
port: 11434
|
||||
host: "localhost"
|
||||
check_url: "http://localhost:11434/api/tags"
|
||||
timeout: 5
|
||||
enabled: true
|
||||
```
|
||||
|
||||
### 添加健康檢查函數
|
||||
|
||||
在 `monitor/service/health_check.sh` 中添加:
|
||||
|
||||
```bash
|
||||
check_ollama() {
|
||||
local start=$(date +%s%N)
|
||||
if curl -s http://localhost:11434/api/tags > /dev/null 2>&1; then
|
||||
local end=$(date +%s%N)
|
||||
local ms=$(( (end - start) / 1000000 ))
|
||||
echo -e "${GREEN}✓${NC} Ollama (11434) - ${ms}ms"
|
||||
record_service "ollama" "up" "$ms" ""
|
||||
return 0
|
||||
else
|
||||
echo -e "${RED}✗${NC} Ollama (11434) - Down"
|
||||
record_service "ollama" "down" "0" "Connection failed"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 卸載步驟
|
||||
|
||||
### 重要: 路徑說明
|
||||
|
||||
| 路徑 | 類型 | 說明 |
|
||||
|------|------|------|
|
||||
| `/Users/accusys/momentry/var/ollama/` | 數據 | **不要刪除** - Ollama 數據 |
|
||||
| `/Users/accusys/momentry/etc/ollama/` | 配置 | **不要刪除** - Ollama 配置 |
|
||||
| `/Users/accusys/momentry/log/` | 日誌 | **不要刪除** - 日誌目錄 |
|
||||
| `/opt/homebrew/opt/ollama/` | 安裝 | **刪除** - Ollama 安裝目錄 |
|
||||
|
||||
### Step 1: 停止 Ollama
|
||||
|
||||
```bash
|
||||
# 找到 Ollama 進程
|
||||
ps aux | grep ollama | grep -v grep
|
||||
|
||||
# 停止 Ollama
|
||||
pkill ollama
|
||||
|
||||
# 確認停止
|
||||
ps aux | grep ollama | grep -v grep || echo "Ollama 已停止"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Step 2: 卸載 Ollama
|
||||
|
||||
```bash
|
||||
# 卸載 Ollama
|
||||
brew uninstall ollama
|
||||
|
||||
# 移除 plist
|
||||
sudo launchctl unload /Library/LaunchDaemons/com.momentry.ollama.plist
|
||||
sudo rm /Library/LaunchDaemons/com.momentry.ollama.plist
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Step 3: 刪除專屬檔案
|
||||
|
||||
```bash
|
||||
# 刪除日誌 (可選)
|
||||
rm -f /Users/accusys/momentry/log/ollama.log
|
||||
rm -f /Users/accusys/momentry/log/ollama.error.log
|
||||
```
|
||||
|
||||
**注意: 不要刪除以下目錄**:
|
||||
```bash
|
||||
# 這些是重要的,不要刪除!
|
||||
# /Users/accusys/.ollama/models
|
||||
# /Users/accusys/momentry/log
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Step 4: 卸載後檢查清單
|
||||
|
||||
```bash
|
||||
echo "=== Ollama 卸載後檢查 ==="
|
||||
|
||||
# 1. 檢查 Ollama 進程
|
||||
echo "1. Ollama 進程:"
|
||||
ps aux | grep ollama | grep -v grep && echo " ✗ 仍在運行" || echo " ✓ 已停止"
|
||||
|
||||
# 2. Port 11434
|
||||
echo "2. Port 11434:"
|
||||
lsof -i :11434 > /dev/null 2>&1 && echo " ✗ 仍被佔用" || echo " ✓ 已釋放"
|
||||
|
||||
# 3. ollama 命令
|
||||
echo "3. ollama 命令:"
|
||||
which ollama > /dev/null 2>&1 && echo " ✗ 仍存在" || echo " ✓ 已移除"
|
||||
|
||||
# 4. brew 安裝
|
||||
echo "4. brew 安裝:"
|
||||
brew list ollama > /dev/null 2>&1 && echo " ✗ 仍存在" || echo " ✓ 已移除"
|
||||
|
||||
# 5. launchctl 服務
|
||||
echo "5. launchctl 服務:"
|
||||
sudo launchctl list | grep ollama > /dev/null 2>&1 && echo " ✗ 仍存在" || echo " ✓ 已移除"
|
||||
|
||||
# 6. 模型目錄 (應該保留)
|
||||
echo "6. 模型目錄:"
|
||||
[ -d "/Users/accusys/.ollama/models" ] && echo " ✓ 保留" || echo " ✗ 已刪除"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 手動檢查命令
|
||||
|
||||
```bash
|
||||
# 1. 檢查進程
|
||||
ps aux | grep ollama | grep -v grep
|
||||
|
||||
# 2. 檢查 Port
|
||||
lsof -i :11434
|
||||
|
||||
# 3. 測試連線
|
||||
curl http://localhost:11434/
|
||||
|
||||
# 4. 查看版本
|
||||
ollama --version
|
||||
|
||||
# 5. 查看已安裝的模型
|
||||
ollama list
|
||||
|
||||
# 6. 查看日誌
|
||||
tail -20 /Users/accusys/momentry/log/ollama.log
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 連線資訊
|
||||
|
||||
| 項目 | 值 |
|
||||
|------|-----|
|
||||
| Host | localhost |
|
||||
| Port | 11434 |
|
||||
| Models | /Users/accusys/.ollama/models |
|
||||
|
||||
---
|
||||
|
||||
## 環境變數
|
||||
|
||||
在 `.env` 中:
|
||||
|
||||
```env
|
||||
OLLAMA_HOST=0.0.0.0:11434
|
||||
OLLAMA_MODELS=/Users/accusys/.ollama/models
|
||||
OLLAMA_FLASH_ATTENTION=1
|
||||
OLLAMA_KV_CACHE_TYPE=q8_0
|
||||
```
|
||||
|
||||
### 環境變數說明
|
||||
|
||||
| 變數 | 說明 | 預設值 |
|
||||
|------|------|---------|
|
||||
| OLLAMA_HOST | 綁定主機和端口 | 127.0.0.1:11434 |
|
||||
| OLLAMA_MODELS | 模型儲存目錄 | ~/.ollama/models |
|
||||
| OLLAMA_FLASH_ATTENTION | 啟用 Flash Attention | 0 |
|
||||
| OLLAMA_KV_CACHE_TYPE | KV 緩存類型 | f16 |
|
||||
|
||||
---
|
||||
|
||||
## 遠端訪問
|
||||
|
||||
- Ollama 綁定到 `0.0.0.0:11434` (所有網路介面)
|
||||
- 本地網路其他機器可透過 IP 訪問
|
||||
- 請注意安全風險
|
||||
|
||||
---
|
||||
|
||||
## 故障排除
|
||||
|
||||
### Ollama 無法啟動
|
||||
|
||||
```bash
|
||||
# 檢查日誌
|
||||
tail -f /Users/accusys/momentry/log/ollama.log
|
||||
|
||||
# 檢查模型目錄權限
|
||||
ls -la /Users/accusys/.ollama/models/
|
||||
|
||||
# 重新設定權限
|
||||
chown -R $(whoami):staff /Users/accusys/.ollama
|
||||
```
|
||||
|
||||
### Port 被佔用
|
||||
|
||||
```bash
|
||||
# 檢查哪個程序佔用 port 11434
|
||||
lsof -i :11434
|
||||
|
||||
# 終止佔用程序
|
||||
kill <PID>
|
||||
```
|
||||
|
||||
### 需要重新載入 plist
|
||||
|
||||
```bash
|
||||
# 卸載舊服務 (如果存在)
|
||||
sudo launchctl unload /Library/LaunchDaemons/com.momentry.ollama.plist 2>/dev/null
|
||||
|
||||
# 載入新服務
|
||||
sudo launchctl load /Library/LaunchDaemons/com.momentry.ollama.plist
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 檔案位置
|
||||
|
||||
| 類型 | 路徑 | 說明 |
|
||||
|------|------|------|
|
||||
| 安裝 | `/opt/homebrew/opt/ollama/` | Ollama 安裝目錄 |
|
||||
| 執行檔 | `/opt/homebrew/opt/ollama/bin/ollama` | Ollama 執行檔 |
|
||||
| 數據目錄 | `/Users/accusys/momentry/var/ollama/` | 數據儲存 |
|
||||
| 配置目錄 | `/Users/accusys/momentry/etc/ollama/` | 配置儲存 |
|
||||
| 模型目錄 | `/Users/accusys/.ollama/models/` | AI 模型儲存 |
|
||||
| 日誌 | `/Users/accusys/momentry/log/ollama.log` | 執行日誌 |
|
||||
| 錯誤日誌 | `/Users/accusys/momentry/log/ollama.error.log` | 錯誤日誌 |
|
||||
| plist | `/Library/LaunchDaemons/com.momentry.ollama.plist` | 開機啟動 |
|
||||
| 備份 | `/Users/accusys/momentry/var/ollama_backup/environment.txt` | 環境變數備份 |
|
||||
|
||||
---
|
||||
|
||||
## 常用指令
|
||||
|
||||
```bash
|
||||
# 查看版本
|
||||
ollama --version
|
||||
|
||||
# 查看已安裝的模型
|
||||
ollama list
|
||||
|
||||
# 拉取模型
|
||||
ollama pull mistral
|
||||
ollama pull llama2
|
||||
|
||||
# 運行模型
|
||||
ollama run mistral
|
||||
|
||||
# 刪除模型
|
||||
ollama remove mistral
|
||||
|
||||
# 查看模型資訊
|
||||
ollama show mistral
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 已安裝的模型
|
||||
|
||||
查看已安裝的模型:
|
||||
```bash
|
||||
ollama list
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 版本資訊
|
||||
|
||||
- 版本: 0.13.5
|
||||
- Port: 11434
|
||||
- Models: /Users/accusys/.ollama/models/
|
||||
- 日誌目錄: /Users/accusys/momentry/log/
|
||||
@@ -1,395 +0,0 @@
|
||||
# PHP 安裝指南 (本地部署)
|
||||
|
||||
| 項目 | 內容 |
|
||||
|------|------|
|
||||
| 建立者 | Warren |
|
||||
| 建立時間 | 2026-03-16 |
|
||||
| 文件版本 | V1.0 |
|
||||
|
||||
---
|
||||
|
||||
## 版本歷史
|
||||
|
||||
| 版本 | 日期 | 目的 | 操作人 | 工具/模型 |
|
||||
|------|------|------|--------|-----------|
|
||||
| V1.0 | 2026-03-16 | 創建文件 | Warren | OpenCode / MiniMax M2.5 |
|
||||
|
||||
---
|
||||
|
||||
## 概述
|
||||
|
||||
本文檔說明如何在 macOS 上安裝 PHP 及 PHP-FPM,配置為本地部署。
|
||||
|
||||
---
|
||||
|
||||
## 當前狀態
|
||||
|
||||
| 項目 | 狀態 |
|
||||
|------|------|
|
||||
| PHP | ✅ 已安裝 v8.5.2 |
|
||||
| PHP-FPM | ✅ 執行中 |
|
||||
| 配置目錄 | /Users/accusys/momentry/etc/php/ |
|
||||
| 日誌目錄 | /Users/accusys/momentry/log/ |
|
||||
| Plist | /Library/LaunchDaemons/com.momentry.php.plist |
|
||||
|
||||
---
|
||||
|
||||
## 安裝步驟
|
||||
|
||||
### Step 1: 安裝 PHP (使用 brew)
|
||||
|
||||
```bash
|
||||
# 安裝 PHP
|
||||
brew install php
|
||||
|
||||
# 安裝 PHP-FPM (通常包含在 php 中)
|
||||
brew install php --fpm
|
||||
```
|
||||
|
||||
**驗證**:
|
||||
```bash
|
||||
php --version
|
||||
# PHP 8.5.2 (cli)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Step 2: 建立目錄
|
||||
|
||||
```bash
|
||||
# 建立配置目錄
|
||||
mkdir -p /Users/accusys/momentry/etc/php
|
||||
|
||||
# 建立日誌目錄
|
||||
mkdir -p /Users/accusys/momentry/log
|
||||
|
||||
# 建立日誌文件
|
||||
touch /Users/accusys/momentry/log/php.log
|
||||
touch /Users/accusys/momentry/log/php.error.log
|
||||
|
||||
# 設定權限
|
||||
chown -R accusys:staff /Users/accusys/momentry/etc/php
|
||||
chown -R accusys:staff /Users/accusys/momentry/log
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Step 3: 建立設定檔
|
||||
|
||||
建立 `/Users/accusys/momentry/etc/php/php-fpm.conf`:
|
||||
|
||||
```ini
|
||||
[global]
|
||||
pid = /Users/accusys/momentry/var/php-fpm.pid
|
||||
error_log = /Users/accusys/momentry/log/php.error.log
|
||||
log_level = notice
|
||||
|
||||
[www]
|
||||
user = accusys
|
||||
group = staff
|
||||
listen = 127.0.0.1:9000
|
||||
listen.owner = accusys
|
||||
listen.group = staff
|
||||
pm = dynamic
|
||||
pm.max_children = 5
|
||||
pm.start_servers = 2
|
||||
pm.min_spare_servers = 1
|
||||
pm.max_spare_servers = 3
|
||||
```
|
||||
|
||||
複製 php.ini:
|
||||
```bash
|
||||
cp /opt/homebrew/etc/php/8.5/php.ini /Users/accusys/momentry/etc/php/
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Step 4: 使用 plist 開機自動啟動
|
||||
|
||||
```bash
|
||||
# 複製 plist 到 LaunchDaemons 目錄
|
||||
sudo cp /Users/accusys/momentry_core_0.1/momentry_runtime/plist/com.momentry.php.plist /Library/LaunchDaemons/
|
||||
|
||||
# 載入並啟動
|
||||
sudo launchctl load /Library/LaunchDaemons/com.momentry.php.plist
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 監控配置
|
||||
|
||||
### 添加到監控配置
|
||||
|
||||
在 `monitor/config/monitor_config.yaml` 中添加:
|
||||
|
||||
```yaml
|
||||
service:
|
||||
services:
|
||||
- name: "php-fpm"
|
||||
type: "tcp"
|
||||
port: 9000
|
||||
host: "localhost"
|
||||
timeout: 5
|
||||
enabled: true
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 卸載步驟
|
||||
|
||||
### 重要: 路徑說明
|
||||
|
||||
| 路徑 | 類型 | 說明 |
|
||||
|------|------|------|
|
||||
| `/Users/accusys/momentry/etc/php/` | 配置 | **不要刪除** - PHP 配置 |
|
||||
| `/Users/accusys/momentry/log/` | 日誌 | **不要刪除** - 日誌目錄 |
|
||||
| `/opt/homebrew/opt/php/` | 安裝 | **刪除** - PHP 安裝目錄 |
|
||||
|
||||
### Step 1: 停止 PHP-FPM
|
||||
|
||||
```bash
|
||||
# 找到 PHP-FPM 進程
|
||||
ps aux | grep php-fpm | grep -v grep
|
||||
|
||||
# 停止 PHP-FPM
|
||||
pkill php-fpm
|
||||
|
||||
# 確認停止
|
||||
ps aux | grep php-fpm | grep -v grep || echo "PHP-FPM 已停止"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Step 2: 卸載 PHP
|
||||
|
||||
```bash
|
||||
# 卸載 PHP
|
||||
brew uninstall php
|
||||
|
||||
# 移除 plist
|
||||
sudo launchctl unload /Library/LaunchDaemons/com.momentry.php.plist
|
||||
sudo rm /Library/LaunchDaemons/com.momentry.php.plist
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Step 3: 刪除專屬檔案
|
||||
|
||||
```bash
|
||||
# 刪除配置目錄 (可選)
|
||||
rm -rf /Users/accusys/momentry/etc/php
|
||||
|
||||
# 刪除日誌 (可選)
|
||||
rm -f /Users/accusys/momentry/log/php.log
|
||||
rm -f /Users/accusys/momentry/log/php.error.log
|
||||
```
|
||||
|
||||
**注意: 不要刪除以下共用目錄**:
|
||||
```bash
|
||||
# 這些是共用的,不要刪除!
|
||||
# /Users/accusys/momentry/etc
|
||||
# /Users/accusys/momentry/log
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Step 4: 卸載後檢查清單
|
||||
|
||||
```bash
|
||||
echo "=== PHP 卸載後檢查 ==="
|
||||
|
||||
# 1. 檢查 PHP-FPM 進程
|
||||
echo "1. PHP-FPM 進程:"
|
||||
ps aux | grep php-fpm | grep -v grep && echo " ✗ 仍在運行" || echo " ✓ 已停止"
|
||||
|
||||
# 2. Port 9000
|
||||
echo "2. Port 9000:"
|
||||
lsof -i :9000 > /dev/null 2>&1 && echo " ✗ 仍被佔用" || echo " ✓ 已釋放"
|
||||
|
||||
# 3. php 命令
|
||||
echo "3. php 命令:"
|
||||
which php > /dev/null 2>&1 && echo " ✗ 仍存在" || echo " ✓ 已移除"
|
||||
|
||||
# 4. brew 安裝
|
||||
echo "4. brew 安裝:"
|
||||
brew list php > /dev/null 2>&1 && echo " ✗ 仍存在" || echo " ✓ 已移除"
|
||||
|
||||
# 5. launchctl 服務
|
||||
echo "5. launchctl 服務:"
|
||||
sudo launchctl list | grep php > /dev/null 2>&1 && echo " ✗ 仍存在" || echo " ✓ 已移除"
|
||||
|
||||
# 6. 配置目錄 (可選刪除)
|
||||
echo "6. 配置目錄:"
|
||||
[ -d "/Users/accusys/momentry/etc/php" ] && echo " ✓ 保留" || echo " ✗ 已刪除"
|
||||
|
||||
# 7. 日誌目錄 (可選刪除)
|
||||
echo "7. 日誌目錄:"
|
||||
[ -d "/Users/accusys/momentry/log" ] && echo " ✓ 保留" || echo " ✗ 已刪除"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 手動檢查命令
|
||||
|
||||
```bash
|
||||
# 1. 檢查 PHP 版本
|
||||
php --version
|
||||
|
||||
# 2. 檢查 PHP-FPM 進程
|
||||
ps aux | grep php-fpm | grep -v grep
|
||||
|
||||
# 3. 檢查 Port
|
||||
lsof -i :9000
|
||||
|
||||
# 4. 測試 PHP
|
||||
php -r "echo 'PHP OK' . PHP_EOL;"
|
||||
|
||||
# 5. 查看 PHP 模組
|
||||
php -m
|
||||
|
||||
# 6. 查看 PHP 配置
|
||||
php -i | grep "Loaded Configuration File"
|
||||
|
||||
# 7. 查看日誌
|
||||
tail -20 /Users/accusys/momentry/log/php.log
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 連線資訊
|
||||
|
||||
| 項目 | 值 |
|
||||
|------|-----|
|
||||
| PHP-FPM Port | 9000 |
|
||||
| PHP Version | 8.5.2 |
|
||||
| Config | /Users/accusys/momentry/etc/php/php-fpm.conf |
|
||||
| php.ini | /Users/accusys/momentry/etc/php/php.ini |
|
||||
|
||||
---
|
||||
|
||||
## 環境變數
|
||||
|
||||
在 `.env` 中:
|
||||
|
||||
```env
|
||||
PHP_INI_SCAN_DIR=/Users/accusys/momentry/etc/php/conf.d
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 故障排除
|
||||
|
||||
### PHP-FPM 無法啟動
|
||||
|
||||
```bash
|
||||
# 檢查日誌
|
||||
tail -f /Users/accusys/momentry/log/php.error.log
|
||||
|
||||
# 檢查配置語法
|
||||
/opt/homebrew/opt/php/sbin/php-fpm --test --fpm-config /Users/accusys/momentry/etc/php/php-fpm.conf
|
||||
|
||||
# 檢查目錄權限
|
||||
ls -la /Users/accusys/momentry/etc/php/
|
||||
|
||||
# 重新設定權限
|
||||
chown -R $(whoami):staff /Users/accusys/momentry/etc/php
|
||||
```
|
||||
|
||||
### Port 被佔用
|
||||
|
||||
```bash
|
||||
# 檢查哪個程序佔用 port 9000
|
||||
lsof -i :9000
|
||||
|
||||
# 終止佔用程序
|
||||
kill <PID>
|
||||
```
|
||||
|
||||
### 需要重新載入 plist
|
||||
|
||||
```bash
|
||||
# 卸載舊服務 (如果存在)
|
||||
sudo launchctl unload /Library/LaunchDaemons/com.momentry.php.plist 2>/dev/null
|
||||
|
||||
# 載入新服務
|
||||
sudo launchctl load /Library/LaunchDaemons/com.momentry.php.plist
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 檔案位置
|
||||
|
||||
| 類型 | 路徑 | 說明 |
|
||||
|------|------|------|
|
||||
| 安裝 | `/opt/homebrew/opt/php/` | PHP 安裝目錄 |
|
||||
| 執行檔 | `/opt/homebrew/bin/php` | PHP 執行檔 |
|
||||
| PHP-FPM | `/opt/homebrew/opt/php/sbin/php-fpm` | PHP-FPM 執行檔 |
|
||||
| php.ini | `/Users/accusys/momentry/etc/php/8.5/php.ini` | PHP 配置 |
|
||||
| PHP-FPM 配置 | `/Users/accusys/momentry/etc/php/8.5/php-fpm.conf` | PHP-FPM 主配置 |
|
||||
| PHP-FPM pool | `/Users/accusys/momentry/etc/php/8.5/php-fpm.d/` | Pool 配置 |
|
||||
| 日誌 | `/Users/accusys/momentry/log/php.log` | 執行日誌 |
|
||||
| 錯誤日誌 | `/opt/homebrew/var/log/php-fpm.log` | PHP-FPM 錯誤日誌 |
|
||||
| plist | `/Library/LaunchDaemons/com.momentry.php.plist` | 開機啟動 |
|
||||
| 備份 | `/Users/accusys/momentry/backup/daily/php/` | 配置備份 |
|
||||
|
||||
---
|
||||
|
||||
## 常用指令
|
||||
|
||||
```bash
|
||||
# 測試 PHP-FPM 配置
|
||||
/opt/homebrew/opt/php/sbin/php-fpm --test --fpm-config /Users/accusys/momentry/etc/php/php-fpm.conf
|
||||
|
||||
# 查看 PHP 模組
|
||||
php -m
|
||||
|
||||
# 查看已載入的配置
|
||||
php -i
|
||||
|
||||
# 測試 PHP 腳本
|
||||
php -r "echo 'Hello World' . PHP_EOL;"
|
||||
|
||||
# 查看 PHP-FPM 狀態
|
||||
curl http://127.0.0.1:9000/status
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 備份與恢復
|
||||
|
||||
### 備份
|
||||
|
||||
```bash
|
||||
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
|
||||
BACKUP_DIR="/Users/accusys/momentry/backup/daily/php"
|
||||
|
||||
mkdir -p "$BACKUP_DIR"
|
||||
|
||||
# 備份配置 (注意:PHP-FPM 實際使用 /Users/accusys/momentry/etc/php/8.5/ 配置)
|
||||
tar -czf "$BACKUP_DIR/php_cfg_${TIMESTAMP}.tar.gz" \
|
||||
/Users/accusys/momentry/etc/php/8.5/php.ini \
|
||||
/Users/accusys/momentry/etc/php/8.5/php-fpm.conf \
|
||||
/Users/accusys/momentry/etc/php/8.5/php-fpm.d/
|
||||
|
||||
# 驗證
|
||||
sha256sum "$BACKUP_DIR/php_cfg_${TIMESTAMP}.tar.gz" > "$BACKUP_DIR/php_${TIMESTAMP}.sha256"
|
||||
```
|
||||
|
||||
### 恢復
|
||||
|
||||
```bash
|
||||
# 解壓配置
|
||||
tar -xzf /Users/accusys/momentry/backup/daily/php/php_cfg_20260316_102727.tar.gz -C /
|
||||
|
||||
# 測試配置
|
||||
/opt/homebrew/opt/php/sbin/php-fpm --test --fpm-config /Users/accusys/momentry/etc/php/8.5/php-fpm.conf
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 版本資訊
|
||||
|
||||
- PHP Version: 8.5.2
|
||||
- PHP-FPM Port: 9000
|
||||
- 配置目錄: /Users/accusys/momentry/etc/php/
|
||||
- 日誌目錄: /Users/accusys/momentry/log/
|
||||
@@ -1,397 +0,0 @@
|
||||
# PostgreSQL 安裝指南 (本地部署)
|
||||
|
||||
| 項目 | 內容 |
|
||||
|------|------|
|
||||
| 建立者 | Warren |
|
||||
| 建立時間 | 2026-03-15 |
|
||||
| 文件版本 | V1.0 |
|
||||
|
||||
---
|
||||
|
||||
## 版本歷史
|
||||
|
||||
| 版本 | 日期 | 目的 | 操作人 | 工具/模型 |
|
||||
|------|------|------|--------|-----------|
|
||||
| V1.0 | 2026-03-15 | 創建文件 | Warren | OpenCode / MiniMax M2.5 |
|
||||
|
||||
---
|
||||
|
||||
## 概述
|
||||
|
||||
本文檔說明如何在 macOS 上安裝 PostgreSQL,配置為本地部署,支援遠端訪問。
|
||||
|
||||
---
|
||||
|
||||
## 當前狀態
|
||||
|
||||
| 項目 | 狀態 |
|
||||
|------|------|
|
||||
| PostgreSQL | ✅ 已安裝 v18.1 |
|
||||
| 數據目錄 | /Users/accusys/momentry/var/postgresql |
|
||||
| 日誌目錄 | /Users/accusys/momentry/log/ |
|
||||
| Plist | /Library/LaunchDaemons/com.momentry.postgresql.plist |
|
||||
| Launchd 狀態 | ✅ 已註冊 |
|
||||
| RunAtLoad | ✅ 已設定 |
|
||||
| KeepAlive | ✅ 已設定 |
|
||||
|
||||
### 重要更新 (2026-03-24)
|
||||
|
||||
1. **資料目錄已變更**: 從 `/opt/homebrew/var/postgresql@18` 遷移到 `/Users/accusys/momentry/var/postgresql`
|
||||
2. **統一管理**: 所有 Momentry 服務現在都使用 `/Library/LaunchDaemons/` 下的自定義 plist
|
||||
3. **避免衝突**: 刪除了 homebrew plist,避免 reboot 後使用舊資料目錄
|
||||
|
||||
---
|
||||
|
||||
## 安裝步驟
|
||||
|
||||
### Step 1: 安裝 PostgreSQL (使用 brew)
|
||||
|
||||
```bash
|
||||
# 安裝 PostgreSQL
|
||||
brew install postgresql@18
|
||||
```
|
||||
|
||||
**驗證**:
|
||||
```bash
|
||||
postgres --version
|
||||
# postgres (PostgreSQL) 18.1
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Step 2: 建立目錄結構
|
||||
|
||||
```bash
|
||||
# 建立數據目錄
|
||||
mkdir -p /Users/accusys/momentry/var/postgresql
|
||||
|
||||
# 建立配置目錄
|
||||
mkdir -p /Users/accusys/momentry/etc/postgresql
|
||||
|
||||
# 建立日誌目錄
|
||||
mkdir -p /Users/accusys/momentry/log
|
||||
|
||||
# 建立日誌文件
|
||||
touch /Users/accusys/momentry/log/postgresql.log
|
||||
touch /Users/accusys/momentry/log/postgresql.error.log
|
||||
|
||||
# 設定權限
|
||||
chown -R accusys:staff /Users/accusys/momentry/var/postgresql
|
||||
chown -R accusys:staff /Users/accusys/momentry/etc/postgresql
|
||||
chown -R accusys:staff /Users/accusys/momentry/log
|
||||
```
|
||||
|
||||
**注意**: 如果需要從舊數據遷移,需要先初始化新目錄:
|
||||
```bash
|
||||
# 初始化新數據目錄 (會創建默認數據庫)
|
||||
initdb -D /Users/accusys/momentry/var/postgresql -U accusys
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Step 3: 使用 plist 開機自動啟動
|
||||
|
||||
```bash
|
||||
# 複製 plist 到 LaunchDaemons 目錄
|
||||
sudo cp /Users/accusys/momentry_core_0.1/momentry_runtime/plist/com.momentry.postgresql.plist /Library/LaunchDaemons/
|
||||
|
||||
# 載入並啟動
|
||||
sudo launchctl load /Library/LaunchDaemons/com.momentry.postgresql.plist
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 監控配置
|
||||
|
||||
### 添加到監控配置
|
||||
|
||||
在 `monitor/config/monitor_config.yaml` 中添加:
|
||||
|
||||
```yaml
|
||||
database:
|
||||
postgresql:
|
||||
enabled: true
|
||||
host: "localhost"
|
||||
port: 5432
|
||||
user: "accusys"
|
||||
database: "momentry"
|
||||
```
|
||||
|
||||
### 添加健康檢查函數
|
||||
|
||||
在 `monitor/database/postgres_monitor.sh` 中已包含 PostgreSQL 監控。
|
||||
|
||||
---
|
||||
|
||||
## 卸載步驟
|
||||
|
||||
### 重要: 路徑說明
|
||||
|
||||
| 路徑 | 類型 | 說明 |
|
||||
|------|------|------|
|
||||
| `/Users/accusys/momentry/var/postgresql/` | 數據 | **不要刪除** - 數據目錄 |
|
||||
| `/Users/accusys/momentry/etc/postgresql/` | 配置 | **不要刪除** - 配置目錄 |
|
||||
| `/Users/accusys/momentry/log/` | 日誌 | **不要刪除** - 日誌目錄 |
|
||||
| `/opt/homebrew/opt/postgresql@18/` | 安裝 | **刪除** - PostgreSQL 安裝目錄 |
|
||||
|
||||
### Step 1: 停止 PostgreSQL
|
||||
|
||||
```bash
|
||||
# 找到 PostgreSQL 進程
|
||||
ps aux | grep postgres | grep -v grep
|
||||
|
||||
# 停止 PostgreSQL
|
||||
pg_ctl -D /opt/homebrew/var/postgresql@18 stop
|
||||
# 或
|
||||
pkill -f postgresql
|
||||
|
||||
# 確認停止
|
||||
ps aux | grep postgres | grep -v grep || echo "PostgreSQL 已停止"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Step 2: 卸載 PostgreSQL
|
||||
|
||||
```bash
|
||||
# 卸載 PostgreSQL
|
||||
brew uninstall postgresql@18
|
||||
|
||||
# 移除 plist
|
||||
sudo launchctl unload /Library/LaunchDaemons/com.momentry.postgresql.plist
|
||||
sudo rm /Library/LaunchDaemons/com.momentry.postgresql.plist
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Step 3: 刪除專屬檔案
|
||||
|
||||
```bash
|
||||
# 刪除數據目錄 (可選)
|
||||
rm -rf /Users/accusys/momentry/var/postgresql
|
||||
|
||||
# 刪除日誌 (可選)
|
||||
rm -f /Users/accusys/momentry/log/postgresql.log
|
||||
rm -f /Users/accusys/momentry/log/postgresql.error.log
|
||||
```
|
||||
|
||||
**注意: 不要刪除以下共用目錄**:
|
||||
```bash
|
||||
# 這些是共用的,不要刪除!
|
||||
# /Users/accusys/momentry/var
|
||||
# /Users/accusys/momentry/log
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Step 4: 卸載後檢查清單
|
||||
|
||||
```bash
|
||||
echo "=== PostgreSQL 卸載後檢查 ==="
|
||||
|
||||
# 1. 檢查 PostgreSQL 進程
|
||||
echo "1. PostgreSQL 進程:"
|
||||
ps aux | grep postgres | grep -v grep && echo " ✗ 仍在運行" || echo " ✓ 已停止"
|
||||
|
||||
# 2. Port 5432
|
||||
echo "2. Port 5432:"
|
||||
lsof -i :5432 > /dev/null 2>&1 && echo " ✗ 仍被佔用" || echo " ✓ 已釋放"
|
||||
|
||||
# 3. postgres 命令
|
||||
echo "3. postgres 命令:"
|
||||
which postgres > /dev/null 2>&1 && echo " ✗ 仍存在" || echo " ✓ 已移除"
|
||||
|
||||
# 4. brew 安裝
|
||||
echo "4. brew 安裝:"
|
||||
brew list postgresql@18 > /dev/null 2>&1 && echo " ✗ 仍存在" || echo " ✓ 已移除"
|
||||
|
||||
# 5. launchctl 服務
|
||||
echo "5. launchctl 服務:"
|
||||
sudo launchctl list | grep postgresql > /dev/null 2>&1 && echo " ✗ 仍存在" || echo " ✓ 已移除"
|
||||
|
||||
# 6. 數據目錄 (可選刪除)
|
||||
echo "6. 數據目錄:"
|
||||
[ -d "/Users/accusys/momentry/var/postgresql" ] && echo " ✓ 保留" || echo " ✗ 已刪除"
|
||||
|
||||
# 7. 日誌目錄 (可選刪除)
|
||||
echo "7. 日誌目錄:"
|
||||
[ -d "/Users/accusys/momentry/log" ] && echo " ✓ 保留" || echo " ✗ 已刪除"
|
||||
```
|
||||
|
||||
**預期結果**:
|
||||
```
|
||||
=== PostgreSQL 卸載後檢查 ===
|
||||
1. PostgreSQL 進程:
|
||||
✓ 已停止
|
||||
2. Port 5432:
|
||||
✓ 已釋放
|
||||
3. postgres 命令:
|
||||
✓ 已移除
|
||||
4. brew 安裝:
|
||||
✓ 已移除
|
||||
5. launchctl 服務:
|
||||
✓ 已移除
|
||||
6. 數據目錄:
|
||||
✓ 保留 (或 ✗ 已刪除)
|
||||
7. 日誌目錄:
|
||||
✓ 保留 (或 ✗ 已刪除)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 手動檢查命令
|
||||
|
||||
```bash
|
||||
# 1. 檢查進程
|
||||
ps aux | grep postgres | grep -v grep
|
||||
|
||||
# 2. 檢查 Port
|
||||
lsof -i :5432
|
||||
|
||||
# 3. 測試連線
|
||||
psql -U accusys -l
|
||||
|
||||
# 4. 查看所有數據庫
|
||||
psql -U accusys -c "\l"
|
||||
|
||||
# 5. 查看連接
|
||||
psql -U accusys -c "\conninfo"
|
||||
|
||||
# 6. 查看表
|
||||
psql -U accusys -d momentry -c "\dt"
|
||||
|
||||
# 7. 查看日誌
|
||||
tail -20 /Users/accusys/momentry/log/postgresql.log
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 連線資訊
|
||||
|
||||
| 項目 | 值 |
|
||||
|------|-----|
|
||||
| Host | localhost |
|
||||
| Port | 5432 |
|
||||
| User | accusys |
|
||||
| Database | momentry, video_register, gitea, n8n |
|
||||
|
||||
---
|
||||
|
||||
## 環境變數
|
||||
|
||||
在 `.env` 中:
|
||||
|
||||
```env
|
||||
POSTGRES_URL=postgresql://accusys@localhost:5432
|
||||
POSTGRES_DB=momentry
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 遠端訪問
|
||||
|
||||
- PostgreSQL 綁定到所有網路介面 (0.0.0.0)
|
||||
- 本地網路其他機器可透過 IP 訪問
|
||||
- 請設定 `pg_hba.conf` 限制訪問 IP
|
||||
|
||||
---
|
||||
|
||||
## 故障排除
|
||||
|
||||
### PostgreSQL 無法啟動
|
||||
|
||||
```bash
|
||||
# 檢查日誌
|
||||
tail -f /Users/accusys/momentry/log/postgresql.log
|
||||
|
||||
# 檢查目錄權限
|
||||
ls -la /Users/accusys/momentry/var/postgresql/
|
||||
|
||||
# 重新設定權限
|
||||
chown -R $(whoami):staff /Users/accusys/momentry/var/postgresql
|
||||
```
|
||||
|
||||
### Port 被佔用
|
||||
|
||||
```bash
|
||||
# 檢查哪個程序佔用 port 5432
|
||||
lsof -i :5432
|
||||
|
||||
# 終止佔用程序
|
||||
kill <PID>
|
||||
```
|
||||
|
||||
### 需要重新載入 plist
|
||||
|
||||
```bash
|
||||
# 卸載舊服務 (如果存在)
|
||||
sudo launchctl unload /Library/LaunchDaemons/com.momentry.postgresql.plist 2>/dev/null
|
||||
|
||||
# 載入新服務
|
||||
sudo launchctl load /Library/LaunchDaemons/com.momentry.postgresql.plist
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 檔案位置
|
||||
|
||||
| 類型 | 路徑 | 說明 |
|
||||
|------|------|------|
|
||||
| 安裝 | `/opt/homebrew/opt/postgresql@18/` | PostgreSQL 安裝目錄 |
|
||||
| 執行檔 | `/opt/homebrew/opt/postgresql@18/bin/postgres` | PostgreSQL 執行檔 |
|
||||
| 數據目錄 | `/Users/accusys/momentry/var/postgresql/` | 數據儲存 |
|
||||
| 日誌 | `/Users/accusys/momentry/log/postgresql.log` | 執行日誌 |
|
||||
| 錯誤日誌 | `/Users/accusys/momentry/log/postgresql.error.log` | 錯誤日誌 |
|
||||
| plist | `/Library/LaunchDaemons/com.momentry.postgresql.plist` | 開機啟動 |
|
||||
| 備份 | `/Users/accusys/momentry/var/momentry_db_backup_latest.sql` | momentry 數據庫備份 |
|
||||
| 備份 | `/Users/accusys/momentry/var/video_register_db_backup_latest.sql` | video_register 數據庫備份 |
|
||||
|
||||
---
|
||||
|
||||
## 備份與恢復
|
||||
|
||||
### 備份 (pg_dump)
|
||||
|
||||
```bash
|
||||
# 備份 momentry 數據庫
|
||||
pg_dump -U accusys momentry > /Users/accusys/momentry/var/momentry_db_backup_latest.sql
|
||||
|
||||
# 備份 video_register 數據庫
|
||||
pg_dump -U accusys video_register > /Users/accusys/momentry/var/video_register_db_backup_latest.sql
|
||||
```
|
||||
|
||||
### 恢復 (psql)
|
||||
|
||||
```bash
|
||||
# 恢復 momentry 數據庫
|
||||
psql -U accusys -d momentry < /Users/accusys/momentry/var/momentry_db_backup_latest.sql
|
||||
|
||||
# 恢復 video_register 數據庫
|
||||
psql -U accusys -d video_register < /Users/accusys/momentry/var/video_register_db_backup_latest.sql
|
||||
```
|
||||
|
||||
### 數據目錄複製 (完整遷移)
|
||||
|
||||
```bash
|
||||
# 1. 停止 PostgreSQL
|
||||
pg_ctl -D /Users/accusys/momentry/var/postgresql stop
|
||||
|
||||
# 2. 複製數據目錄
|
||||
cp -r /opt/homebrew/var/postgresql@18/* /Users/accusys/momentry/var/postgresql/
|
||||
|
||||
# 3. 設定權限
|
||||
chown -R $(whoami):staff /Users/accusys/momentry/var/postgresql
|
||||
|
||||
# 4. 啟動 PostgreSQL
|
||||
sudo launchctl load /Library/LaunchDaemons/com.momentry.postgresql.plist
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 版本資訊
|
||||
|
||||
- 版本: 18.1
|
||||
- Port: 5432
|
||||
- User: accusys
|
||||
- 數據目錄: /Users/accusys/momentry/var/postgresql/
|
||||
- 日誌目錄: /Users/accusys/momentry/log/
|
||||
@@ -1,472 +0,0 @@
|
||||
# Qdrant 安裝指南 (本地部署)
|
||||
|
||||
| 項目 | 內容 |
|
||||
|------|------|
|
||||
| 建立者 | Warren |
|
||||
| 建立時間 | 2026-03-16 |
|
||||
| 文件版本 | V1.0 |
|
||||
|
||||
---
|
||||
|
||||
## 版本歷史
|
||||
|
||||
| 版本 | 日期 | 目的 | 操作人 | 工具/模型 |
|
||||
|------|------|------|--------|-----------|
|
||||
| V1.0 | 2026-03-16 | 創建文件 | Warren | OpenCode / MiniMax M2.5 |
|
||||
|
||||
---
|
||||
|
||||
## 概述
|
||||
|
||||
本文檔說明如何在 macOS 上安裝 Qdrant Vector Database,配置為本地部署,支援遠端訪問。
|
||||
|
||||
---
|
||||
|
||||
## 當前狀態
|
||||
|
||||
| 項目 | 狀態 |
|
||||
|------|------|
|
||||
| Qdrant | ✅ 已安裝 v1.17.0 |
|
||||
| 數據目錄 | /Users/accusys/momentry/var/qdrant/ |
|
||||
| 日誌目錄 | /Users/accusys/momentry/log/ |
|
||||
| Plist | /Library/LaunchDaemons/com.momentry.qdrant.plist |
|
||||
|
||||
---
|
||||
|
||||
## 安裝步驟
|
||||
|
||||
### Step 1: 安裝 Qdrant (使用 cargo)
|
||||
|
||||
```bash
|
||||
# 安裝 Qdrant 從 GitHub
|
||||
cargo install --git https://github.com/qdrant/qdrant.git --locked
|
||||
```
|
||||
|
||||
**驗證**:
|
||||
```bash
|
||||
qdrant --version
|
||||
# qdrant 1.17.0
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Step 2: 驗證 Qdrant 安裝
|
||||
|
||||
```bash
|
||||
# 驗證 Qdrant 安裝
|
||||
~/.cargo/bin/qdrant --version
|
||||
# qdrant 1.17.0
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Step 3: 建立目錄結構
|
||||
|
||||
```bash
|
||||
# 建立數據目錄
|
||||
mkdir -p /Users/accusys/momentry/var/qdrant
|
||||
|
||||
# 建立配置目錄
|
||||
mkdir -p /Users/accusys/momentry/etc/qdrant
|
||||
|
||||
# 建立日誌目錄
|
||||
mkdir -p /Users/accusys/momentry/log
|
||||
|
||||
# 建立日誌文件
|
||||
touch /Users/accusys/momentry/log/qdrant.log
|
||||
touch /Users/accusys/momentry/log/qdrant.error.log
|
||||
|
||||
# 設定權限
|
||||
chown -R accusys:staff /Users/accusys/momentry/var/qdrant
|
||||
chown -R accusys:staff /Users/accusys/momentry/etc/qdrant
|
||||
chown -R accusys:staff /Users/accusys/momentry/log
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Step 4: 使用 plist 開機自動啟動
|
||||
|
||||
```bash
|
||||
# 複製 plist 到 LaunchDaemons 目錄
|
||||
sudo cp /Users/accusys/momentry_core_0.1/momentry_runtime/plist/com.momentry.qdrant.plist /Library/LaunchDaemons/
|
||||
|
||||
# 載入並啟動
|
||||
sudo launchctl load /Library/LaunchDaemons/com.momentry.qdrant.plist
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 監控配置
|
||||
|
||||
### 添加到監控配置
|
||||
|
||||
在 `monitor/config/monitor_config.yaml` 中添加:
|
||||
|
||||
```yaml
|
||||
database:
|
||||
qdrant:
|
||||
enabled: true
|
||||
host: "localhost"
|
||||
port: 6333
|
||||
```
|
||||
|
||||
### 添加健康檢查函數
|
||||
|
||||
在 `monitor/database/qdrant_monitor.sh` 中已包含 Qdrant 監控。
|
||||
|
||||
---
|
||||
|
||||
## 卸載步驟
|
||||
|
||||
### 重要: 路徑說明
|
||||
|
||||
| 路徑 | 類型 | 說明 |
|
||||
|------|------|------|
|
||||
| `/Users/accusys/momentry/var/qdrant/` | 數據 | **不要刪除** - Qdrant 數據 |
|
||||
| `/Users/accusys/momentry/etc/qdrant/` | 配置 | **不要刪除** - Qdrant 配置 |
|
||||
| `/Users/accusys/momentry/log/` | 日誌 | **不要刪除** - 日誌目錄 |
|
||||
| `~/.cargo/bin/qdrant` | 安裝 | **刪除** - Qdrant 執行檔 |
|
||||
|
||||
### Step 1: 停止 Qdrant
|
||||
|
||||
```bash
|
||||
# 找到 Qdrant 進程
|
||||
ps aux | grep qdrant | grep -v grep
|
||||
|
||||
# 停止 Qdrant
|
||||
pkill qdrant
|
||||
# 或
|
||||
kill <PID>
|
||||
|
||||
# 確認停止
|
||||
ps aux | grep qdrant | grep -v grep || echo "Qdrant 已停止"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Step 2: 卸載 Qdrant (cargo)
|
||||
|
||||
```bash
|
||||
# 移除 cargo 安裝的 Qdrant
|
||||
cargo uninstall qdrant
|
||||
|
||||
# 移除 plist
|
||||
sudo launchctl unload /Library/LaunchDaemons/com.momentry.qdrant.plist
|
||||
sudo rm /Library/LaunchDaemons/com.momentry.qdrant.plist
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Step 3: 刪除專屬檔案
|
||||
|
||||
```bash
|
||||
# 刪除數據目錄 (可選)
|
||||
rm -rf /Users/accusys/momentry/var/qdrant
|
||||
|
||||
# 刪除日誌 (可選)
|
||||
rm -f /Users/accusys/momentry/log/qdrant.log
|
||||
rm -f /opt/homebrew/var/log/qdrant.error.log
|
||||
```
|
||||
|
||||
**注意: 不要刪除以下共用目錄**:
|
||||
```bash
|
||||
# 這些是共用的,不要刪除!
|
||||
# /Users/accusys/momentry/var
|
||||
# /Users/accusys/momentry/log
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Step 4: 卸載後檢查清單
|
||||
|
||||
```bash
|
||||
echo "=== Qdrant 卸載後檢查 ==="
|
||||
|
||||
# 1. 檢查 Qdrant 進程
|
||||
echo "1. Qdrant 進程:"
|
||||
ps aux | grep qdrant | grep -v grep && echo " ✗ 仍在運行" || echo " ✓ 已停止"
|
||||
|
||||
# 2. Port 6333
|
||||
echo "2. Port 6333:"
|
||||
lsof -i :6333 > /dev/null 2>&1 && echo " ✗ 仍被佔用" || echo " ✓ 已釋放"
|
||||
|
||||
# 3. Port 6334
|
||||
echo "3. Port 6334:"
|
||||
lsof -i :6334 > /dev/null 2>&1 && echo " ✗ 仍被佔用" || echo " ✓ 已釋放"
|
||||
|
||||
# 4. qdrant 命令
|
||||
echo "4. qdrant 命令:"
|
||||
which qdrant > /dev/null 2>&1 && echo " ✗ 仍存在" || echo " ✓ 已移除"
|
||||
|
||||
# 5. cargo 安裝
|
||||
echo "5. cargo 安裝:"
|
||||
cargo install --list | grep qdrant > /dev/null 2>&1 && echo " ✗ 仍存在" || echo " ✓ 已移除"
|
||||
|
||||
# 6. launchctl 服務
|
||||
echo "6. launchctl 服務:"
|
||||
sudo launchctl list | grep qdrant > /dev/null 2>&1 && echo " ✗ 仍存在" || echo " ✓ 已移除"
|
||||
|
||||
# 7. 數據目錄 (可選刪除)
|
||||
echo "7. 數據目錄:"
|
||||
[ -d "/Users/accusys/momentry/var/qdrant" ] && echo " ✓ 保留" || echo " ✗ 已刪除"
|
||||
|
||||
# 8. 日誌目錄 (可選刪除)
|
||||
echo "8. 日誌目錄:"
|
||||
[ -d "/Users/accusys/momentry/log" ] && echo " ✓ 保留" || echo " ✗ 已刪除"
|
||||
```
|
||||
|
||||
**預期結果**:
|
||||
```
|
||||
=== Qdrant 卸載後檢查 ===
|
||||
1. Qdrant 進程:
|
||||
✓ 已停止
|
||||
2. Port 6333:
|
||||
✓ 已釋放
|
||||
3. Port 6334:
|
||||
✓ 已釋放
|
||||
4. qdrant 命令:
|
||||
✓ 已移除
|
||||
5. cargo 安裝:
|
||||
✓ 已移除
|
||||
6. launchctl 服務:
|
||||
✓ 已移除
|
||||
7. 數據目錄:
|
||||
✓ 保留 (或 ✗ 已刪除)
|
||||
8. 日誌目錄:
|
||||
✓ 保留 (或 ✗ 已刪除)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 手動檢查命令
|
||||
|
||||
```bash
|
||||
# 1. 檢查進程
|
||||
ps aux | grep qdrant | grep -v grep
|
||||
|
||||
# 2. 檢查 Port
|
||||
lsof -i :6333
|
||||
lsof -i :6334
|
||||
|
||||
# 3. 測試連線 (無認證)
|
||||
curl http://localhost:6333/collections
|
||||
|
||||
# 4. 測試連線 (有認證)
|
||||
curl -H "api-key: Test3200Test3200Test3200" http://localhost:6333/collections
|
||||
|
||||
# 5. 查看所有 collections
|
||||
curl -s -H "api-key: Test3200Test3200Test3200" http://localhost:6333/collections
|
||||
|
||||
# 6. 查看日誌
|
||||
tail -20 /Users/accusys/momentry/log/qdrant.log
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 連線資訊
|
||||
|
||||
| 項目 | 值 |
|
||||
|------|-----|
|
||||
| REST API | http://localhost:6333 |
|
||||
| gRPC | localhost:6334 |
|
||||
| API Key | Test3200Test3200Test3200 |
|
||||
| Qdrant UI | http://localhost:6333/dashboard |
|
||||
|
||||
---
|
||||
|
||||
## 環境變數
|
||||
|
||||
在 `.env` 中:
|
||||
|
||||
```env
|
||||
QDRANT_URL=http://localhost:6333
|
||||
QDRANT_API_KEY=Test3200Test3200Test3200
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 遠端訪問
|
||||
|
||||
- Qdrant 綁定到 `0.0.0.0` (所有網路介面)
|
||||
- 本地網路其他機器可透過 IP 訪問
|
||||
- 建議設定防火牆規則限制訪問 IP
|
||||
|
||||
---
|
||||
|
||||
## 故障排除
|
||||
|
||||
### Qdrant 無法啟動
|
||||
|
||||
```bash
|
||||
# 檢查日誌
|
||||
tail -f /Users/accusys/momentry/log/qdrant.log
|
||||
|
||||
# 檢查目錄權限
|
||||
ls -la /Users/accusys/momentry/var/qdrant/
|
||||
|
||||
# 重新設定權限
|
||||
chown -R $(whoami):staff /Users/accusys/momentry/var/qdrant
|
||||
```
|
||||
|
||||
### Port 被佔用
|
||||
|
||||
```bash
|
||||
# 檢查哪個程序佔用 port 6333
|
||||
lsof -i :6333
|
||||
|
||||
# 終止佔用程序
|
||||
kill <PID>
|
||||
```
|
||||
|
||||
### 需要重新載入 plist
|
||||
|
||||
```bash
|
||||
# 卸載舊服務 (如果存在)
|
||||
sudo launchctl unload /Library/LaunchDaemons/com.momentry.qdrant.plist 2>/dev/null
|
||||
|
||||
# 載入新服務
|
||||
sudo launchctl load /Library/LaunchDaemons/com.momentry.qdrant.plist
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 檔案位置
|
||||
|
||||
| 類型 | 路徑 | 說明 |
|
||||
|------|------|------|
|
||||
| `/Users/accusys/.cargo/bin/qdrant` | 二進制 | cargo 安裝位置 (直接使用) |
|
||||
| `/opt/homebrew/bin/qdrant` | 符號連結 | ~~已棄用~~ - 不再需要 |
|
||||
| 數據目錄 | `/Users/accusys/momentry/var/qdrant/` | 數據儲存 |
|
||||
| 日誌 | `/Users/accusys/momentry/log/qdrant.log` | 執行日誌 |
|
||||
| 錯誤日誌 | `/opt/homebrew/var/log/qdrant.error.log` | 錯誤日誌 |
|
||||
| plist | `/Library/LaunchDaemons/com.momentry.qdrant.plist` | 開機啟動 |
|
||||
| 備份 | `/Users/accusys/momentry/var/qdrant_backup/` | 數據備份 |
|
||||
|
||||
---
|
||||
|
||||
## 備份與恢復
|
||||
|
||||
### 備份
|
||||
|
||||
Qdrant 提供兩種備份方式:Snapshots (推薦) 和手動複製。
|
||||
|
||||
#### 方式一:使用 Snapshots API (推薦)
|
||||
|
||||
```bash
|
||||
# 創建備份目錄
|
||||
mkdir -p /Users/accusys/momentry/var/qdrant_backup
|
||||
|
||||
# 獲取所有 collections
|
||||
curl -s -H "api-key: Test3200Test3200Test3200" \
|
||||
http://localhost:6333/collections | jq -r '.result[].name'
|
||||
|
||||
# 為每個 collection 創建 snapshot
|
||||
# 假設 collection 名稱為 "chunks"
|
||||
curl -X POST -H "api-key: Test3200Test3200Test3200" \
|
||||
http://localhost:6333/collections/chunks/snapshots \
|
||||
-o /Users/accusys/momentry/var/qdrant_backup/chunks_snapshot_$(date +%Y%m%d).tar.gz
|
||||
```
|
||||
|
||||
#### 方式二:手動複製數據目錄 (停機備份)
|
||||
|
||||
```bash
|
||||
# 停止 Qdrant
|
||||
pkill qdrant
|
||||
|
||||
# 等待停止
|
||||
sleep 2
|
||||
|
||||
# 複製數據目錄
|
||||
TIMESTAMP=$(date +%Y%m%d)
|
||||
mkdir -p /Users/accusys/momentry/var/qdrant_backup
|
||||
tar -czf /Users/accusys/momentry/var/qdrant_backup/qdrant_data_${TIMESTAMP}.tar.gz \
|
||||
-C /Users/accusys/momentry/var qdrant/
|
||||
|
||||
# 啟動 Qdrant
|
||||
launchctl load /Library/LaunchDaemons/com.momentry.qdrant.plist
|
||||
```
|
||||
|
||||
#### 自動備份腳本
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# backup_qdrant.sh
|
||||
set -e
|
||||
|
||||
QDRANT_HOST="localhost"
|
||||
QDRANT_PORT="6333"
|
||||
QDRANT_API_KEY="Test3200Test3200Test3200"
|
||||
BACKUP_DIR="/Users/accusys/momentry/var/qdrant_backup"
|
||||
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
|
||||
|
||||
mkdir -p "$BACKUP_DIR"
|
||||
|
||||
echo "開始 Qdrant 備份..."
|
||||
|
||||
# 獲取所有 collections
|
||||
COLLECTIONS=$(curl -s -H "api-key: $QDRANT_API_KEY" \
|
||||
http://${QDRANT_HOST}:${QDRANT_PORT}/collections | \
|
||||
jq -r '.result[].name')
|
||||
|
||||
if [ -z "$COLLECTIONS" ]; then
|
||||
echo "警告: 沒有找到任何 collections"
|
||||
else
|
||||
for COLLECTION in $COLLECTIONS; do
|
||||
echo "備份 collection: $COLLECTION"
|
||||
curl -X POST -H "api-key: $QDRANT_API_KEY" \
|
||||
"http://${QDRANT_HOST}:${QDRANT_PORT}/collections/${COLLECTION}/snapshots" \
|
||||
-o "${BACKUP_DIR}/${COLLECTION}_${TIMESTAMP}.tar.gz" 2>/dev/null || \
|
||||
echo "警告: $COLLECTION 備份失敗"
|
||||
done
|
||||
fi
|
||||
|
||||
# 壓縮所有 snapshot
|
||||
cd "$BACKUP_DIR"
|
||||
tar -czf qdrant_snapshots_${TIMESTAMP}.tar.gz *.tar.gz 2>/dev/null || true
|
||||
rm -f *.tar.gz
|
||||
|
||||
# 清理 30 天前的備份
|
||||
find "$BACKUP_DIR" -name "qdrant_snapshots_*.tar.gz" -mtime +30 -delete
|
||||
|
||||
echo "Qdrant 備份完成: ${BACKUP_DIR}/qdrant_snapshots_${TIMESTAMP}.tar.gz"
|
||||
```
|
||||
|
||||
### 恢復
|
||||
|
||||
```bash
|
||||
# 停止 Qdrant
|
||||
pkill qdrant
|
||||
sleep 2
|
||||
|
||||
# 解壓縮備份
|
||||
TIMESTAMP="20260315"
|
||||
tar -xzf /Users/accusys/momentry/var/qdrant_backup/qdrant_snapshots_${TIMESTAMP}.tar.gz \
|
||||
-C /Users/accusys/momentry/var/qdrant_backup/
|
||||
|
||||
# 恢復數據目錄 (方式二備份)
|
||||
# rm -rf /Users/accusys/momentry/var/qdrant/*
|
||||
# tar -xzf /Users/accusys/momentry/var/qdrant_backup/qdrant_data_${TIMESTAMP}.tar.gz \
|
||||
# -C /Users/accusys/momentry/var/
|
||||
|
||||
# 啟動 Qdrant
|
||||
launchctl load /Library/LaunchDaemons/com.momentry.qdrant.plist
|
||||
```
|
||||
|
||||
### 排程備份
|
||||
|
||||
```bash
|
||||
# 編輯 crontab
|
||||
crontab -e
|
||||
|
||||
# 添加每天凌晨 3 點執行備份
|
||||
0 3 * * * /Users/accusys/momentry/scripts/backup_qdrant.sh >> /Users/accusys/momentry/log/backup.log 2>&1
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 版本資訊
|
||||
|
||||
- 版本: 1.17.0
|
||||
- API Key: Test3200Test3200Test3200
|
||||
- 數據目錄: /Users/accusys/momentry/var/qdrant/
|
||||
- 日誌目錄: /Users/accusys/momentry/log/
|
||||
@@ -1,481 +0,0 @@
|
||||
# Redis 安裝指南 (本地部署)
|
||||
|
||||
| 項目 | 內容 |
|
||||
|------|------|
|
||||
| 建立者 | Warren |
|
||||
| 建立時間 | 2026-03-15 |
|
||||
| 文件版本 | V1.0 |
|
||||
|
||||
---
|
||||
|
||||
## 版本歷史
|
||||
|
||||
| 版本 | 日期 | 目的 | 操作人 | 工具/模型 |
|
||||
|------|------|------|--------|-----------|
|
||||
| V1.0 | 2026-03-15 | 創建文件 | Warren | OpenCode / MiniMax M2.5 |
|
||||
| V1.1 | 2026-03-21 | 更新 rust redis crate 版本至 0.32.7 | OpenCode | - |
|
||||
| V1.2 | 2026-03-21 | 添加 Redis 用戶配置說明 | OpenCode | - |
|
||||
|
||||
---
|
||||
|
||||
## 概述
|
||||
|
||||
本文檔說明如何在 macOS 上安裝 Redis,配置為本地部署,支援遠端訪問。
|
||||
|
||||
---
|
||||
|
||||
## 當前狀態
|
||||
|
||||
| 項目 | 狀態 |
|
||||
|------|------|
|
||||
| Redis | ✅ 已安裝 v8.4.0 |
|
||||
| 數據目錄 | /opt/homebrew/var/db/redis/ |
|
||||
| 日誌目錄 | /Users/accusys/momentry/log/ |
|
||||
| Plist | /Library/LaunchDaemons/com.momentry.redis.plist |
|
||||
|
||||
---
|
||||
|
||||
## 安裝步驟
|
||||
|
||||
### Step 1: 安裝 Redis (使用 brew)
|
||||
|
||||
```bash
|
||||
# 安裝 Redis
|
||||
brew install redis
|
||||
```
|
||||
|
||||
**驗證**:
|
||||
```bash
|
||||
redis-server --version
|
||||
# Redis server v8.4.0
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Step 2: 建立目錄結構
|
||||
|
||||
```bash
|
||||
# 建立數據目錄
|
||||
mkdir -p /Users/accusys/momentry/var/redis
|
||||
|
||||
# 建立配置目錄
|
||||
mkdir -p /Users/accusys/momentry/etc/redis
|
||||
|
||||
# 建立日誌目錄
|
||||
mkdir -p /Users/accusys/momentry/log
|
||||
|
||||
# 建立日誌文件
|
||||
touch /Users/accusys/momentry/log/redis.log
|
||||
touch /Users/accusys/momentry/log/redis.error.log
|
||||
|
||||
# 設定權限
|
||||
chown -R accusys:staff /Users/accusys/momentry/var/redis
|
||||
chown -R accusys:staff /Users/accusys/momentry/etc/redis
|
||||
chown -R accusys:staff /Users/accusys/momentry/log
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Step 3: 使用 plist 開機自動啟動
|
||||
|
||||
```bash
|
||||
# 複製 plist 到 LaunchDaemons 目錄
|
||||
sudo cp /Users/accusys/momentry_core_0.1/momentry_runtime/plist/com.momentry.redis.plist /Library/LaunchDaemons/
|
||||
|
||||
# 載入並啟動
|
||||
sudo launchctl load /Library/LaunchDaemons/com.momentry.redis.plist
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 卸載步驟
|
||||
|
||||
### 重要: 路徑說明
|
||||
|
||||
| 路徑 | 類型 | 說明 |
|
||||
|------|------|------|
|
||||
| `/Users/accusys/momentry/var/redis/` | 數據 | **不要刪除** - 數據目錄 |
|
||||
| `/Users/accusys/momentry/etc/redis/` | 配置 | **不要刪除** - 配置目錄 |
|
||||
| `/Users/accusys/momentry/log/` | 日誌 | **不要刪除** - 日誌目錄 |
|
||||
| `/opt/homebrew/opt/redis/` | 安裝 | **刪除** - Redis 安裝目錄 |
|
||||
|
||||
### Step 1: 停止 Redis
|
||||
|
||||
```bash
|
||||
# 找到 Redis 進程
|
||||
ps aux | grep redis | grep -v grep
|
||||
|
||||
# 停止 Redis
|
||||
redis-cli -a accusys SHUTDOWN
|
||||
# 或
|
||||
pkill redis-server
|
||||
|
||||
# 確認停止
|
||||
ps aux | grep redis | grep -v grep || echo "Redis 已停止"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Step 2: 卸載 Redis
|
||||
|
||||
```bash
|
||||
# 卸載 Redis
|
||||
brew uninstall redis
|
||||
|
||||
# 移除 plist
|
||||
sudo launchctl unload /Library/LaunchDaemons/com.momentry.redis.plist
|
||||
sudo rm /Library/LaunchDaemons/com.momentry.redis.plist
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Step 3: 刪除專屬檔案
|
||||
|
||||
```bash
|
||||
# 刪除數據目錄 (可選)
|
||||
rm -rf /Users/accusys/momentry/var/redis
|
||||
|
||||
# 刪除配置目錄 (可選)
|
||||
rm -rf /Users/accusys/momentry/etc/redis
|
||||
|
||||
# 刪除日誌 (可選)
|
||||
rm -f /Users/accusys/momentry/log/redis.log
|
||||
rm -f /Users/accusys/momentry/log/redis.error.log
|
||||
```
|
||||
|
||||
**注意: 不要刪除以下共用目錄**:
|
||||
```bash
|
||||
# 這些是共用的,不要刪除!
|
||||
# /Users/accusys/momentry/var
|
||||
# /Users/accusys/momentry/etc
|
||||
# /Users/accusys/momentry/log
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Step 4: 卸載後檢查清單
|
||||
|
||||
```bash
|
||||
echo "=== Redis 卸載後檢查 ==="
|
||||
|
||||
# 1. 檢查 Redis 進程
|
||||
echo "1. Redis 進程:"
|
||||
ps aux | grep redis | grep -v grep && echo " ✗ 仍在運行" || echo " ✓ 已停止"
|
||||
|
||||
# 2. Port 6379
|
||||
echo "2. Port 6379:"
|
||||
lsof -i :6379 > /dev/null 2>&1 && echo " ✗ 仍被佔用" || echo " ✓ 已釋放"
|
||||
|
||||
# 3. redis-server 命令
|
||||
echo "3. redis-server 命令:"
|
||||
which redis-server > /dev/null 2>&1 && echo " ✗ 仍存在" || echo " ✓ 已移除"
|
||||
|
||||
# 4. brew 安裝
|
||||
echo "4. brew 安裝:"
|
||||
brew list redis > /dev/null 2>&1 && echo " ✗ 仍存在" || echo " ✓ 已移除"
|
||||
|
||||
# 5. launchctl 服務
|
||||
echo "5. launchctl 服務:"
|
||||
sudo launchctl list | grep redis > /dev/null 2>&1 && echo " ✗ 仍存在" || echo " ✓ 已移除"
|
||||
|
||||
# 6. 數據目錄 (可選刪除)
|
||||
echo "6. 數據目錄:"
|
||||
[ -d "/Users/accusys/momentry/var/redis" ] && echo " ✓ 保留" || echo " ✗ 已刪除"
|
||||
|
||||
# 7. 日誌目錄 (可選刪除)
|
||||
echo "7. 日誌目錄:"
|
||||
[ -d "/Users/accusys/momentry/log" ] && echo " ✓ 保留" || echo " ✗ 已刪除"
|
||||
```
|
||||
|
||||
**預期結果**:
|
||||
```
|
||||
=== Redis 卸載後檢查 ===
|
||||
1. Redis 進程:
|
||||
✓ 已停止
|
||||
2. Port 6379:
|
||||
✓ 已釋放
|
||||
3. redis-server 命令:
|
||||
✓ 已移除
|
||||
4. brew 安裝:
|
||||
✓ 已移除
|
||||
5. launchctl 服務:
|
||||
✓ 已移除
|
||||
6. 數據目錄:
|
||||
✓ 保留 (或 ✗ 已刪除)
|
||||
7. 日誌目錄:
|
||||
✓ 保留 (或 ✗ 已刪除)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 監控配置
|
||||
|
||||
### 添加到監控配置
|
||||
|
||||
在 `monitor/config/monitor_config.yaml` 中添加:
|
||||
|
||||
```yaml
|
||||
service:
|
||||
services:
|
||||
- name: "redis"
|
||||
type: "tcp"
|
||||
port: 6379
|
||||
host: "localhost"
|
||||
timeout: 5
|
||||
enabled: true
|
||||
```
|
||||
|
||||
### 添加健康檢查函數
|
||||
|
||||
在 `monitor/service/health_check.sh` 中添加:
|
||||
|
||||
```bash
|
||||
check_redis() {
|
||||
local start=$(date +%s%N)
|
||||
if redis-cli -a accusys ping > /dev/null 2>&1; then
|
||||
local end=$(date +%s%N)
|
||||
local ms=$(( (end - start) / 1000000 ))
|
||||
echo -e "${GREEN}✓${NC} Redis (6379) - ${ms}ms"
|
||||
record_service "redis" "up" "$ms" ""
|
||||
return 0
|
||||
else
|
||||
echo -e "${RED}✗${NC} Redis (6379) - Down"
|
||||
record_service "redis" "down" "0" "Connection failed"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
```
|
||||
|
||||
```bash
|
||||
# 1. 檢查進程
|
||||
ps aux | grep redis | grep -v grep
|
||||
|
||||
# 2. 檢查 Port
|
||||
lsof -i :6379
|
||||
|
||||
# 3. 測試連線 (無認證)
|
||||
redis-cli -a accusys PING
|
||||
|
||||
# 4. 測試連線 (有認證)
|
||||
redis-cli -a accusys -e "PING"
|
||||
|
||||
# 5. 查看所有 keys
|
||||
redis-cli -a accusys KEYS '*'
|
||||
|
||||
# 6. 查看 info
|
||||
redis-cli -a accusys INFO
|
||||
|
||||
# 7. 查看日誌
|
||||
tail -20 /Users/accusys/momentry/log/redis.log
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 連線資訊
|
||||
|
||||
| 項目 | 值 |
|
||||
|------|-----|
|
||||
| Host | localhost |
|
||||
| Port | 6379 |
|
||||
| Password | accusys |
|
||||
|
||||
---
|
||||
|
||||
## 環境變數
|
||||
|
||||
在 `.env` 中:
|
||||
|
||||
```env
|
||||
REDIS_URL=redis://:accusys@localhost:6379
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 遠端訪問
|
||||
|
||||
- Redis 綁定到 `0.0.0.0` (所有網路介面)
|
||||
- 本地網路其他機器可透過 IP 訪問
|
||||
- 密碼認證: `accusys`
|
||||
|
||||
---
|
||||
|
||||
## 故障排除
|
||||
|
||||
### Redis 無法啟動
|
||||
|
||||
```bash
|
||||
# 檢查日誌
|
||||
tail -f /Users/accusys/momentry/log/redis.log
|
||||
|
||||
# 檢查目錄權限
|
||||
ls -la /Users/accusys/momentry/var/redis/
|
||||
|
||||
# 重新設定權限
|
||||
chown -R $(whoami):staff /Users/accusys/momentry/var/redis
|
||||
```
|
||||
|
||||
### Port 被佔用
|
||||
|
||||
```bash
|
||||
# 檢查哪個程序佔用 port 6379
|
||||
lsof -i :6379
|
||||
|
||||
# 終止佔用程序
|
||||
kill <PID>
|
||||
```
|
||||
|
||||
### 需要重新載入 plist
|
||||
|
||||
```bash
|
||||
# 卸載舊服務 (如果存在)
|
||||
sudo launchctl unload /Library/LaunchDaemons/com.momentry.redis.plist 2>/dev/null
|
||||
|
||||
# 載入新服務
|
||||
sudo launchctl load /Library/LaunchDaemons/com.momentry.redis.plist
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 檔案位置
|
||||
|
||||
| 類型 | 路徑 | 說明 |
|
||||
|------|------|------|
|
||||
| 安裝 | `/opt/homebrew/opt/redis/` | Redis 安裝目錄 |
|
||||
| 執行檔 | `/opt/homebrew/opt/redis/bin/redis-server` | Redis 執行檔 |
|
||||
| 數據目錄 | `/Users/accusys/momentry/var/redis/` | 數據儲存 |
|
||||
| 配置目錄 | `/Users/accusys/momentry/etc/redis/` | 配置儲存 |
|
||||
| 日誌 | `/Users/accusys/momentry/log/redis.log` | 執行日誌 |
|
||||
| 錯誤日誌 | `/Users/accusys/momentry/log/redis.error.log` | 錯誤日誌 |
|
||||
| plist | `/Library/LaunchDaemons/com.momentry.redis.plist` | 開機啟動 |
|
||||
| 備份 | `/Users/accusys/momentry/var/redis_backup_latest.rdb` | 數據備份 |
|
||||
|
||||
---
|
||||
|
||||
## 備份與恢復
|
||||
|
||||
### 備份
|
||||
|
||||
```bash
|
||||
# 觸發保存並備份
|
||||
redis-cli -a accusys SAVE
|
||||
cp /opt/homebrew/var/db/redis/dump.rdb /Users/accusys/momentry/var/redis_backup_latest.rdb
|
||||
```
|
||||
|
||||
### 恢復
|
||||
|
||||
```bash
|
||||
# 停止 Redis
|
||||
redis-cli -a accusys SHUTDOWN
|
||||
|
||||
# 複製備份文件覆蓋
|
||||
cp /Users/accusys/momentry/var/redis_backup_latest.rdb /Users/accusys/momentry/var/redis/dump.rdb
|
||||
|
||||
# 啟動 Redis
|
||||
sudo launchctl load /Library/LaunchDaemons/com.momentry.redis.plist
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 版本資訊
|
||||
|
||||
| 項目 | 值 |
|
||||
|------|-----|
|
||||
| Redis Server | 8.4.0 |
|
||||
| Rust redis crate | 0.32.7 |
|
||||
| Port | 6379 |
|
||||
| Password | accusys |
|
||||
| 數據目錄 | /Users/accusys/momentry/var/redis/ |
|
||||
| 日誌目錄 | /Users/accusys/momentry/log/ |
|
||||
|
||||
---
|
||||
|
||||
## Rust redis crate 版本
|
||||
|
||||
Cargo.toml 中的 redis 依賴:
|
||||
|
||||
```toml
|
||||
redis = { version = "0.32", features = ["tokio-comp"] }
|
||||
```
|
||||
|
||||
### 版本歷史
|
||||
|
||||
| 版本 | 日期 | 變更 |
|
||||
|------|------|-------|
|
||||
| 0.25.4 | - | 原始版本(有未來相容性警告) |
|
||||
| 0.32.7 | 2026-03-21 | **升級** - 修復 Rust 2024 never type 回退問題 |
|
||||
|
||||
### 升級說明
|
||||
|
||||
升級到 0.32.x 的優點:
|
||||
- 修復 Rust 2024 edition 未來相容性問題
|
||||
- API 完全向後相容
|
||||
- 無需修改現有程式碼
|
||||
|
||||
---
|
||||
|
||||
## Redis 用戶配置說明
|
||||
|
||||
### 當前狀態
|
||||
|
||||
| 項目 | 狀態 |
|
||||
|------|------|
|
||||
| 用戶類型 | 僅有 `default` 用戶 |
|
||||
| 自訂用戶 | ❌ 未配置 |
|
||||
| ACL 持久化 | ❌ 未配置 |
|
||||
|
||||
### Redis ACL 狀態
|
||||
|
||||
```bash
|
||||
# 查看 ACL
|
||||
redis-cli -a accusys ACL LIST
|
||||
|
||||
# 輸出:
|
||||
# user default on sanitize-payload #hash ~* &* +@all
|
||||
```
|
||||
|
||||
### 連線格式說明
|
||||
|
||||
| 格式 | 狀態 | 說明 |
|
||||
|------|------|------|
|
||||
| `redis://:accusys@localhost:6379` | ✅ 正確 | 使用默認用戶 + 密碼 |
|
||||
| `redis://accusys:accusys@localhost:6379` | ❌ 失敗 | 用戶 `accusys` 不存在 |
|
||||
|
||||
### 為何用戶名不可用
|
||||
|
||||
1. **Redis 啟動方式**:使用 `--requirepass` 參數,僅設定默認用戶密碼
|
||||
2. **無 ACL 配置文件**:未指定 `--aclfile` 參數
|
||||
3. **動態建立用戶**:手動建立的用戶不會持久化(重啟後消失)
|
||||
|
||||
### 解決方案
|
||||
|
||||
#### 方案 A:使用默認用戶(現行)
|
||||
|
||||
```env
|
||||
REDIS_URL=redis://:accusys@localhost:6379
|
||||
```
|
||||
|
||||
**適用於**:單一應用、簡單部署
|
||||
|
||||
#### 方案 B:建立 ACL 配置文件
|
||||
|
||||
```bash
|
||||
# 1. 建立 ACL 文件
|
||||
cat > /Users/accusys/momentry/etc/redis/users.acl << 'EOF'
|
||||
user default on sanitize-payload ~* &* +@all >accusys
|
||||
user accusys on sanitize-payload ~* &* +@all >accusys
|
||||
EOF
|
||||
|
||||
# 2. 修改 plist (添加 --aclfile 參數)
|
||||
# --aclfile /Users/accusys/momentry/etc/redis/users.acl
|
||||
|
||||
# 3. 重啟 Redis
|
||||
sudo launchctl unload /Library/LaunchDaemons/com.momentry.redis.plist
|
||||
sudo launchctl load /Library/LaunchDaemons/com.momentry.redis.plist
|
||||
```
|
||||
|
||||
**適用於**:多應用、需要用戶隔離
|
||||
|
||||
### 參考
|
||||
|
||||
- 問題追蹤:`docs/PENDING_ISSUES.md` 問題 #5
|
||||
- 測試結果:2026-03-21 Redis 認證測試
|
||||
@@ -1,300 +0,0 @@
|
||||
# RustDesk 安裝指南 (本地部署)
|
||||
|
||||
| 項目 | 內容 |
|
||||
|------|------|
|
||||
| 建立者 | Warren |
|
||||
| 建立時間 | 2026-03-15 |
|
||||
| 文件版本 | V1.0 |
|
||||
|
||||
---
|
||||
|
||||
## 版本歷史
|
||||
|
||||
| 版本 | 日期 | 目的 | 操作人 | 工具/模型 |
|
||||
|------|------|------|--------|-----------|
|
||||
| V1.0 | 2026-03-15 | 創建文件 | Warren | OpenCode / MiniMax M2.5 |
|
||||
|
||||
---
|
||||
|
||||
## 概述
|
||||
|
||||
本文檔說明如何在 macOS 上安裝 RustDesk 遠端桌面服務,配置為本地部署。
|
||||
|
||||
---
|
||||
|
||||
## 當前狀態
|
||||
|
||||
| 項目 | 狀態 |
|
||||
|------|------|
|
||||
| RustDesk | ✅ 已安裝 |
|
||||
| 數據目錄 | /Users/accusys/momentry/var/rustdesk/ |
|
||||
| 日誌目錄 | /Users/accusys/momentry/log/ |
|
||||
| HBBS Plist | /Library/LaunchDaemons/com.momentry.rustdesk.hbbs.plist |
|
||||
| HBBR Plist | /Library/LaunchDaemons/com.momentry.rustdesk.hbbr.plist |
|
||||
|
||||
---
|
||||
|
||||
## 服務端口
|
||||
|
||||
| 服務 | Port | 協議 |
|
||||
|------|------|------|
|
||||
| hbbs (TCP) | 21115 | 主端口 |
|
||||
| hbbs (TCP/UDP) | 21116 | NAT 測試 |
|
||||
| hbbs (WebSocket) | 21118 | WebSocket |
|
||||
| hbbr (TCP) | 21117 | 中繼端口 |
|
||||
| hbbr (TCP) | 21119 | 中繼 extra |
|
||||
|
||||
---
|
||||
|
||||
## 安裝步驟
|
||||
|
||||
### Step 1: 安裝 RustDesk (使用 brew)
|
||||
|
||||
```bash
|
||||
# 安裝 RustDesk
|
||||
brew install rustdesk
|
||||
```
|
||||
|
||||
**驗證**:
|
||||
```bash
|
||||
hbbs --version
|
||||
hbbr --version
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Step 2: 建立目錄
|
||||
|
||||
```bash
|
||||
# 建立數據目錄
|
||||
mkdir -p /Users/accusys/momentry/var/rustdesk
|
||||
|
||||
# 建立配置目錄
|
||||
mkdir -p /Users/accusys/momentry/etc/rustdesk
|
||||
|
||||
# 建立日誌目錄
|
||||
mkdir -p /Users/accusys/momentry/log
|
||||
|
||||
# 建立日誌文件
|
||||
touch /Users/accusys/momentry/log/rustdesk.hbbs.log
|
||||
touch /Users/accusys/momentry/log/rustdesk.hbbs.error.log
|
||||
touch /Users/accusys/momentry/log/rustdesk.hbbr.log
|
||||
touch /Users/accusys/momentry/log/rustdesk.hbbr.error.log
|
||||
|
||||
# 設定權限
|
||||
chown -R accusys:staff /Users/accusys/momentry/var/rustdesk
|
||||
chown -R accusys:staff /Users/accusys/momentry/etc/rustdesk
|
||||
chown -R accusys:staff /Users/accusys/momentry/log
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Step 3: 使用 plist 開機自動啟動
|
||||
|
||||
```bash
|
||||
# 複製 plist 到 LaunchDaemons 目錄
|
||||
sudo cp /Users/accusys/momentry_core_0.1/momentry_runtime/plist/com.momentry.rustdesk.hbbs.plist /Library/LaunchDaemons/
|
||||
sudo cp /Users/accusys/momentry_core_0.1/momentry_runtime/plist/com.momentry.rustdesk.hbbr.plist /Library/LaunchDaemons/
|
||||
|
||||
# 移除舊 plist (如果存在)
|
||||
sudo launchctl unload /Library/LaunchDaemons/com.rustdesk.hbbs.plist 2>/dev/null
|
||||
sudo launchctl unload /Library/LaunchDaemons/com.rustdesk.hbbr.plist 2>/dev/null
|
||||
sudo rm /Library/LaunchDaemons/com.rustdesk.hbbs.plist 2>/dev/null
|
||||
sudo rm /Library/LaunchDaemons/com.rustdesk.hbbr.plist 2>/dev/null
|
||||
|
||||
# 載入並啟動
|
||||
sudo launchctl load /Library/LaunchDaemons/com.momentry.rustdesk.hbbs.plist
|
||||
sudo launchctl load /Library/LaunchDaemons/com.momentry.rustdesk.hbbr.plist
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 監控配置
|
||||
|
||||
### 添加到監控配置
|
||||
|
||||
在 `monitor/config/monitor_config.yaml` 中添加:
|
||||
|
||||
```yaml
|
||||
service:
|
||||
services:
|
||||
- name: "rustdesk-hbbs"
|
||||
type: "tcp"
|
||||
port: 21115
|
||||
host: "localhost"
|
||||
timeout: 5
|
||||
enabled: true
|
||||
- name: "rustdesk-hbbr"
|
||||
type: "tcp"
|
||||
port: 21117
|
||||
host: "localhost"
|
||||
timeout: 5
|
||||
enabled: true
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 卸載步驟
|
||||
|
||||
### 重要: 路徑說明
|
||||
|
||||
| 路徑 | 類型 | 說明 |
|
||||
|------|------|------|
|
||||
| `/Users/accusys/momentry/var/rustdesk/` | 數據 | **不要刪除** - RustDesk 數據 |
|
||||
| `/Users/accusys/momentry/log/` | 日誌 | **不要刪除** - 日誌目錄 |
|
||||
| `/opt/homebrew/bin/hbbr` | 安裝 | **刪除** - RustDesk 安裝 |
|
||||
| `/opt/homebrew/bin/hbbs` | 安裝 | **刪除** - RustDesk 安裝 |
|
||||
|
||||
### Step 1: 停止 RustDesk
|
||||
|
||||
```bash
|
||||
# 停止 RustDesk 服務
|
||||
sudo launchctl unload /Library/LaunchDaemons/com.momentry.rustdesk.hbbs.plist
|
||||
sudo launchctl unload /Library/LaunchDaemons/com.momentry.rustdesk.hbbr.plist
|
||||
|
||||
# 確認停止
|
||||
ps aux | grep rustdesk | grep -v grep || echo "RustDesk 已停止"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Step 2: 卸載 RustDesk
|
||||
|
||||
```bash
|
||||
# 卸載 RustDesk
|
||||
brew uninstall rustdesk
|
||||
|
||||
# 移除 plist
|
||||
sudo rm /Library/LaunchDaemons/com.momentry.rustdesk.hbbs.plist
|
||||
sudo rm /Library/LaunchDaemons/com.momentry.rustdesk.hbbr.plist
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Step 3: 刪除專屬檔案
|
||||
|
||||
```bash
|
||||
# 刪除數據目錄 (可選)
|
||||
rm -rf /Users/accusys/momentry/var/rustdesk
|
||||
|
||||
# 刪除日誌 (可選)
|
||||
rm -f /Users/accusys/momentry/log/rustdesk-*.log
|
||||
```
|
||||
|
||||
**注意: 不要刪除以下共用目錄**:
|
||||
```bash
|
||||
# 這些是共用的,不要刪除!
|
||||
# /Users/accusys/momentry/var
|
||||
# /Users/accusys/momentry/log
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 手動檢查命令
|
||||
|
||||
```bash
|
||||
# 1. 檢查進程
|
||||
ps aux | grep rustdesk | grep -v grep
|
||||
|
||||
# 2. 檢查 Port
|
||||
lsof -i :21115
|
||||
lsof -i :21116
|
||||
lsof -i :21117
|
||||
lsof -i :21118
|
||||
lsof -i :21119
|
||||
|
||||
# 3. 測試連線
|
||||
nc -zv localhost 21115
|
||||
nc -zv localhost 21116
|
||||
|
||||
# 4. 查看日誌
|
||||
tail -20 /Users/accusys/momentry/log/rustdesk-hbbs.log
|
||||
tail -20 /Users/accusys/momentry/log/rustdesk-hbbr.log
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 連線資訊
|
||||
|
||||
| 項目 | 值 |
|
||||
|------|-----|
|
||||
| Server ID | 59.124.167.225 |
|
||||
| NAT Test Port | 21116 |
|
||||
| Relay Port | 21117, 21119 |
|
||||
|
||||
---
|
||||
|
||||
## 故障排除
|
||||
|
||||
### RustDesk 無法啟動
|
||||
|
||||
```bash
|
||||
# 檢查日誌
|
||||
tail -f /Users/accusys/momentry/log/rustdesk-hbbs.log
|
||||
tail -f /Users/accusys/momentry/log/rustdesk-hbbr.log
|
||||
|
||||
# 檢查數據目錄權限
|
||||
ls -la /Users/accusys/momentry/var/rustdesk/
|
||||
|
||||
# 重新設定權限
|
||||
chown -R $(whoami):staff /Users/accusys/momentry/var/rustdesk
|
||||
```
|
||||
|
||||
### Port 被佔用
|
||||
|
||||
```bash
|
||||
# 檢查哪個程序佔用 port
|
||||
lsof -i :21116
|
||||
|
||||
# 終止佔用程序
|
||||
kill <PID>
|
||||
```
|
||||
|
||||
### 需要重新載入 plist
|
||||
|
||||
```bash
|
||||
# 卸載舊服務
|
||||
sudo launchctl unload /Library/LaunchDaemons/com.momentry.rustdesk.hbbs.plist
|
||||
sudo launchctl unload /Library/LaunchDaemons/com.momentry.rustdesk.hbbr.plist
|
||||
|
||||
# 載入新服務
|
||||
sudo launchctl load /Library/LaunchDaemons/com.momentry.rustdesk.hbbs.plist
|
||||
sudo launchctl load /Library/LaunchDaemons/com.momentry.rustdesk.hbbr.plist
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 檔案位置
|
||||
|
||||
| 類型 | 路徑 | 說明 |
|
||||
|------|------|------|
|
||||
| 安裝 | `/opt/homebrew/bin/hbbs` | RustDesk Server 執行檔 |
|
||||
| 安裝 | `/opt/homebrew/bin/hbbr` | RustDesk Relay 執行檔 |
|
||||
| 數據目錄 | `/Users/accusys/momentry/var/rustdesk/` | 數據儲存 |
|
||||
| HBBS 日誌 | `/Users/accusys/momentry/log/rustdesk-hbbs.log` | 服務日誌 |
|
||||
| HBBR 日誌 | `/Users/accusys/momentry/log/rustdesk-hbbr.log` | 中繼日誌 |
|
||||
| HBBS Plist | `/Library/LaunchDaemons/com.momentry.rustdesk.hbbs.plist` | 開機啟動 |
|
||||
| HBBR Plist | `/Library/LaunchDaemons/com.momentry.rustdesk.hbbr.plist` | 開機啟動 |
|
||||
|
||||
---
|
||||
|
||||
## 版本資訊
|
||||
|
||||
- 安裝方式: Homebrew (Cask)
|
||||
- Client 版本: 1.4.6
|
||||
- Server 版本: 1.1.15 (hbbs/hbbr binaries from homebrew)
|
||||
- 數據目錄: /Users/accusys/momentry/var/rustdesk/
|
||||
- 日誌目錄: /Users/accusys/momentry/log/
|
||||
|
||||
---
|
||||
|
||||
## 注意事項
|
||||
|
||||
### Server 版本
|
||||
|
||||
Homebrew 的 RustDesk Cask 只提供客戶端應用程序。服務器二進制文件 (hbbs, hbbr) 需要從其他來源安裝或自行編譯。當前使用的版本較舊 (1.1.15)。
|
||||
|
||||
如需更新服務器版本,可以考慮:
|
||||
1. 從源代碼編譯最新版本
|
||||
2. 使用 RustDesk 官方提供的 Docker 鏡像
|
||||
3. 等待 Homebrew 添加服務器公式
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,332 +0,0 @@
|
||||
# WordPress 安裝指南 (Portal)
|
||||
|
||||
| 項目 | 內容 |
|
||||
|------|------|
|
||||
| 建立者 | Warren |
|
||||
| 建立時間 | 2026-03-22 |
|
||||
| 文件版本 | V1.0 |
|
||||
|
||||
---
|
||||
|
||||
## 版本歷史
|
||||
|
||||
| 版本 | 日期 | 目的 | 操作人 | 工具/模型 |
|
||||
|------|------|------|--------|-----------|
|
||||
| V1.0 | 2026-03-22 | 創建文件 | Warren | OpenCode / big-pickle |
|
||||
|
||||
---
|
||||
|
||||
## 1. 概述
|
||||
|
||||
本文檔說明 Momentry Portal 的 WordPress 安裝配置,作為系統入口整合 n8n 自動化與 sftpgo 檔案服務。
|
||||
|
||||
---
|
||||
|
||||
## 2. 當前狀態
|
||||
|
||||
| 項目 | 狀態 |
|
||||
|------|------|
|
||||
| WordPress 版本 | 6.x |
|
||||
| URL | https://wp.momentry.ddns.net |
|
||||
| 安裝路徑 | `/Users/accusys/wordpress/web` |
|
||||
| 資料庫 | wordpress (MariaDB) |
|
||||
| 資料庫用戶 | wp_user |
|
||||
|
||||
---
|
||||
|
||||
## 3. 目錄結構
|
||||
|
||||
```
|
||||
/Users/accusys/wordpress/
|
||||
├── web/ # WordPress 主目錄
|
||||
│ ├── wp-admin/ # WordPress 管理面板
|
||||
│ ├── wp-content/ # 內容目錄
|
||||
│ │ ├── ai1wm-backups/ # 備份檔案 (*.wpress)
|
||||
│ │ ├── languages/ # 語言檔案
|
||||
│ │ ├── plugins/ # 插件目錄
|
||||
│ │ ├── themes/ # 主題目錄
|
||||
│ │ ├── uploads/ # 上傳檔案
|
||||
│ │ └── cache/ # 快取目錄
|
||||
│ ├── wp-includes/ # WordPress 核心
|
||||
│ └── wp-config.php # 配置文件
|
||||
├── docker-compose.yml # Docker 配置
|
||||
└── wordpress_backup.sql # 資料庫備份
|
||||
```
|
||||
|
||||
### 空間使用
|
||||
|
||||
| 目錄 | 大小 | 說明 |
|
||||
|------|------|------|
|
||||
| `ai1wm-backups/` | ~250MB | 完整備份 (保留 2 個) |
|
||||
| `plugins/` | 80MB | 插件 |
|
||||
| `themes/` | 14MB | 主題 |
|
||||
| `uploads/` | 12MB | 上傳檔案 |
|
||||
|
||||
---
|
||||
|
||||
## 4. 程式碼位置
|
||||
|
||||
自訂程式碼存放位置:
|
||||
|
||||
| 類型 | 路徑 |
|
||||
|------|------|
|
||||
| 主題 | `/Users/accusys/wordpress/web/wp-content/themes/` |
|
||||
| 插件 | `/Users/accusys/wordpress/web/wp-content/plugins/` |
|
||||
| 必須插件 | `/Users/accusys/wordpress/web/wp-content/mu-plugins/` |
|
||||
|
||||
---
|
||||
|
||||
## 5. 插件清單
|
||||
|
||||
| 插件 | 用途 | 說明 |
|
||||
|------|------|------|
|
||||
| elementor | 頁面建構 | 視覺化頁面編輯器 |
|
||||
| all-in-one-wp-migration | 網站遷移/備份 | 完整網站備份工具 |
|
||||
| akismet | 垃圾留言過濾 | 保護留言區域 |
|
||||
| code-snippets | 自訂程式碼 | 無需修改主題即可添加 PHP |
|
||||
|
||||
### Elementor 版本
|
||||
- 版本: 最新穩定版
|
||||
- 用途: 頁面建構(開發階段)
|
||||
|
||||
### 未來計畫
|
||||
- Phase 2: OpenCode 重構
|
||||
- 目標: 交付無 Elementor 依賴版本
|
||||
|
||||
---
|
||||
|
||||
## 6. 主題清單
|
||||
|
||||
| 主題 | 說明 |
|
||||
|------|------|
|
||||
| twentytwentyfive | 目前使用主題 |
|
||||
| twentytwentyfour | 備用 |
|
||||
| twentytwentythree | 備用 |
|
||||
|
||||
---
|
||||
|
||||
## 7. 整合計畫
|
||||
|
||||
### 7.1 n8n 整合
|
||||
|
||||
n8n 作為自動化引擎,WordPress 頁面透過 REST API 或 Webhook 與 n8n 通訊。
|
||||
|
||||
| 整合方式 | 說明 |
|
||||
|----------|------|
|
||||
| REST API | WordPress 呼叫 n8n API |
|
||||
| Webhook | n8n 觸發 WordPress 動作 |
|
||||
|
||||
### 7.2 sftpgo 整合
|
||||
|
||||
sftpgo 作為檔案服務,WordPress 頁面提供檔案上傳/下載功能。
|
||||
|
||||
| 整合方式 | 說明 |
|
||||
|----------|------|
|
||||
| WebDAV | 透過 WebDAV API 操作檔案 |
|
||||
| REST API | 透過 sftpgo API 操作檔案 |
|
||||
|
||||
---
|
||||
|
||||
## 8. 管理命令
|
||||
|
||||
### 8.1 清理 ai1wm 備份
|
||||
|
||||
```bash
|
||||
# 查看現有備份
|
||||
ls -lt /Users/accusys/wordpress/web/wp-content/ai1wm-backups/*.wpress
|
||||
|
||||
# 保留最近 2 個,刪除舊的
|
||||
ls -t /Users/accusys/wordpress/web/wp-content/ai1wm-backups/*.wpress | tail -n +3 | xargs rm
|
||||
|
||||
# 驗證結果
|
||||
du -sh /Users/accusys/wordpress/web/wp-content/ai1wm-backups/
|
||||
```
|
||||
|
||||
### 8.2 清理 WordPress 快取
|
||||
|
||||
```bash
|
||||
# 刪除 Object Cache
|
||||
wp cache flush
|
||||
|
||||
# 刪除 Elementor 快取
|
||||
wp elementor flush_css
|
||||
|
||||
# 刪除全部快取
|
||||
wp cache flush && wp elementor flush_css
|
||||
```
|
||||
|
||||
### 8.3 資料庫操作
|
||||
|
||||
```bash
|
||||
# 匯出資料庫
|
||||
mysqldump -u wp_user -p wordpress > wordpress_backup.sql
|
||||
|
||||
# 匯入資料庫
|
||||
mysql -u wp_user -p wordpress < wordpress_backup.sql
|
||||
```
|
||||
|
||||
### 8.4 權限檢查
|
||||
|
||||
```bash
|
||||
# 檢查目錄權限
|
||||
ls -la /Users/accusys/wordpress/web/wp-content/
|
||||
|
||||
# 確認 wp-content 可寫入
|
||||
chown -R _www:_www /Users/accusys/wordpress/web/wp-content/
|
||||
chmod -R 755 /Users/accusys/wordpress/web/wp-content/
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 9. 故障排除
|
||||
|
||||
### 9.1 常見問題
|
||||
|
||||
| 問題 | 解決方案 |
|
||||
|------|----------|
|
||||
| 頁面載入緩慢 | 清理 Elementor/Object Cache |
|
||||
| 上傳檔案失敗 | 檢查 wp-content/uploads 權限 |
|
||||
| 502/504 錯誤 | 重啟 PHP-FPM |
|
||||
| 資料庫連線失敗 | 檢查 wp-config.php 設定 |
|
||||
|
||||
### 9.2 診斷命令
|
||||
|
||||
```bash
|
||||
# 檢查 PHP-FPM 狀態
|
||||
lsof -i :9000
|
||||
|
||||
# 檢查 MySQL/MariaDB 狀態
|
||||
lsof -i :3306
|
||||
|
||||
# 檢查 Apache/Nginx 狀態
|
||||
lsof -i :80
|
||||
|
||||
# 查看 WordPress 錯誤日誌
|
||||
tail -100 /Users/accusys/momentry/log/php-fpm.log
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 10. 開發協作
|
||||
|
||||
### 10.1 與 marcom 團隊協作
|
||||
|
||||
| 角色 | 負責 |
|
||||
|------|------|
|
||||
| marcom 團隊 | Figma 設計 / Elementor 建構 |
|
||||
| OpenCode | 程式碼實作 / 重構 |
|
||||
|
||||
### 10.2 開發流程
|
||||
|
||||
```
|
||||
Phase 1: marcom 建構 (現在)
|
||||
└── Elementor 頁面建構
|
||||
|
||||
Phase 2: 交付審視 (TBD)
|
||||
└── 功能確認 / 重構評估
|
||||
|
||||
Phase 3: OpenCode 重構 (討論後)
|
||||
└── 純程式碼實作
|
||||
└── 交付客戶 (無 Elementor 依賴)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 11. PHP LSP 開發環境
|
||||
|
||||
### 11.1 軟體需求
|
||||
|
||||
| 軟體 | 版本 | 安裝方式 |
|
||||
|------|------|----------|
|
||||
| PHP | 8.0+ | 已安裝 (8.5.2) |
|
||||
| Composer | 2.0+ | `brew install composer` |
|
||||
| phpactor | Latest | PHAR 安裝 |
|
||||
|
||||
### 11.2 安裝步驟
|
||||
|
||||
#### 1. 安裝 Composer
|
||||
|
||||
```bash
|
||||
brew install composer
|
||||
```
|
||||
|
||||
#### 2. 安裝 phpactor
|
||||
|
||||
```bash
|
||||
curl -sSL https://github.com/phpactor/phpactor/releases/latest/download/phpactor.phar -o ~/bin/phpactor
|
||||
chmod +x ~/bin/phpactor
|
||||
export PATH="$HOME/bin:$PATH"
|
||||
```
|
||||
|
||||
#### 3. 安裝 WordPress Stubs
|
||||
|
||||
```bash
|
||||
cd /Users/accusys/wordpress/web
|
||||
composer require --dev php-stubs/wordpress-stubs
|
||||
```
|
||||
|
||||
#### 4. 設定 phpactor
|
||||
|
||||
```bash
|
||||
# 設定檔位置
|
||||
mkdir -p ~/.config/phpactor
|
||||
|
||||
# 設定內容
|
||||
cat > ~/.config/phpactor/phpactor.json << 'EOF'
|
||||
{
|
||||
"core.min_memory_limit": 1610612736,
|
||||
"worse_reflection.additive_stubs": [
|
||||
"/Users/accusys/wordpress/web/vendor/php-stubs/wordpress-stubs/wordpress-stubs.php"
|
||||
]
|
||||
}
|
||||
EOF
|
||||
```
|
||||
|
||||
#### 5. 建立索引
|
||||
|
||||
```bash
|
||||
cd /Users/accusys/wordpress/web
|
||||
~/bin/phpactor index:build --reset
|
||||
```
|
||||
|
||||
### 11.3 OpenCode 使用方式
|
||||
|
||||
```bash
|
||||
# 確認安裝
|
||||
~/bin/phpactor --version
|
||||
|
||||
# 查詢類別
|
||||
~/bin/phpactor class:search "WP_User"
|
||||
|
||||
# 查看類別資訊
|
||||
~/bin/phpactor index:query WP_User
|
||||
|
||||
# 導航到定義
|
||||
~/bin/phpactor navigate /path/to/file.php
|
||||
|
||||
# 查詢參照
|
||||
~/bin/phpactor references /path/to/file.php
|
||||
```
|
||||
|
||||
### 11.4 常用指令
|
||||
|
||||
| 指令 | 用途 |
|
||||
|------|------|
|
||||
| `phpactor class:search` | 搜尋類別 |
|
||||
| `phpactor index:query` | 查詢索引 |
|
||||
| `phpactor index:build` | 建立索引 |
|
||||
| `phpactor index:clean` | 清除索引 |
|
||||
| `phpactor config:dump` | 顯示設定 |
|
||||
|
||||
---
|
||||
|
||||
## 12. 檔案位置
|
||||
|
||||
| 類型 | 路徑 | 說明 |
|
||||
|------|------|------|
|
||||
| WordPress 主目錄 | `/Users/accusys/wordpress/web` | 網站根目錄 |
|
||||
| 備份目錄 | `/Users/accusys/momentry/backup/wordpress/` | 每日備份 |
|
||||
| 日誌目錄 | `/Users/accusys/momentry/log/` | PHP/Apache 日誌 |
|
||||
| phpactor | `~/bin/phpactor` | PHP LSP 主程式 |
|
||||
| phpactor 設定 | `~/.config/phpactor/phpactor.json` | LSP 設定檔 |
|
||||
| WordPress Stubs | `/Users/accusys/wordpress/web/vendor/php-stubs/` | WordPress 函數定義 |
|
||||
@@ -1,682 +0,0 @@
|
||||
# Job Worker 實作計畫
|
||||
|
||||
| 項目 | 內容 |
|
||||
|------|------|
|
||||
| 建立者 | Warren / OpenCode |
|
||||
| 建立時間 | 2026-03-24 |
|
||||
| 文件版本 | V1.1 |
|
||||
| 狀態 | ✅ 已實作 |
|
||||
|
||||
---
|
||||
|
||||
## 版本歷史
|
||||
|
||||
| 版本 | 日期 | 目的 | 操作人 |
|
||||
|------|------|------|--------|
|
||||
| V1.0 | 2026-03-24 | 建立實作計畫 | OpenCode |
|
||||
| V1.1 | 2026-03-25 | 實作完成,更新狀態 | OpenCode |
|
||||
|
||||
---
|
||||
|
||||
## 實作狀態
|
||||
|
||||
### ✅ 已完成
|
||||
|
||||
| 元件 | 檔案 | 狀態 |
|
||||
|------|------|------|
|
||||
| MonitorJob 結構 | `src/core/db/postgres_db.rs` | ✅ |
|
||||
| ProcessorResult 結構 | `src/core/db/postgres_db.rs` | ✅ |
|
||||
| Worker 配置 | `src/worker/config.rs` | ✅ |
|
||||
| Job Worker | `src/worker/job_worker.rs` | ✅ |
|
||||
| Processor Pool | `src/worker/processor.rs` | ✅ |
|
||||
| Worker 模組 | `src/worker/mod.rs` | ✅ |
|
||||
| PostgreSQL 表格 | `monitor_jobs`, `processor_results` | ✅ |
|
||||
| 類型修復 | `i32`, `NaiveDateTime` | ✅ |
|
||||
|
||||
### 待整合
|
||||
|
||||
| 項目 | 說明 |
|
||||
|------|------|
|
||||
| Worker 服務啟動 | 需要加入 launchd plist |
|
||||
| 監控整合 | 需要加入 MOMENTRY_CORE_MONITORING.md |
|
||||
| 備份涵蓋 | 需要確認備份包含新表格 |
|
||||
|
||||
---
|
||||
|
||||
## 1. 設計決策
|
||||
|
||||
### 1.1 確認的設計決策
|
||||
|
||||
| 項目 | 決策 | 理由 |
|
||||
|------|------|------|
|
||||
| 觸發方式 | 輪詢(Job Worker) | 暫無可靠的 API 觸發機制 |
|
||||
| 並行處理 | 最多 2 個 | 可根據 CPU/GPU 能力調整 |
|
||||
| 失敗處理 | 獨立模組,部分完成可接續 | 任何模組失敗都產出狀態記錄 |
|
||||
| Worker 啟動 | 獨立進程 | 隔離、易管理 |
|
||||
| 並行上限調整 | 環境變數 + 預設值 | 靈活、可調整 |
|
||||
| 狀態同步 | PostgreSQL + Redis | 可靠 + 即時 |
|
||||
|
||||
### 1.2 環境變數
|
||||
|
||||
| 變數 | 預設值 | 說明 |
|
||||
|------|--------|------|
|
||||
| `MOMENTRY_MAX_CONCURRENT` | 2 | 最大並行 processor 數 |
|
||||
| `MOMENTRY_POLL_INTERVAL` | 5 | 輪詢間隔(秒) |
|
||||
| `MOMENTRY_WORKER_ENABLED` | true | 是否啟用 worker |
|
||||
|
||||
---
|
||||
|
||||
## 2. 系統架構
|
||||
|
||||
### 2.1 完整流程圖
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────────┐
|
||||
│ 檔案註冊觸發處理流程 │
|
||||
├─────────────────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ 1. SFTPGo 上傳 │
|
||||
│ │ │
|
||||
│ ▼ │
|
||||
│ 2. Hook 呼叫 Register API │
|
||||
│ │ │
|
||||
│ ▼ │
|
||||
│ 3. Register API │
|
||||
│ ├─► ffprobe 提取 metadata │
|
||||
│ ├─► 寫入 videos 表 │
|
||||
│ └─► 建立 monitor_jobs 記錄 (status=pending) │
|
||||
│ │ │
|
||||
│ ▼ │
|
||||
│ 4. Job Worker (獨立進程,輪詢機制) │
|
||||
│ ├─► 輪詢 pending jobs │
|
||||
│ ├─► 檢查 videos 表 fs_json 決定需要處理什麼 │
|
||||
│ ├─► 並行執行 processors (最多 2 個) │
|
||||
│ └─► 更新 videos, monitor_jobs, processor_results 表 │
|
||||
│ │ │
|
||||
│ ▼ │
|
||||
│ 5. 處理結果 │
|
||||
│ ├─► 更新 videos 表 (fs_json, psql_chunk, qvector_chunk) │
|
||||
│ ├─► 更新 monitor_jobs 表 (status, progress) │
|
||||
│ ├─► 更新 processor_results 表 (每個模組狀態) │
|
||||
│ └─► Redis Pub/Sub 即時進度 │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 2.2 Job Worker 架構
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────┐
|
||||
│ Job Worker 架構 │
|
||||
├─────────────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
|
||||
│ │ PostgreSQL │ ───▶ │ Worker │ ───▶ │ Processor │ │
|
||||
│ │ Job Queue │ │ Loop │ │ Pool │ │
|
||||
│ └─────────────┘ └──────┬──────┘ └──────┬──────┘ │
|
||||
│ │ │ │
|
||||
│ ▼ ▼ │
|
||||
│ ┌─────────────┐ ┌─────────────┐ │
|
||||
│ │ Video State │ │ Processor 1 │ │
|
||||
│ │ Check │ │ (ASR/YOLO) │ │
|
||||
│ └─────────────┘ ├─────────────┤ │
|
||||
│ │ Processor 2 │ │
|
||||
│ │ (CUT/OCR) │ │
|
||||
│ └─────────────┘ │
|
||||
│ │
|
||||
│ Redis ──── Pub/Sub ──── 即時進度 │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. 資料庫結構
|
||||
|
||||
### 3.1 Migration 檔案
|
||||
|
||||
**檔案**: `migrations/003_job_worker.sql`
|
||||
|
||||
```sql
|
||||
-- ================================================================
|
||||
-- Migration 003: Job Worker System
|
||||
-- ================================================================
|
||||
|
||||
-- 3.1.1 更新 videos 表
|
||||
ALTER TABLE videos ADD COLUMN IF NOT EXISTS status VARCHAR(20) DEFAULT 'pending';
|
||||
ALTER TABLE videos ADD COLUMN IF NOT EXISTS user_id BIGINT;
|
||||
ALTER TABLE videos ADD COLUMN IF NOT EXISTS job_id INTEGER REFERENCES monitor_jobs(id);
|
||||
|
||||
COMMENT ON COLUMN videos.status IS 'pending, processing, completed, failed';
|
||||
COMMENT ON COLUMN videos.user_id IS 'WordPress user ID';
|
||||
COMMENT ON COLUMN videos.job_id IS 'Associated monitor_jobs ID';
|
||||
|
||||
-- 3.1.2 更新 monitor_jobs 表
|
||||
ALTER TABLE monitor_jobs ADD COLUMN IF NOT EXISTS video_id BIGINT REFERENCES videos(id);
|
||||
ALTER TABLE monitor_jobs ADD COLUMN IF NOT EXISTS user_id BIGINT;
|
||||
ALTER TABLE monitor_jobs ADD COLUMN IF NOT EXISTS processors VARCHAR(20)[];
|
||||
ALTER TABLE monitor_jobs ADD COLUMN IF NOT EXISTS completed_processors VARCHAR(20)[];
|
||||
ALTER TABLE monitor_jobs ADD COLUMN IF NOT EXISTS failed_processors VARCHAR(20)[];
|
||||
|
||||
COMMENT ON COLUMN monitor_jobs.processors IS 'Processors to run: asr, cut, yolo, ocr, face, pose, asrx';
|
||||
COMMENT ON COLUMN monitor_jobs.completed_processors IS 'Successfully completed processors';
|
||||
COMMENT ON COLUMN monitor_jobs.failed_processors IS 'Failed processors';
|
||||
|
||||
-- 3.1.3 新增 processor_results 表
|
||||
CREATE TABLE IF NOT EXISTS processor_results (
|
||||
id SERIAL PRIMARY KEY,
|
||||
job_id INTEGER REFERENCES monitor_jobs(id) ON DELETE CASCADE,
|
||||
video_id BIGINT REFERENCES videos(id) ON DELETE CASCADE,
|
||||
processor VARCHAR(20) NOT NULL,
|
||||
status VARCHAR(20) NOT NULL DEFAULT 'pending',
|
||||
output_path TEXT,
|
||||
started_at TIMESTAMP,
|
||||
completed_at TIMESTAMP,
|
||||
error_message TEXT,
|
||||
progress_total INT DEFAULT 0,
|
||||
progress_current INT DEFAULT 0,
|
||||
last_checkpoint JSONB,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
UNIQUE(job_id, processor)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_processor_results_job ON processor_results(job_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_processor_results_video ON processor_results(video_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_processor_results_status ON processor_results(status);
|
||||
|
||||
COMMENT ON TABLE processor_results IS 'Tracks individual processor execution status';
|
||||
COMMENT ON COLUMN processor_results.status IS 'pending, running, completed, failed, skipped';
|
||||
|
||||
-- 3.1.4 更新 videos 表標記欄位用途
|
||||
COMMENT ON COLUMN videos.fs_video IS 'Video file exists on filesystem';
|
||||
COMMENT ON COLUMN videos.fs_json IS 'All processor JSON files generated';
|
||||
COMMENT ON COLUMN videos.fs_chunks IS 'Chunk files generated';
|
||||
COMMENT ON COLUMN videos.fs_vectors IS 'Vector files generated';
|
||||
COMMENT ON COLUMN videos.psql_chunk IS 'Chunks stored in PostgreSQL';
|
||||
COMMENT ON COLUMN videos.pvector_chunk IS 'Vectors stored in PostgreSQL';
|
||||
COMMENT ON COLUMN videos.qvector_chunk IS 'Vectors stored in Qdrant';
|
||||
```
|
||||
|
||||
### 3.2 表關係圖
|
||||
|
||||
```
|
||||
videos monitor_jobs
|
||||
┌──────────────────────┐ ┌──────────────────────┐
|
||||
│ id (PK) │◄────────│ video_id (FK) │
|
||||
│ uuid │ │ user_id │
|
||||
│ status │ │ processors[] │
|
||||
│ fs_video │ │ completed_processors[]│
|
||||
│ fs_json │ │ failed_processors[] │
|
||||
│ job_id (FK)─────────┼────────►│ status │
|
||||
│ user_id │ │ id (PK) │
|
||||
└──────────────────────┘ └──────────────────────┘
|
||||
│
|
||||
│
|
||||
processor_results
|
||||
┌──────────────────────┐
|
||||
│ job_id (FK) │
|
||||
│ video_id (FK) │
|
||||
│ processor │
|
||||
│ status │
|
||||
│ progress_current │
|
||||
│ last_checkpoint │
|
||||
│ id (PK) │
|
||||
└──────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. 模組並行策略
|
||||
|
||||
### 4.1 模組分類
|
||||
|
||||
| 模組 | 資源需求 | 獨立性 | 建議並行 |
|
||||
|------|----------|--------|----------|
|
||||
| ASR | GPU/CPU | 高 | ✅ 可並行 |
|
||||
| CUT | CPU | 高 | ✅ 可並行 |
|
||||
| YOLO | GPU | 中 | ✅ 可並行 |
|
||||
| OCR | GPU/CPU | 高 | ✅ 可並行 |
|
||||
| Face | GPU | 中 | ✅ 可並行 |
|
||||
| Pose | GPU | 中 | ✅ 可並行 |
|
||||
| ASRX | GPU/CPU | 高 | ✅ 可並行 |
|
||||
|
||||
### 4.2 建議並行組合
|
||||
|
||||
| 組合 | 模組 1 | 模組 2 | 說明 |
|
||||
|------|---------|---------|------|
|
||||
| GPU+CPU | YOLO/Pose/Face | ASR/CUT/OCR | 平衡負載 |
|
||||
| 雙GPU | YOLO | Pose | 雙 GPU 卡片 |
|
||||
| 雙CPU | ASR | CUT/OCR | 無 GPU 時 |
|
||||
|
||||
### 4.3 Worker 配置
|
||||
|
||||
```rust
|
||||
// src/worker/config.rs
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct WorkerConfig {
|
||||
pub max_concurrent: usize, // 預設 2
|
||||
pub poll_interval_secs: u64, // 預設 5
|
||||
pub enabled: bool, // 預設 true
|
||||
}
|
||||
|
||||
impl Default for WorkerConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
max_concurrent: 2,
|
||||
poll_interval_secs: 5,
|
||||
enabled: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl WorkerConfig {
|
||||
pub fn from_env() -> Self {
|
||||
Self {
|
||||
max_concurrent: std::env::var("MOMENTRY_MAX_CONCURRENT")
|
||||
.ok()
|
||||
.and_then(|v| v.parse().ok())
|
||||
.unwrap_or(2),
|
||||
poll_interval_secs: std::env::var("MOMENTRY_POLL_INTERVAL")
|
||||
.ok()
|
||||
.and_then(|v| v.parse().ok())
|
||||
.unwrap_or(5),
|
||||
enabled: std::env::var("MOMENTRY_WORKER_ENABLED")
|
||||
.ok()
|
||||
.map(|v| v != "false")
|
||||
.unwrap_or(true),
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. 失敗處理機制
|
||||
|
||||
### 5.1 設計原則
|
||||
|
||||
```
|
||||
每個模組獨立處理:
|
||||
- 成功 → 產出完整 .json,status=completed
|
||||
- 失敗 → 產出 .json 包含 error 狀態,status=failed
|
||||
- 部分完成 → 可從 checkpoint 繼續,status=running
|
||||
```
|
||||
|
||||
### 5.2 Processor 輸出格式
|
||||
|
||||
```json
|
||||
{
|
||||
"processor": "asr",
|
||||
"status": "completed|failed|partial",
|
||||
"completed_at": "2026-03-24T12:00:00Z",
|
||||
"result": { ... },
|
||||
"error": null,
|
||||
"last_checkpoint": {
|
||||
"frame": 5000,
|
||||
"timestamp": 180.5
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 5.3 失敗處理流程
|
||||
|
||||
```rust
|
||||
async fn run_processor(&self, module: &str, video: &Video) -> Result<()> {
|
||||
let output_path = self.get_output_path(video, module);
|
||||
|
||||
match self.execute_processor(module, video, &output_path).await {
|
||||
Ok(result) => {
|
||||
// 成功:更新狀態
|
||||
self.db.update_processor_status(job_id, module, "completed").await?;
|
||||
self.publish_progress(job_id, module, 100).await?;
|
||||
}
|
||||
Err(e) => {
|
||||
// 失敗:仍然保存部分結果
|
||||
let partial_result = self.get_partial_result(&output_path);
|
||||
self.db.update_processor_status(job_id, module, "failed").await?;
|
||||
self.db.save_error_message(job_id, module, &e.to_string()).await?;
|
||||
|
||||
// 記錄錯誤但不中斷其他模組
|
||||
tracing::warn!("Processor {} failed: {}", module, e);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. 實作結構
|
||||
|
||||
### 6.1 目錄結構
|
||||
|
||||
```
|
||||
src/
|
||||
├── worker/
|
||||
│ ├── mod.rs # Worker 模組導出
|
||||
│ ├── config.rs # Worker 配置
|
||||
│ ├── worker.rs # Worker 主邏輯
|
||||
│ ├── processor.rs # Processor 執行器
|
||||
│ ├── queue.rs # Job 佇列管理
|
||||
│ └── progress.rs # 進度追蹤
|
||||
├── api/
|
||||
│ └── server.rs # 更新 Register API
|
||||
└── main.rs # 新增 worker 命令
|
||||
```
|
||||
|
||||
### 6.2 核心模組
|
||||
|
||||
#### 6.2.1 Worker Config (`src/worker/config.rs`)
|
||||
|
||||
```rust
|
||||
pub struct WorkerConfig {
|
||||
pub max_concurrent: usize,
|
||||
pub poll_interval_secs: u64,
|
||||
pub enabled: bool,
|
||||
}
|
||||
|
||||
impl WorkerConfig {
|
||||
pub fn from_env() -> Self { ... }
|
||||
}
|
||||
```
|
||||
|
||||
#### 6.2.2 Worker Loop (`src/worker/worker.rs`)
|
||||
|
||||
```rust
|
||||
pub struct JobWorker {
|
||||
db: PostgresDb,
|
||||
redis: RedisCache,
|
||||
config: WorkerConfig,
|
||||
semaphore: Arc<Semaphore>,
|
||||
}
|
||||
|
||||
impl JobWorker {
|
||||
pub async fn run(&self) -> Result<()> {
|
||||
loop {
|
||||
if self.config.enabled {
|
||||
self.process_pending_jobs().await?;
|
||||
}
|
||||
tokio::time::sleep(Duration::from_secs(self.config.poll_interval_secs)).await;
|
||||
}
|
||||
}
|
||||
|
||||
async fn process_pending_jobs(&self) -> Result<()> {
|
||||
// 1. 檢查並發數
|
||||
// 2. 取得 pending jobs
|
||||
// 3. 分配給 worker pool
|
||||
// 4. 並行執行 processors
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 6.2.3 Processor Pool (`src/worker/processor.rs`)
|
||||
|
||||
```rust
|
||||
pub struct ProcessorPool {
|
||||
max_concurrent: usize,
|
||||
}
|
||||
|
||||
impl ProcessorPool {
|
||||
pub async fn execute(&self, job: &Job, video: &Video) -> Result<ProcessorResult> {
|
||||
// 根據 videos 表決定需要執行哪些 processor
|
||||
// 並行執行最多 2 個
|
||||
// 處理失敗但不中斷其他 processor
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. API 端點設計
|
||||
|
||||
### 7.1 新增端點
|
||||
|
||||
| 端點 | 方法 | 說明 |
|
||||
|------|------|------|
|
||||
| `/api/v1/jobs` | GET | 列出所有 jobs |
|
||||
| `/api/v1/jobs/:uuid` | GET | 取得特定 job 詳細 |
|
||||
| `/api/v1/jobs/:uuid/retry` | POST | 重試失敗的 processor |
|
||||
| `/api/v1/jobs/:uuid/cancel` | POST | 取消 job |
|
||||
|
||||
### 7.2 端點詳情
|
||||
|
||||
#### GET /api/v1/jobs
|
||||
|
||||
```json
|
||||
Response:
|
||||
{
|
||||
"jobs": [
|
||||
{
|
||||
"id": 1,
|
||||
"uuid": "abc123def456",
|
||||
"status": "running",
|
||||
"progress": 60,
|
||||
"processors": ["asr", "cut", "yolo", "ocr", "face", "pose"],
|
||||
"completed": ["asr", "cut", "yolo"],
|
||||
"failed": []
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
#### GET /api/v1/jobs/:uuid
|
||||
|
||||
```json
|
||||
Response:
|
||||
{
|
||||
"id": 1,
|
||||
"uuid": "abc123def456",
|
||||
"video_id": 10,
|
||||
"status": "running",
|
||||
"processors": {
|
||||
"asr": {"status": "completed", "progress": 100},
|
||||
"cut": {"status": "completed", "progress": 100},
|
||||
"yolo": {"status": "running", "progress": 45, "current": 5000, "total": 11000},
|
||||
"ocr": {"status": "pending"},
|
||||
"face": {"status": "pending"},
|
||||
"pose": {"status": "pending"}
|
||||
},
|
||||
"created_at": "2026-03-24T12:00:00Z",
|
||||
"started_at": "2026-03-24T12:01:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. Redis Key 設計
|
||||
|
||||
### 8.1 現有 Key 保持
|
||||
|
||||
```bash
|
||||
momentry:job:{uuid} # Job Hash
|
||||
momentry:job:{uuid}:processor:{name} # Processor Hash
|
||||
momentry:progress:{uuid} # Pub/Sub Channel
|
||||
momentry:jobs:active # Set: 運行中 UUIDs
|
||||
momentry:jobs:completed # Set: 完成 UUIDs
|
||||
momentry:jobs:failed # Set: 失敗 UUIDs
|
||||
```
|
||||
|
||||
### 8.2 進度更新時序
|
||||
|
||||
```
|
||||
Processor 執行
|
||||
│
|
||||
├─► 每秒更新 Redis Hash (即時)
|
||||
│
|
||||
├─► 每 10% 或完成時更新 PostgreSQL (持久)
|
||||
│
|
||||
└─► 失敗時立即更新 PostgreSQL (錯誤記錄)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 9. 實作順序
|
||||
|
||||
### Phase 1: 資料庫遷移
|
||||
|
||||
| 任務 | 說明 |
|
||||
|------|------|
|
||||
| 1.1 | 建立 `migrations/003_job_worker.sql` |
|
||||
| 1.2 | 更新 `postgres_db.rs` 對應的 struct |
|
||||
| 1.3 | 執行 migration 驗證 |
|
||||
|
||||
### Phase 2: Worker 框架
|
||||
|
||||
| 任務 | 說明 |
|
||||
|------|------|
|
||||
| 2.1 | 建立 `src/worker/mod.rs` |
|
||||
| 2.2 | 建立 `src/worker/config.rs` |
|
||||
| 2.3 | 建立 `src/worker/worker.rs` |
|
||||
| 2.4 | 建立 `src/worker/processor.rs` |
|
||||
|
||||
### Phase 3: Register API 整合
|
||||
|
||||
| 任務 | 說明 |
|
||||
|------|------|
|
||||
| 3.1 | 修改 `src/api/server.rs` 的 register 函數 |
|
||||
| 3.2 | 加入建立 monitor_jobs 的邏輯 |
|
||||
| 3.3 | 更新 videos 表 status 欄位 |
|
||||
|
||||
### Phase 4: Processor 執行
|
||||
|
||||
| 任務 | 說明 |
|
||||
|------|------|
|
||||
| 4.1 | 實作 processor 並行執行(最多 2 個) |
|
||||
| 4.2 | 實作失敗處理(保存部分結果) |
|
||||
| 4.3 | 實作 checkpoint 恢復 |
|
||||
|
||||
### Phase 5: 進度追蹤
|
||||
|
||||
| 任務 | 說明 |
|
||||
|------|------|
|
||||
| 5.1 | Redis Pub/Sub 整合 |
|
||||
| 5.2 | PostgreSQL 定期同步 |
|
||||
| 5.3 | API 進度端點更新 |
|
||||
|
||||
### Phase 6: API 端點
|
||||
|
||||
| 任務 | 說明 |
|
||||
|------|------|
|
||||
| 6.1 | GET /api/v1/jobs |
|
||||
| 6.2 | GET /api/v1/jobs/:uuid |
|
||||
| 6.3 | POST /api/v1/jobs/:uuid/retry |
|
||||
| 6.4 | POST /api/v1/jobs/:uuid/cancel |
|
||||
|
||||
### Phase 7: CLI 命令
|
||||
|
||||
| 任務 | 說明 |
|
||||
|------|------|
|
||||
| 7.1 | `cargo run -- worker` 命令 |
|
||||
| 7.2 | Worker 啟動/停止/狀態顯示 |
|
||||
| 7.3 | launchd plist 設定 |
|
||||
|
||||
### Phase 8: 測試
|
||||
|
||||
| 任務 | 說明 |
|
||||
|------|------|
|
||||
| 8.1 | 單元測試 |
|
||||
| 8.2 | 端到端測試 |
|
||||
| 8.3 | 失敗處理測試 |
|
||||
| 8.4 | 並行執行測試 |
|
||||
|
||||
---
|
||||
|
||||
## 10. CLI 命令
|
||||
|
||||
### 10.1 Worker 命令
|
||||
|
||||
```bash
|
||||
# 啟動 worker
|
||||
cargo run -- worker
|
||||
|
||||
# 顯示 worker 幫助
|
||||
cargo run -- worker --help
|
||||
```
|
||||
|
||||
### 10.2 環境變數
|
||||
|
||||
```bash
|
||||
# Worker 配置
|
||||
export MOMENTRY_MAX_CONCURRENT=2
|
||||
export MOMENTRY_POLL_INTERVAL=5
|
||||
export MOMENTRY_WORKER_ENABLED=true
|
||||
|
||||
# 現有環境變數
|
||||
export DATABASE_URL=postgres://accusys@localhost:5432/momentry
|
||||
export REDIS_URL=redis://:accusys@localhost:6379
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 11. 預估工時
|
||||
|
||||
| Phase | 任務 | 預估工時 |
|
||||
|-------|------|----------|
|
||||
| 1 | 資料庫遷移 | 2h |
|
||||
| 2 | Worker 框架 | 4h |
|
||||
| 3 | Register API 整合 | 2h |
|
||||
| 4 | Processor 執行 | 4h |
|
||||
| 5 | 進度追蹤 | 2h |
|
||||
| 6 | API 端點 | 3h |
|
||||
| 7 | CLI 命令 | 2h |
|
||||
| 8 | 測試 | 4h |
|
||||
| **總計** | | **23h** |
|
||||
|
||||
---
|
||||
|
||||
## 12. 參考文件
|
||||
|
||||
| 文件 | 用途 |
|
||||
|------|------|
|
||||
| `docs/MOMENTRY_CORE_MONITORING.md` | 監控系統規範 |
|
||||
| `docs/MOMENTRY_CORE_REDIS_KEYS.md` | Redis Key 設計 |
|
||||
| `docs/PROCESSING_PIPELINE.md` | 處理流程 |
|
||||
| `docs/CHUNK_DESIGN.md` | 資料庫設計 |
|
||||
| `docs/API_REFERENCE.md` | API 參考 |
|
||||
|
||||
---
|
||||
|
||||
## 13. 附錄
|
||||
|
||||
### A. 狀態機
|
||||
|
||||
```
|
||||
┌──────────────┐
|
||||
│ PENDING │
|
||||
└──────┬───────┘
|
||||
│ register 後
|
||||
▼
|
||||
┌──────────────┐
|
||||
┌─────▶│ PROCESSING │◀──────┐
|
||||
│ └──────┬───────┘ │
|
||||
│ │ │
|
||||
部分失敗 all completed 全部失敗
|
||||
│ │ │
|
||||
▼ ▼ ▼
|
||||
┌──────────┐ ┌──────────┐ ┌──────────┐
|
||||
│ PARTIAL │ │COMPLETED │ │ FAILED │
|
||||
└──────────┘ └──────────┘ └──────────┘
|
||||
```
|
||||
|
||||
### B. videos 表 status 欄位
|
||||
|
||||
| 值 | 說明 |
|
||||
|------|------|
|
||||
| `pending` | 已註冊,等待處理 |
|
||||
| `processing` | 處理中 |
|
||||
| `completed` | 所有處理完成 |
|
||||
| `failed` | 處理失敗 |
|
||||
|
||||
### C. processor_results 表 status 欄位
|
||||
|
||||
| 值 | 說明 |
|
||||
|------|------|
|
||||
| `pending` | 等待執行 |
|
||||
| `running` | 執行中 |
|
||||
| `completed` | 執行成功 |
|
||||
| `failed` | 執行失敗 |
|
||||
| `skipped` | 跳過(如檔案已存在) |
|
||||
@@ -1,519 +0,0 @@
|
||||
# Momentry JSON 輸出檔案規範
|
||||
|
||||
| 項目 | 內容 |
|
||||
|------|------|
|
||||
| 建立者 | Warren |
|
||||
| 建立時間 | 2026-03-16 |
|
||||
| 文件版本 | V1.0 |
|
||||
|
||||
---
|
||||
|
||||
## 版本歷史
|
||||
|
||||
| 版本 | 日期 | 目的 | 操作人 | 工具/模型 |
|
||||
|------|------|------|--------|-----------|
|
||||
| V1.0 | 2026-03-16 | 創建文件 | Warren | OpenCode / MiniMax M2.5 |
|
||||
|
||||
---
|
||||
|
||||
本文檔定義 Momentry Core 系統中所有 JSON 輸出檔案的結構、命名規範與儲存位置。
|
||||
|
||||
---
|
||||
|
||||
## 1. 輸出檔案總覽
|
||||
|
||||
### 1.1 檔案類型
|
||||
|
||||
| 類型 | 前綴 | 說明 | 狀態 |
|
||||
|------|------|------|------|
|
||||
| **Probe** | `{uuid}.probe.json` | 影片元數據 | ✅ 已實作 |
|
||||
| **ASR** | `{uuid}.asr.json` | 語音識別結果 | ✅ 已實作 |
|
||||
| **ASRx** | `{uuid}.asrx.json` | 說話者分離 | 🔜 規劃中 |
|
||||
| **OCR** | `{uuid}.ocr.json` | 文字辨識結果 | 🔜 規劃中 |
|
||||
| **YOLO** | `{uuid}.yolo.json` | 物件偵測結果 | 🔜 規劃中 |
|
||||
| **Face** | `{uuid}.face.json` | 人臉偵測結果 | 🔜 規劃中 |
|
||||
| **Pose** | `{uuid}.pose.json` | 姿態估計結果 | 🔜 規劃中 |
|
||||
| **Thumbnail** | `{uuid}/thumb_XXX.jpg` | 縮圖檔案 | ✅ 已實作 |
|
||||
|
||||
### 1.2 命名規範
|
||||
|
||||
```
|
||||
{UUID}.{類型}.json
|
||||
|
||||
範例:
|
||||
1636719dc31f78ac.probe.json - 影片探測結果
|
||||
1636719dc31f78ac.asr.json - 語音識別結果
|
||||
1636719dc31f78ac.ocr.json - 文字辨識結果
|
||||
```
|
||||
|
||||
- **UUID**: 16 字元,基於檔案路徑計算
|
||||
- **類型**: 小寫 snake_case
|
||||
- **副檔名**: `.json`
|
||||
|
||||
---
|
||||
|
||||
## 2. 輸出目錄結構
|
||||
|
||||
### 2.1 預設輸出位置
|
||||
|
||||
```
|
||||
momentry_core_0.1/
|
||||
├── {uuid}.probe.json # 影片探測
|
||||
├── {uuid}.asr.json # 語音識別
|
||||
├── {uuid}.asrx.json # 說話者分離
|
||||
├── {uuid}.ocr.json # 文字辨識
|
||||
├── {uuid}.yolo.json # 物件偵測
|
||||
├── {uuid}.face.json # 人臉偵測
|
||||
├── {uuid}.pose.json # 姿態估計
|
||||
└── thumbnails/
|
||||
└── {uuid}/
|
||||
├── thumb_000.jpg
|
||||
├── thumb_001.jpg
|
||||
└── ...
|
||||
```
|
||||
|
||||
### 2.2 儲存策略
|
||||
|
||||
| 資料類型 | 儲存位置 | 說明 |
|
||||
|----------|----------|------|
|
||||
| JSON 檔案 | 專案根目錄 | 方便快速存取 |
|
||||
| 縮圖 | thumbnails/{uuid}/ | 分離儲存 |
|
||||
| 資料庫 | PostgreSQL | 長期儲存 |
|
||||
|
||||
---
|
||||
|
||||
## 3. JSON 結構定義
|
||||
|
||||
### 3.1 Probe (影片探測)
|
||||
|
||||
**檔案**: `{uuid}.probe.json`
|
||||
|
||||
```json
|
||||
{
|
||||
"streams": [
|
||||
{
|
||||
"index": 0,
|
||||
"codec_name": "h264",
|
||||
"codec_type": "video",
|
||||
"width": 1920,
|
||||
"height": 1080,
|
||||
"r_frame_rate": "60000/1001",
|
||||
"duration": "6879.329524",
|
||||
"sample_rate": null,
|
||||
"channels": null
|
||||
},
|
||||
{
|
||||
"index": 1,
|
||||
"codec_name": "aac",
|
||||
"codec_type": "audio",
|
||||
"width": null,
|
||||
"height": null,
|
||||
"r_frame_rate": "0/0",
|
||||
"duration": "6879.245333",
|
||||
"sample_rate": "48000",
|
||||
"channels": 2
|
||||
}
|
||||
],
|
||||
"format": {
|
||||
"filename": "/path/to/video.mov",
|
||||
"format_name": "mov,mp4,m4a,3gp,3g2,mj2",
|
||||
"duration": "6879.329524",
|
||||
"size": "2361629896",
|
||||
"bit_rate": "2748000"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**欄位說明**:
|
||||
|
||||
| 欄位 | 類型 | 說明 |
|
||||
|------|------|------|
|
||||
| `streams` | Array | 媒體串流陣列 |
|
||||
| `streams[].index` | Integer | 串流索引 |
|
||||
| `streams[].codec_name` | String | 編碼名稱 |
|
||||
| `streams[].codec_type` | String | 串流類型 (video/audio) |
|
||||
| `streams[].width` | Integer | 寬度 (video) |
|
||||
| `streams[].height` | Integer | 高度 (video) |
|
||||
| `streams[].r_frame_rate` | String | 幀率 |
|
||||
| `streams[].duration` | String | 持續時間 (秒) |
|
||||
| `streams[].sample_rate` | String | 採樣率 (audio) |
|
||||
| `streams[].channels` | Integer | 聲道數 (audio) |
|
||||
| `format` | Object | 檔案格式資訊 |
|
||||
| `format.filename` | String | 原始檔案路徑 |
|
||||
| `format.format_name` | String | 格式名稱 |
|
||||
| `format.duration` | String | 總時長 (秒) |
|
||||
| `format.size` | String | 檔案大小 (bytes) |
|
||||
| `format.bit_rate` | String | 位元率 |
|
||||
|
||||
---
|
||||
|
||||
### 3.2 ASR (語音識別)
|
||||
|
||||
**檔案**: `{uuid}.asr.json`
|
||||
|
||||
```json
|
||||
{
|
||||
"language": "en",
|
||||
"language_probability": 0.9945855736732483,
|
||||
"segments": [
|
||||
{
|
||||
"start": 0.0,
|
||||
"end": 19.04,
|
||||
"text": "Hello and welcome to the old-time movie show."
|
||||
},
|
||||
{
|
||||
"start": 19.04,
|
||||
"end": 25.44,
|
||||
"text": "Today we are featuring the 1963 comedy mystery film Charade."
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**欄位說明**:
|
||||
|
||||
| 欄位 | 類型 | 說明 |
|
||||
|------|------|------|
|
||||
| `language` | String | 偵測語言代碼 (ISO 639-1) |
|
||||
| `language_probability` | Float | 語言偵測機率 (0-1) |
|
||||
| `segments` | Array | 語音分段陣列 |
|
||||
| `segments[].start` | Float | 開始時間 (秒) |
|
||||
| `segments[].end` | Float | 結束時間 (秒) |
|
||||
| `segments[].text` | String | 識別文字 |
|
||||
|
||||
---
|
||||
|
||||
### 3.3 ASRx (說話者分離)
|
||||
|
||||
**檔案**: `{uuid}.asrx.json`
|
||||
|
||||
```json
|
||||
{
|
||||
"language": "en",
|
||||
"language_probability": 0.95,
|
||||
"segments": [
|
||||
{
|
||||
"start": 0.0,
|
||||
"end": 19.04,
|
||||
"text": "Hello and welcome to the old-time movie show.",
|
||||
"speaker_id": "SPEAKER_00",
|
||||
"speaker_embedding": [0.123, -0.456, ...]
|
||||
},
|
||||
{
|
||||
"start": 19.04,
|
||||
"end": 25.44,
|
||||
"text": "Today we are featuring the 1963 comedy mystery film Charade.",
|
||||
"speaker_id": "SPEAKER_01",
|
||||
"speaker_embedding": [0.789, -0.123, ...]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**欄位說明**:
|
||||
|
||||
| 欄位 | 類型 | 說明 |
|
||||
|------|------|------|
|
||||
| `language` | String | 偵測語言代碼 |
|
||||
| `language_probability` | Float | 語言偵測機率 |
|
||||
| `segments` | Array | 語音分段陣列 |
|
||||
| `segments[].start` | Float | 開始時間 (秒) |
|
||||
| `segments[].end` | Float | 結束時間 (秒) |
|
||||
| `segments[].text` | String | 識別文字 |
|
||||
| `segments[].speaker_id` | String | 說話者 ID |
|
||||
| `segments[].speaker_embedding` | Array | 說話者嵌入向量 (可選) |
|
||||
|
||||
---
|
||||
|
||||
### 3.4 OCR (文字辨識)
|
||||
|
||||
**檔案**: `{uuid}.ocr.json`
|
||||
|
||||
```json
|
||||
{
|
||||
"segments": [
|
||||
{
|
||||
"start": 10.5,
|
||||
"end": 12.3,
|
||||
"text": "EXAMPLE TEXT",
|
||||
"boxes": [
|
||||
{
|
||||
"x1": 100,
|
||||
"y1": 50,
|
||||
"x2": 400,
|
||||
"y2": 100
|
||||
}
|
||||
],
|
||||
"confidence": 0.95
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**欄位說明**:
|
||||
|
||||
| 欄位 | 類型 | 說明 |
|
||||
|------|------|------|
|
||||
| `segments` | Array | OCR 分段陣列 |
|
||||
| `segments[].start` | Float | 開始時間 (秒) |
|
||||
| `segments[].end` | Float | 結束時間 (秒) |
|
||||
| `segments[].text` | String | 辨識文字 |
|
||||
| `segments[].boxes` | Array | 文字邊界框陣列 |
|
||||
| `segments[].boxes[].x1` | Integer | 左上 X 座標 |
|
||||
| `segments[].boxes[].y1` | Integer | 左上 Y 座標 |
|
||||
| `segments[].boxes[].x2` | Integer | 右下 X 座標 |
|
||||
| `segments[].boxes[].y2` | Integer | 右下 Y 座標 |
|
||||
| `segments[].confidence` | Float | 辨識信心度 |
|
||||
|
||||
---
|
||||
|
||||
### 3.5 YOLO (物件偵測)
|
||||
|
||||
**檔案**: `{uuid}.yolo.json`
|
||||
|
||||
```json
|
||||
{
|
||||
"segments": [
|
||||
{
|
||||
"start": 0.0,
|
||||
"end": 1.0,
|
||||
"objects": [
|
||||
{
|
||||
"class": "person",
|
||||
"confidence": 0.92,
|
||||
"box": {
|
||||
"x1": 150,
|
||||
"y1": 200,
|
||||
"x2": 400,
|
||||
"y2": 800
|
||||
}
|
||||
},
|
||||
{
|
||||
"class": "car",
|
||||
"confidence": 0.87,
|
||||
"box": {
|
||||
"x1": 800,
|
||||
"y1": 400,
|
||||
"x2": 1200,
|
||||
"y2": 700
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**欄位說明**:
|
||||
|
||||
| 欄位 | 類型 | 說明 |
|
||||
|------|------|------|
|
||||
| `segments` | Array | 時間分段陣列 |
|
||||
| `segments[].start` | Float | 開始時間 (秒) |
|
||||
| `segments[].end` | Float | 結束時間 (秒) |
|
||||
| `segments[].objects` | Array | 偵測物件陣列 |
|
||||
| `segments[].objects[].class` | String | 物件類別 |
|
||||
| `segments[].objects[].confidence` | Float | 偵測信心度 |
|
||||
| `segments[].objects[].box` | Object | 邊界框 |
|
||||
|
||||
---
|
||||
|
||||
### 3.6 Face (人臉偵測)
|
||||
|
||||
**檔案**: `{uuid}.face.json`
|
||||
|
||||
```json
|
||||
{
|
||||
"segments": [
|
||||
{
|
||||
"start": 0.0,
|
||||
"end": 1.0,
|
||||
"faces": [
|
||||
{
|
||||
"face_id": "face_001",
|
||||
"box": {
|
||||
"x1": 100,
|
||||
"y1": 50,
|
||||
"x2": 300,
|
||||
"y2": 350
|
||||
},
|
||||
"embedding": [0.123, -0.456, ...],
|
||||
"emotion": "happy",
|
||||
"age": 35,
|
||||
"gender": "female"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**欄位說明**:
|
||||
|
||||
| 欄位 | 類型 | 說明 |
|
||||
|------|------|------|
|
||||
| `segments` | Array | 時間分段陣列 |
|
||||
| `segments[].start` | Float | 開始時間 (秒) |
|
||||
| `segments[].end` | Float | 結束時間 (秒) |
|
||||
| `segments[].faces` | Array | 人臉陣列 |
|
||||
| `segments[].faces[].face_id` | String | 人臉 ID |
|
||||
| `segments[].faces[].box` | Object | 邊界框 |
|
||||
| `segments[].faces[].embedding` | Array | 人臉嵌入向量 |
|
||||
| `segments[].faces[].emotion` | String | 情緒分類 (可選) |
|
||||
| `segments[].faces[].age` | Integer | 年齡估計 (可選) |
|
||||
| `segments[].faces[].gender` | String | 性別估計 (可選) |
|
||||
|
||||
---
|
||||
|
||||
### 3.7 Pose (姿態估計)
|
||||
|
||||
**檔案**: `{uuid}.pose.json`
|
||||
|
||||
```json
|
||||
{
|
||||
"segments": [
|
||||
{
|
||||
"start": 0.0,
|
||||
"end": 1.0,
|
||||
"poses": [
|
||||
{
|
||||
"person_id": "person_001",
|
||||
"keypoints": {
|
||||
"nose": {"x": 320, "y": 120, "confidence": 0.98},
|
||||
"left_eye": {"x": 335, "y": 110, "confidence": 0.95},
|
||||
"right_eye": {"x": 305, "y": 110, "confidence": 0.93},
|
||||
"left_shoulder": {"x": 280, "y": 180, "confidence": 0.91},
|
||||
"right_shoulder": {"x": 360, "y": 180, "confidence": 0.89}
|
||||
},
|
||||
"confidence": 0.92
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**欄位說明**:
|
||||
|
||||
| 欄位 | 類型 | 說明 |
|
||||
|------|------|------|
|
||||
| `segments` | Array | 時間分段陣列 |
|
||||
| `segments[].start` | Float | 開始時間 (秒) |
|
||||
| `segments[].end` | Float | 結束時間 (秒) |
|
||||
| `segments[].poses` | Array | 姿態陣列 |
|
||||
| `segments[].poses[].person_id` | String | 人員 ID |
|
||||
| `segments[].poses[].keypoints` | Object | 關鍵點 |
|
||||
| `segments[].poses[].keypoints.{name}` | Object | 各關鍵點 |
|
||||
| `segments[].poses[].keypoints.{name}.x` | Integer | X 座標 |
|
||||
| `segments[].poses[].keypoints.{name}.y` | Integer | Y 座標 |
|
||||
| `segments[].poses[].keypoints.{name}.confidence` | Float | 信心度 |
|
||||
| `segments[].poses[].confidence` | Float | 整體信心度 |
|
||||
|
||||
---
|
||||
|
||||
## 4. 處理流程
|
||||
|
||||
### 4.1 處理管線
|
||||
|
||||
```
|
||||
影片檔案
|
||||
│
|
||||
▼
|
||||
┌─────────────────┐
|
||||
│ 1. Register │ 建立 UUID,註冊影片
|
||||
└────────┬────────┘
|
||||
│
|
||||
┌────▼────┐
|
||||
│ 2. Probe │ ffprobe 擷取元數據
|
||||
└────┬────┘
|
||||
│ {uuid}.probe.json
|
||||
┌────▼─────┐
|
||||
│ 3. ASR │ faster-whisper 語音識別
|
||||
└────┬─────┘
|
||||
│ {uuid}.asr.json
|
||||
┌────▼──────┐
|
||||
│ 4. ASRx │ 說話者分離 (pyannote)
|
||||
└────┬──────┘
|
||||
│ {uuid}.asrx.json
|
||||
┌────▼────┐
|
||||
│ 5. OCR │ Tesseract 文字辨識
|
||||
└────┬────┘
|
||||
│ {uuid}.ocr.json
|
||||
┌────▼────┐
|
||||
│ 6. YOLO │ 物件偵測
|
||||
└────┬────┘
|
||||
│ {uuid}.yolo.json
|
||||
┌────▼────┐
|
||||
│ 7. Face │ 人臉偵測
|
||||
└────┬────┘
|
||||
│ {uuid}.face.json
|
||||
┌────▼────┐
|
||||
│ 8. Pose │ 姿態估計
|
||||
└────┬────┘
|
||||
│ {uuid}.pose.json
|
||||
┌────▼──────┐
|
||||
│ 9. Chunk │ 轉換為資料庫 chunks
|
||||
└───────────┘
|
||||
```
|
||||
|
||||
### 4.2 失敗處理
|
||||
|
||||
| 階段 | 失敗時 | 處理 |
|
||||
|------|--------|------|
|
||||
| Probe | 無法讀取影片 | 終止流程,輸出錯誤 |
|
||||
| ASR | 無音軌 | 產生空 segments,繼續流程 |
|
||||
| OCR/YOLO/Face/Pose | 處理失敗 | 跳過該階段,記錄日誌 |
|
||||
|
||||
---
|
||||
|
||||
## 5. 資料庫儲存
|
||||
|
||||
### 5.1 Chunk 結構
|
||||
|
||||
```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,
|
||||
end_time DOUBLE PRECISION NOT NULL,
|
||||
content JSONB NOT NULL,
|
||||
created_at TIMESTAMP DEFAULT NOW(),
|
||||
UNIQUE(uuid, chunk_id)
|
||||
);
|
||||
```
|
||||
|
||||
### 5.2 轉換範例
|
||||
|
||||
```rust
|
||||
// ASR → Chunk (Sentence)
|
||||
for (i, seg) in asr_result.segments.iter().enumerate() {
|
||||
let chunk = Chunk::new(
|
||||
uuid.clone(),
|
||||
i as u32,
|
||||
ChunkType::Sentence,
|
||||
seg.start,
|
||||
seg.end,
|
||||
serde_json::json!({"text": seg.text}),
|
||||
);
|
||||
db.store_chunk(&chunk).await?;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. 版本歷史
|
||||
|
||||
| 版本 | 日期 | 變更 |
|
||||
|------|------|------|
|
||||
| 1.0.0 | 2026-03-16 | 初始版本 |
|
||||
|
||||
---
|
||||
|
||||
## 7. 相關文件
|
||||
|
||||
- [RUST_DEVELOPMENT.md](./RUST_DEVELOPMENT.md) - Rust 開發規範
|
||||
- [AGENTS.md](../AGENTS.md) - 開發規範
|
||||
- [monitor_config.yaml](../monitor/config/monitor_config.yaml) - 監控配置
|
||||
@@ -1,782 +0,0 @@
|
||||
# Momentry 系統自動化安裝計劃
|
||||
|
||||
> **計劃階段** - 僅供討論,尚未執行
|
||||
> **建立時間**: 2026-03-23
|
||||
> **目標**: Thunderbolt NVMe 外開機完整安裝
|
||||
|
||||
---
|
||||
|
||||
## 系統概述
|
||||
|
||||
### 當前環境
|
||||
| 項目 | 內容 |
|
||||
|------|------|
|
||||
| **主控機** | Mac mini (M4, 16GB RAM) |
|
||||
| **作業系統** | macOS 26.3.1 (Tahoe) |
|
||||
| **儲存** | Thunderbolt NVMe (2TB) |
|
||||
| **用途** | 開機碟 + 完整 Momentry 系統 |
|
||||
|
||||
### 目標環境
|
||||
| 項目 | 內容 |
|
||||
|------|------|
|
||||
| **目標主機** | 其他 Mac (Intel 或 Apple Silicon) |
|
||||
| **安裝方式** | Thunderbolt NVMe 外接開機 |
|
||||
| **連接方式** | Thunderbolt 3/4 |
|
||||
| **控制方式** | SSH 遠端管理 |
|
||||
|
||||
---
|
||||
|
||||
## 系統架構
|
||||
|
||||
### 服務列表
|
||||
|
||||
| 服務 | 版本 | 用途 | Port |
|
||||
|------|------|------|------|
|
||||
| **PostgreSQL** | 18.1 | 主資料庫、n8n 資料庫 | 5432 |
|
||||
| **MongoDB** | 8.0 | 文件資料庫 | 27017 |
|
||||
| **MariaDB** | 11.4 | WordPress 資料庫 | 3306 |
|
||||
| **Redis** | 7.x | 快取、佇列 | 6379 |
|
||||
| **Qdrant** | 1.7.x | 向量資料庫 | 6333 |
|
||||
| **Ollama** | 0.13.5 | 本地 LLM | 11434 |
|
||||
| **Caddy** | 2.x | 反向代理 | 80/443 |
|
||||
| **Gitea** | 1.21 | Git 服務 | 3000 |
|
||||
| **PHP-FPM** | 8.5 | WordPress | 9000 |
|
||||
| **n8n** | 2.3.5 | 工作流程自動化 | 5678 |
|
||||
| **RustDesk** | hbbs/hbbr | 遠端桌面 | 21115-21119 |
|
||||
| **SFTPGo** | 2.x | SFTP 服務 | 2022 |
|
||||
| **Momentry Core** | 0.1.0 | 影片處理核心 | 3002 |
|
||||
| **Prometheus** | 3.9.1 | 監控 | 9090 |
|
||||
|
||||
### 目錄結構
|
||||
|
||||
```
|
||||
/Volumes/Momentry/
|
||||
├── System/
|
||||
│ └── macOS/ # macOS 系統
|
||||
├── Applications/
|
||||
│ └── Homebrew/ # Homebrew 應用程式
|
||||
├── momentry/
|
||||
│ ├── var/ # 資料目錄
|
||||
│ │ ├── postgresql/ # PostgreSQL 資料
|
||||
│ │ ├── mongodb/ # MongoDB 資料
|
||||
│ │ ├── mariadb/ # MariaDB 資料
|
||||
│ │ ├── redis/ # Redis 資料
|
||||
│ │ ├── qdrant/ # Qdrant 資料
|
||||
│ │ ├── n8n/ # n8n 資料
|
||||
│ │ ├── ollama/ # Ollama 模型
|
||||
│ │ └── ...
|
||||
│ ├── etc/ # 配置檔案
|
||||
│ │ ├── Caddyfile
|
||||
│ │ ├── gitea/
|
||||
│ │ ├── php/
|
||||
│ │ └── ...
|
||||
│ ├── log/ # 日誌
|
||||
│ ├── scripts/ # 管理腳本
|
||||
│ └── backup/ # 備份
|
||||
├── momentry_core/ # Rust 原始碼
|
||||
└── momentry_dashboard/ # Web Dashboard
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 階段一:前置準備
|
||||
|
||||
### 1.1 收集目標主機資訊
|
||||
|
||||
```bash
|
||||
# 需要收集的資訊
|
||||
- Mac 型號 (Intel/Apple Silicon)
|
||||
- macOS 版本
|
||||
- Thunderbolt 版本 (3/4)
|
||||
- 可用記憶體
|
||||
- 目標磁碟代號 (diskX)
|
||||
- 網路配置 (DHCP/固定 IP)
|
||||
```
|
||||
|
||||
### 1.2 準備 Thunderbolt NVMe
|
||||
|
||||
```bash
|
||||
# 檢查 Thunderbolt NVMe
|
||||
diskutil list external
|
||||
|
||||
# 預期輸出:
|
||||
# /dev/diskX (external, physical):
|
||||
# NAME TYPE SIZE
|
||||
# Thunderbolt NVMe ...
|
||||
```
|
||||
|
||||
### 1.3 準備主控機腳本
|
||||
|
||||
```bash
|
||||
# 主控機需要準備的腳本
|
||||
~/momentry/setup/
|
||||
├── 01_prepare_disk.sh
|
||||
├── 02_install_macos.sh
|
||||
├── 03_install_homebrew.sh
|
||||
├── 04_install_dependencies.sh
|
||||
├── 05_install_services.sh
|
||||
├── 06_install_momentry.sh
|
||||
├── 07_configure_network.sh
|
||||
├── 08_start_services.sh
|
||||
└── utils/
|
||||
├── common.sh
|
||||
├── backup.sh
|
||||
└── monitor.sh
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 階段二:Thunderbolt NVMe 準備
|
||||
|
||||
### 2.1 分割磁碟方案 A(推薦)
|
||||
|
||||
```bash
|
||||
# 磁碟分割配置
|
||||
diskutil partitionDisk /dev/diskX \
|
||||
GPT \
|
||||
"APFS System" APFS "Momentry System" 200G \
|
||||
"APFS Data" APFS "Momentry Data" 1.8T
|
||||
```
|
||||
|
||||
### 2.2 分割磁碟方案 B(最小化)
|
||||
|
||||
```bash
|
||||
# 統一 APFS 容器
|
||||
diskutil partitionDisk /dev/diskX \
|
||||
GPT \
|
||||
APFS "Momentry" 100%
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 階段三:安裝 macOS
|
||||
|
||||
### 3.1 建立 macOS 安裝碟
|
||||
|
||||
```bash
|
||||
# 下載 macOS Sonoma (或最新版本)
|
||||
softwareupdate --fetch-full-installer --full-installer-version 14.0
|
||||
|
||||
# 建立可開機安裝碟
|
||||
sudo /Applications/Install\ macOS\ Sonoma.app/Contents/Resources/createinstallinstmedi \
|
||||
--volume /Volumes/Momentry \
|
||||
--nointeraction
|
||||
```
|
||||
|
||||
### 3.2 安裝 macOS 到 Thunderbolt NVMe
|
||||
|
||||
**兩種方法:**
|
||||
|
||||
#### 方法 A: 復原模式安裝
|
||||
1. 連接 Thunderbolt NVMe
|
||||
2. 重啟目標主機,按住Option鍵
|
||||
3. 選擇 Thunderbolt NVMe 開機
|
||||
4. 進入 Recovery Mode (Command+R)
|
||||
5. 使用 Disk Utility 格式化目標磁碟
|
||||
6. 安裝 macOS
|
||||
|
||||
#### 方法 B: ASR 複製(建議)
|
||||
```bash
|
||||
# 從主控機執行
|
||||
# 將現有系統複製到目標磁碟
|
||||
sudo asr restore \
|
||||
--source /Volumes/Macintosh\ HD \
|
||||
--target /Volumes/Momentry \
|
||||
--erase --noprompt
|
||||
```
|
||||
|
||||
### 3.3 設定 macOS
|
||||
|
||||
```bash
|
||||
# 自動化設定腳本
|
||||
./setup/scripts/03_install_homebrew.sh
|
||||
```
|
||||
|
||||
**設定項目:**
|
||||
- 電腦名稱:`momentry-<serial>`
|
||||
- 使用者帳號:`momentry` (管理員)
|
||||
- SSH 遠端登入:啟用
|
||||
- 螢幕鎖定:關閉
|
||||
- 節能設定:永不休眠
|
||||
|
||||
---
|
||||
|
||||
## 階段四:安裝 Homebrew
|
||||
|
||||
### 4.1 安裝 Homebrew
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# 04_install_homebrew.sh
|
||||
|
||||
# 檢查架構
|
||||
ARCH=$(uname -m)
|
||||
|
||||
if [ "$ARCH" = "arm64" ]; then
|
||||
# Apple Silicon
|
||||
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
|
||||
echo 'eval "$(/opt/homebrew/bin/brew shellenv)"' >> ~/.zprofile
|
||||
eval "$(/opt/homebrew/bin/brew shellenv)"
|
||||
elif [ "$ARCH" = "x86_64" ]; then
|
||||
# Intel
|
||||
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
|
||||
echo 'eval "$(/usr/local/bin/brew shellenv)"' >> ~/.zprofile
|
||||
eval "$(/usr/local/bin/brew shellenv)"
|
||||
fi
|
||||
|
||||
# 驗證
|
||||
brew --version
|
||||
```
|
||||
|
||||
### 4.2 安裝基礎工具
|
||||
|
||||
```bash
|
||||
# 基礎開發工具
|
||||
brew install \
|
||||
git \
|
||||
curl \
|
||||
wget \
|
||||
jq \
|
||||
yq \
|
||||
tree \
|
||||
htop \
|
||||
tmux \
|
||||
zsh \
|
||||
zsh-completions
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 階段五:安裝服務
|
||||
|
||||
### 5.1 安裝資料庫服務
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# 05_install_services.sh
|
||||
|
||||
# PostgreSQL
|
||||
brew install postgresql@18
|
||||
brew services start postgresql@18
|
||||
|
||||
# MongoDB
|
||||
brew tap mongodb/brew
|
||||
brew install mongodb-community
|
||||
brew services start mongodb-community
|
||||
|
||||
# MariaDB
|
||||
brew install mariadb
|
||||
brew services start mariadb
|
||||
|
||||
# Redis
|
||||
brew install redis
|
||||
brew services start redis
|
||||
|
||||
# Qdrant (需要 Cargo)
|
||||
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
|
||||
cargo install qdrant
|
||||
```
|
||||
|
||||
### 5.2 安裝應用服務
|
||||
|
||||
```bash
|
||||
# Ollama
|
||||
brew install ollama
|
||||
brew services start ollama
|
||||
|
||||
# Caddy
|
||||
brew install caddy
|
||||
brew services start caddy
|
||||
|
||||
# Gitea
|
||||
brew install gitea
|
||||
brew services start gitea
|
||||
|
||||
# PHP
|
||||
brew install php
|
||||
brew services start php
|
||||
|
||||
# n8n
|
||||
brew install n8n
|
||||
brew services start n8n
|
||||
```
|
||||
|
||||
### 5.3 Launchd 服務配置
|
||||
|
||||
```xml
|
||||
<!-- /Library/LaunchDaemons/com.momentry.postgresql.plist -->
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>Label</key>
|
||||
<string>com.momentry.postgresql</string>
|
||||
<key>UserName</key>
|
||||
<string>momentry</string>
|
||||
<key>ProgramArguments</key>
|
||||
<array>
|
||||
<string>/opt/homebrew/opt/postgresql@18/bin/postgres</string>
|
||||
<string>-D</string>
|
||||
<string>/Volumes/Momentry/momentry/var/postgresql</string>
|
||||
</array>
|
||||
<key>RunAtLoad</key>
|
||||
<true/>
|
||||
<key>KeepAlive</key>
|
||||
<true/>
|
||||
<key>StandardOutPath</key>
|
||||
<string>/Volumes/Momentry/momentry/log/postgresql.log</string>
|
||||
<key>StandardErrorPath</key>
|
||||
<string>/Volumes/Momentry/momentry/log/postgresql.error.log</string>
|
||||
</dict>
|
||||
</plist>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 階段六:安裝 Momentry Core
|
||||
|
||||
### 6.1 複製原始碼
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# 06_install_momentry.sh
|
||||
|
||||
# 建立 Momentry 目錄
|
||||
mkdir -p /Volumes/Momentry/momentry/{var,etc,log,scripts,backup}
|
||||
mkdir -p /Volumes/Momentry/momentry_core
|
||||
|
||||
# 複製原始碼
|
||||
rsync -av \
|
||||
--exclude 'target' \
|
||||
--exclude '.git' \
|
||||
--exclude 'node_modules' \
|
||||
/Users/accusys/momentry_core_0.1/ \
|
||||
/Volumes/Momentry/momentry_core/
|
||||
|
||||
# 編譯 Rust 專案
|
||||
cd /Volumes/Momentry/momentry_core
|
||||
cargo build --release
|
||||
```
|
||||
|
||||
### 6.2 初始化資料庫
|
||||
|
||||
```bash
|
||||
# 建立 PostgreSQL 資料庫
|
||||
psql -U postgres <<EOF
|
||||
CREATE DATABASE momentry;
|
||||
CREATE DATABASE n8n;
|
||||
CREATE DATABASE video_register;
|
||||
CREATE USER momentry WITH PASSWORD 'momentry_password';
|
||||
CREATE USER n8n WITH PASSWORD 'n8n_password';
|
||||
GRANT ALL PRIVILEGES ON DATABASE momentry TO momentry;
|
||||
GRANT ALL PRIVILEGES ON DATABASE n8n TO n8n;
|
||||
EOF
|
||||
|
||||
# 執行 migration
|
||||
cd /Volumes/Momentry/momentry_core
|
||||
sqlx migrate run
|
||||
```
|
||||
|
||||
### 6.3 配置環境變數
|
||||
|
||||
```bash
|
||||
# ~/.zshrc 或 ~/.bash_profile
|
||||
export DATABASE_URL="postgres://momentry:momentry_password@localhost:5432/momentry"
|
||||
export REDIS_URL="redis://:momentry_password@localhost:6379"
|
||||
export QDRANT_URL="http://localhost:6333"
|
||||
export MONGODB_URI="mongodb://localhost:27017/momentry"
|
||||
export MOMENTRY_OUTPUT_DIR="/Volumes/Momentry/momentry/var/output"
|
||||
export MOMENTRY_LOG_LEVEL="info"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 階段七:網路配置
|
||||
|
||||
### 7.1 設定固定 IP(可選)
|
||||
|
||||
```bash
|
||||
# 網路配置腳本
|
||||
#!/bin/bash
|
||||
# 07_configure_network.sh
|
||||
|
||||
# 取得網路介面
|
||||
INTERFACE=$(networksetup -listallnetworkservices | grep "Thunderbolt")
|
||||
|
||||
# 設定固定 IP
|
||||
networksetup -setmanual "$INTERFACE" \
|
||||
192.168.1.100 \
|
||||
255.255.255.0 \
|
||||
192.168.1.1
|
||||
|
||||
# 設定 DNS
|
||||
networksetup -setdnsservers "$INTERFACE" \
|
||||
8.8.8.8 \
|
||||
8.8.4.4
|
||||
```
|
||||
|
||||
### 7.2 配置防火牆
|
||||
|
||||
```bash
|
||||
# 開放服務端口
|
||||
# 使用 macOS Firewall 或 pfctl
|
||||
```
|
||||
|
||||
### 7.3 設定 SSH 金鑰
|
||||
|
||||
```bash
|
||||
# 產生 SSH 金鑰對
|
||||
ssh-keygen -t ed25519 -C "momentry@$(hostname)"
|
||||
|
||||
# 複製公鑰到目標主機
|
||||
ssh-copy-id momentry@target-host
|
||||
|
||||
# 主控機 SSH 配置
|
||||
# ~/.ssh/config
|
||||
Host momentry-target
|
||||
HostName 192.168.1.100
|
||||
User momentry
|
||||
IdentityFile ~/.ssh/id_ed25519
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 階段八:啟動服務
|
||||
|
||||
### 8.1 啟動順序
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# 08_start_services.sh
|
||||
|
||||
# 1. 基礎服務
|
||||
launchctl load /Library/LaunchDaemons/com.momentry.postgresql.plist
|
||||
launchctl load /Library/LaunchDaemons/com.momentry.mongodb.plist
|
||||
launchctl load /Library/LaunchDaemons/com.momentry.mariadb.plist
|
||||
launchctl load /Library/LaunchDaemons/com.momentry.redis.plist
|
||||
|
||||
sleep 10
|
||||
|
||||
# 2. 向量資料庫
|
||||
launchctl load /Library/LaunchDaemons/com.momentry.qdrant.plist
|
||||
|
||||
sleep 5
|
||||
|
||||
# 3. 應用服務
|
||||
launchctl load /Library/LaunchDaemons/com.momentry.ollama.plist
|
||||
launchctl load /Library/LaunchDaemons/com.momentry.caddy.plist
|
||||
|
||||
sleep 5
|
||||
|
||||
# 4. 其他服務
|
||||
launchctl load /Library/LaunchDaemons/com.momentry.gitea.plist
|
||||
launchctl load /Library/LaunchDaemons/com.momentry.php.plist
|
||||
launchctl load /Library/LaunchDaemons/com.momentry.n8n.main.plist
|
||||
launchctl load /Library/LaunchDaemons/com.momentry.n8n.worker.plist
|
||||
|
||||
# 5. Momentry Core
|
||||
launchctl load /Library/LaunchDaemons/com.momentry.sftpgo.plist
|
||||
launchctl load /Library/LaunchDaemons/com.momentry.rustdesk.hbbs.plist
|
||||
launchctl load /Library/LaunchDaemons/com.momentry.rustdesk.hbbr.plist
|
||||
```
|
||||
|
||||
### 8.2 驗證服務
|
||||
|
||||
```bash
|
||||
# 檢查所有服務狀態
|
||||
function check_services() {
|
||||
services=(
|
||||
"postgresql"
|
||||
"mongodb"
|
||||
"mariadb"
|
||||
"redis"
|
||||
"qdrant"
|
||||
"ollama"
|
||||
"caddy"
|
||||
"gitea"
|
||||
"php"
|
||||
"n8n"
|
||||
"sftpgo"
|
||||
)
|
||||
|
||||
for service in "${services[@]}"; do
|
||||
if launchctl list | grep "$service" | grep -q "running"; then
|
||||
echo "✅ $service: Running"
|
||||
else
|
||||
echo "❌ $service: Not running"
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
check_services
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 階段九:備份與還原
|
||||
|
||||
### 9.1 備份策略
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# 備份腳本
|
||||
|
||||
BACKUP_DIR="/Volumes/Momentry/backup/$(date +%Y%m%d)"
|
||||
|
||||
# 1. PostgreSQL 備份
|
||||
pg_dump -U momentry momentry > "$BACKUP_DIR/momentry.sql"
|
||||
pg_dump -U n8n n8n > "$BACKUP_DIR/n8n.sql"
|
||||
|
||||
# 2. MongoDB 備份
|
||||
mongodump --out "$BACKUP_DIR/mongodb"
|
||||
|
||||
# 3. Redis 備份
|
||||
redis-cli BGSAVE
|
||||
cp /Volumes/Momentry/var/redis/dump.rdb "$BACKUP_DIR/redis.rdb"
|
||||
|
||||
# 4. Qdrant 備份
|
||||
curl -X POST http://localhost:6333/collections/accusysdb/snapshots
|
||||
|
||||
# 5. 配置檔案備份
|
||||
tar -czf "$BACKUP_DIR/config.tar.gz" \
|
||||
/Volumes/Momentry/momentry/etc/
|
||||
```
|
||||
|
||||
### 9.2 自動備份 Cron
|
||||
|
||||
```bash
|
||||
# crontab -e
|
||||
0 2 * * * /Volumes/Momentry/scripts/backup.sh
|
||||
0 3 * * 0 /Volumes/Momentry/scripts/backup_full.sh
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 階段十:監控與維護
|
||||
|
||||
### 10.1 健康檢查腳本
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# health_check.sh
|
||||
|
||||
# 檢查所有服務
|
||||
check_postgresql() {
|
||||
pg_isready -q && echo "✅ PostgreSQL" || echo "❌ PostgreSQL"
|
||||
}
|
||||
|
||||
check_mongodb() {
|
||||
mongosh --eval "db.stats()" > /dev/null 2>&1 && echo "✅ MongoDB" || echo "❌ MongoDB"
|
||||
}
|
||||
|
||||
check_redis() {
|
||||
redis-cli ping > /dev/null 2>&1 && echo "✅ Redis" || echo "❌ Redis"
|
||||
}
|
||||
|
||||
check_qdrant() {
|
||||
curl -s http://localhost:6333/health && echo "✅ Qdrant" || echo "❌ Qdrant"
|
||||
}
|
||||
|
||||
check_n8n() {
|
||||
curl -s http://localhost:5678/api/v1/workflows > /dev/null 2>&1 && echo "✅ n8n" || echo "❌ n8n"
|
||||
}
|
||||
|
||||
check_momentry() {
|
||||
curl -s http://localhost:3002/api/v1/videos > /dev/null 2>&1 && echo "✅ Momentry" || echo "❌ Momentry"
|
||||
}
|
||||
```
|
||||
|
||||
### 10.2 日誌輪替
|
||||
|
||||
```bash
|
||||
# 新聞日誌配置
|
||||
/Volumes/Momentry/momentry/log/*.log {
|
||||
daily
|
||||
rotate 7
|
||||
compress
|
||||
missingok
|
||||
notifempty
|
||||
create 644 momentry staff
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 自動化腳本架構
|
||||
|
||||
### 主控腳本:部署控制器
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# deploy_controller.sh
|
||||
# 用於從主控機部署到目標主機
|
||||
|
||||
set -e
|
||||
|
||||
# 配置
|
||||
TARGET_HOST="momentry@192.168.1.100"
|
||||
TARGET_DISK="/dev/disk2"
|
||||
|
||||
# 顏色定義
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
NC='\033[0m'
|
||||
|
||||
function log_info() {
|
||||
echo -e "${GREEN}[INFO]${NC} $1"
|
||||
}
|
||||
|
||||
function log_warn() {
|
||||
echo -e "${YELLOW}[WARN]${NC} $1"
|
||||
}
|
||||
|
||||
function log_error() {
|
||||
echo -e "${RED}[ERROR]${NC} $1"
|
||||
}
|
||||
|
||||
# 階段執行
|
||||
function run_stage() {
|
||||
local stage=$1
|
||||
local script=$2
|
||||
|
||||
log_info "執行階段: $stage..."
|
||||
ssh "$TARGET_HOST" "bash /Volumes/Momentry/scripts/$script"
|
||||
|
||||
if [ $? -eq 0 ]; then
|
||||
log_info "✅ 階段完成: $stage"
|
||||
else
|
||||
log_error "❌ 階段失敗: $stage"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
# 主程序
|
||||
log_info "開始 Momentry 系統部署..."
|
||||
|
||||
# 執行各階段
|
||||
run_stage "磁碟準備" "01_prepare_disk.sh"
|
||||
run_stage "macOS 安裝" "02_install_macos.sh"
|
||||
run_stage "Homebrew 安裝" "03_install_homebrew.sh"
|
||||
run_stage "依賴安裝" "04_install_dependencies.sh"
|
||||
run_stage "服務安裝" "05_install_services.sh"
|
||||
run_stage "Momentry 安裝" "06_install_momentry.sh"
|
||||
run_stage "網路配置" "07_configure_network.sh"
|
||||
run_stage "啟動服務" "08_start_services.sh"
|
||||
|
||||
log_info "✅ 部署完成!"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 待確認事項
|
||||
|
||||
### 需要與使用者確認
|
||||
|
||||
1. **目標主機型號**
|
||||
- Intel Mac 或 Apple Silicon?
|
||||
- Thunderbolt 版本 (3/4)?
|
||||
|
||||
2. **網路配置**
|
||||
- DHCP 或固定 IP?
|
||||
- 目標 IP 網段?
|
||||
|
||||
3. **磁碟配置**
|
||||
- 分割方案 A (200G 系統 + 1.8T 資料)?
|
||||
- 分割方案 B (統一磁碟區)?
|
||||
|
||||
4. **服務需求**
|
||||
- 需要安裝全部服務?
|
||||
- 還是選擇性安裝?
|
||||
|
||||
5. **備份策略**
|
||||
- 本地備份?
|
||||
- 遠端備份?
|
||||
- 備份頻率?
|
||||
|
||||
6. **監控需求**
|
||||
- Prometheus + Grafana?
|
||||
- 簡單腳本監控?
|
||||
|
||||
---
|
||||
|
||||
## 預估時間
|
||||
|
||||
| 階段 | 預估時間 | 備註 |
|
||||
|------|---------|------|
|
||||
| 前置準備 | 30 分鐘 | 收集資訊、準備腳本 |
|
||||
| 磁碟準備 | 10 分鐘 | 分割格式化 |
|
||||
| macOS 安裝 | 30-60 分鐘 | 視 USB 速度 |
|
||||
| Homebrew 安裝 | 15 分鐘 | 下載速度 |
|
||||
| 服務安裝 | 60-90 分鐘 | 多個服務 |
|
||||
| Momentry 安裝 | 20 分鐘 | 編譯 Rust |
|
||||
| 網路配置 | 10 分鐘 | 固定 IP |
|
||||
| 服務啟動 | 15 分鐘 | 依序啟動 |
|
||||
| 驗證測試 | 30 分鐘 | 完整測試 |
|
||||
| **總計** | **3-4 小時** | 自動化後可縮短 |
|
||||
|
||||
---
|
||||
|
||||
## 風險與應對
|
||||
|
||||
| 風險 | 機率 | 影響 | 應對措施 |
|
||||
|------|------|------|---------|
|
||||
| Thunderbolt 不相容 | 低 | 高 | 準備多種驅動 |
|
||||
| macOS 安裝失敗 | 低 | 高 | 準備還原方案 |
|
||||
| 服務啟動失敗 | 中 | 中 | 日誌診斷腳本 |
|
||||
| 網路連線問題 | 中 | 中 | 有線網路備援 |
|
||||
| 儲存空間不足 | 低 | 高 | 磁碟空間檢查 |
|
||||
|
||||
---
|
||||
|
||||
## 下一步行動
|
||||
|
||||
1. ✅ 確認目標主機規格
|
||||
2. ✅ 確認 Thunderbolt NVMe 容量
|
||||
3. ✅ 確認網路配置
|
||||
4. ✅ 選擇服務清單
|
||||
5. ✅ 準備安裝腳本
|
||||
6. ✅ 測試腳本執行
|
||||
7. ✅ 正式部署
|
||||
|
||||
---
|
||||
|
||||
## 附錄
|
||||
|
||||
### A. 服務端口對照表
|
||||
|
||||
| 服務 | Port | 協議 |
|
||||
|------|------|------|
|
||||
| PostgreSQL | 5432 | TCP |
|
||||
| MongoDB | 27017 | TCP |
|
||||
| MariaDB | 3306 | TCP |
|
||||
| Redis | 6379 | TCP |
|
||||
| Qdrant API | 6333 | HTTP |
|
||||
| Qdrant gRPC | 6334 | gRPC |
|
||||
| Ollama | 11434 | HTTP |
|
||||
| Caddy HTTP | 80 | HTTP |
|
||||
| Caddy HTTPS | 443 | HTTPS |
|
||||
| Gitea | 3000 | HTTP |
|
||||
| PHP-FPM | 9000 | FastCGI |
|
||||
| n8n | 5678 | HTTP |
|
||||
| SFTPGo | 2022 | SFTP |
|
||||
| RustDesk hbbs | 21115 | TCP |
|
||||
| RustDesk hbbr | 21117 | TCP |
|
||||
| Momentry | 3002 | HTTP |
|
||||
| Prometheus | 9090 | HTTP |
|
||||
|
||||
### B. 環境變數清單
|
||||
|
||||
見 `.env` 範例檔案或 `docs/MOMENTRY_CORE_MONITORING.md`
|
||||
|
||||
### C. 疑難排解
|
||||
|
||||
見 `docs/PENDING_ISSUES.md`
|
||||
|
||||
---
|
||||
|
||||
**計劃狀態**: 📝 草稿 - 等待使用者確認後執行
|
||||
|
||||
**負責人**: OpenCode AI Assistant
|
||||
|
||||
**最後更新**: 2026-03-23
|
||||
@@ -1,674 +0,0 @@
|
||||
# Momentry Core 監控規範 (暫定)
|
||||
|
||||
| 項目 | 內容 |
|
||||
|------|------|
|
||||
| 建立者 | Warren |
|
||||
| 建立時間 | 2026-03-17 |
|
||||
| 文件版本 | V1.0 |
|
||||
|
||||
---
|
||||
|
||||
## 版本歷史
|
||||
|
||||
| 版本 | 日期 | 目的 | 操作人 | 工具/模型 |
|
||||
|------|------|------|--------|-----------|
|
||||
| V1.0 | 2026-03-17 | 創建文件 | Warren | OpenCode / MiniMax M2.5 |
|
||||
| V1.1 | 2026-03-25 | 新增可配置 Redis Key Prefix 說明 | Warren | OpenCode / GLM-5 |
|
||||
| V1.2 | 2026-03-25 | 新增 Job Worker 監控、processor_results 表 | Warren | OpenCode / GLM-5 |
|
||||
|
||||
---
|
||||
|
||||
> **⚠️ 狀態**: 此文檔為暫定版本,momentry_core 仍在開發中,待開發完成後再正式納入監控系統。
|
||||
|
||||
---
|
||||
|
||||
## 1. 概述
|
||||
|
||||
momentry_core 是 Rust 開發的數字資產管理系統,專注於視頻分析和 RAG 功能。
|
||||
|
||||
## 2. 監控架構
|
||||
|
||||
### Layer 2: Service 監控
|
||||
|
||||
| 項目 | 類型 | 檢查方式 | Port | 狀態 |
|
||||
|------|------|----------|------|------|
|
||||
| postgresql | TCP | `pg_isready` | 5432 | ✅ 運行中 |
|
||||
| redis | TCP | `redis-cli ping` | 6379 | ✅ 運行中 |
|
||||
| n8n | HTTP | `/healthz` | 5678 | ✅ 運行中 |
|
||||
| sftpgo | HTTP | `/healthz`, `/api/v2/token` | 8080 | ✅ 運行中 |
|
||||
| qdrant | HTTP | `/collections` | 6333 | ✅ 運行中 |
|
||||
| momentry_core | CLI | `momentry --version` | - | ⚠️ 待編譯 |
|
||||
|
||||
**附屬服務狀態** (非核心但相關):
|
||||
| gitea | HTTP | `/` | 3000 | ✅ 運行中 |
|
||||
| ollama | HTTP | `/api/tags` | 11434 | ✅ 運行中 |
|
||||
| caddy | HTTP | `/` | 80 | ✅ 運行中 |
|
||||
|
||||
### Layer 7: Backup 監控
|
||||
|
||||
| 項目 | 類型 | 位置 |
|
||||
|------|------|------|
|
||||
| momentry | data | `/Users/accusys/momentry/backup/momentry` |
|
||||
| sftpgo | config + db | `/Users/accusys/momentry/backup/daily/sftpgo/` |
|
||||
|
||||
---
|
||||
|
||||
## 3. Redis 監控架構
|
||||
|
||||
> **⚠️ 注意**: 從 V1.1 開始,所有 Redis Keys 都支援自定義前綴。
|
||||
> 預設前綴:生產環境為 `momentry:`,開發環境為 `momentry_dev:`
|
||||
>
|
||||
> 若使用非預設前綴,請將下方命令中的 `momentry:` 替換為實際前綴。
|
||||
|
||||
### 3.1 健康檢查
|
||||
|
||||
```bash
|
||||
# 檢查 Redis 連線
|
||||
redis-cli -a accusys ping
|
||||
|
||||
# 檢查 momentry 健康狀態
|
||||
redis-cli GET momentry:health:current
|
||||
```
|
||||
|
||||
### 3.2 Job 狀態監控
|
||||
|
||||
```bash
|
||||
# 查看運行中的 Jobs
|
||||
redis-cli SMEMBERS momentry:jobs:active
|
||||
|
||||
# 查看 Job 狀態
|
||||
redis-cli HGETALL momentry:job:5dea6618a606e7c7
|
||||
```
|
||||
|
||||
### 3.3 即時進度監控
|
||||
|
||||
```bash
|
||||
# 訂閱進度頻道
|
||||
redis-cli SUBSCRIBE momentry:progress:5dea6618a606e7c7
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. 監控腳本
|
||||
|
||||
### 4.1 健康檢查項目細節
|
||||
|
||||
基於當前系統狀態,需要監控的核心項目:
|
||||
|
||||
#### 4.1.1 資料庫服務檢查
|
||||
|
||||
```bash
|
||||
# PostgreSQL - 檢查伺服器運行與關鍵資料庫
|
||||
check_postgresql() {
|
||||
if pg_isready -h localhost -p 5432 > /dev/null 2>&1; then
|
||||
echo -e "${GREEN}✓${NC} PostgreSQL"
|
||||
# 檢查關鍵資料庫是否存在
|
||||
local missing_dbs=()
|
||||
for db in momentry video_register n8n sftpgo; do
|
||||
if ! psql -h localhost -U postgres -lqt 2>/dev/null | cut -d\| -f1 | grep -qw "$db"; then
|
||||
missing_dbs+=("$db")
|
||||
fi
|
||||
done
|
||||
if [ ${#missing_dbs[@]} -gt 0 ]; then
|
||||
echo -e " ${YELLOW}⚠${NC} 缺失資料庫: ${missing_dbs[*]}"
|
||||
record_service "postgresql" "warning" "1" "Missing databases: ${missing_dbs[*]}"
|
||||
else
|
||||
record_service "postgresql" "up" "1" ""
|
||||
fi
|
||||
else
|
||||
echo -e "${RED}✗${NC} PostgreSQL"
|
||||
record_service "postgresql" "down" "0" "PostgreSQL not responding"
|
||||
fi
|
||||
}
|
||||
```
|
||||
|
||||
#### 4.1.2 Redis 檢查
|
||||
|
||||
```bash
|
||||
check_redis() {
|
||||
if redis-cli -a "$REDIS_PASSWORD" ping 2>/dev/null | grep -q PONG; then
|
||||
echo -e "${GREEN}✓${NC} Redis"
|
||||
record_service "redis" "up" "1" ""
|
||||
else
|
||||
echo -e "${RED}✗${NC} Redis"
|
||||
record_service "redis" "down" "0" "Redis not responding"
|
||||
fi
|
||||
}
|
||||
```
|
||||
|
||||
#### 4.1.3 SFTPGo 檢查
|
||||
|
||||
```bash
|
||||
check_sftpgo() {
|
||||
# 1. 檢查 HTTP API
|
||||
if curl -s http://localhost:8080/healthz > /dev/null 2>&1; then
|
||||
# 2. 檢查 API 認證
|
||||
if TOKEN=$(curl -s -X GET http://localhost:8080/api/v2/token -u "admin:$SFTPGO_ADMIN_PASSWORD" 2>/dev/null | jq -r '.access_token') && [ -n "$TOKEN" ]; then
|
||||
echo -e "${GREEN}✓${NC} SFTPGo"
|
||||
record_service "sftpgo" "up" "1" ""
|
||||
else
|
||||
echo -e "${YELLOW}⚠${NC} SFTPGo (API認證失敗)"
|
||||
record_service "sftpgo" "warning" "1" "API authentication failed"
|
||||
fi
|
||||
else
|
||||
echo -e "${RED}✗${NC} SFTPGo"
|
||||
record_service "sftpgo" "down" "0" "HTTP API not responding"
|
||||
fi
|
||||
}
|
||||
```
|
||||
|
||||
#### 4.1.4 n8n 檢查
|
||||
|
||||
```bash
|
||||
check_n8n() {
|
||||
if curl -s http://localhost:5678/healthz > /dev/null 2>&1; then
|
||||
echo -e "${GREEN}✓${NC} n8n"
|
||||
record_service "n8n" "up" "1" ""
|
||||
else
|
||||
echo -e "${RED}✗${NC} n8n"
|
||||
record_service "n8n" "down" "0" "API not responding"
|
||||
fi
|
||||
}
|
||||
```
|
||||
|
||||
#### 4.1.5 Qdrant 檢查
|
||||
|
||||
```bash
|
||||
check_qdrant() {
|
||||
if curl -s http://localhost:6333/collections > /dev/null 2>&1; then
|
||||
echo -e "${GREEN}✓${NC} Qdrant"
|
||||
record_service "qdrant" "up" "1" ""
|
||||
else
|
||||
echo -e "${RED}✗${NC} Qdrant"
|
||||
record_service "qdrant" "down" "0" "API not responding"
|
||||
fi
|
||||
}
|
||||
```
|
||||
|
||||
#### 4.1.6 Momentry Core 檢查
|
||||
|
||||
```bash
|
||||
check_momentry_core() {
|
||||
local binary="/Users/accusys/momentry/target/release/momentry"
|
||||
if [ ! -f "$binary" ]; then
|
||||
binary="/Users/accusys/momentry/target/debug/momentry"
|
||||
fi
|
||||
|
||||
if [ -f "$binary" ] && $binary --version > /dev/null 2>&1; then
|
||||
echo -e "${GREEN}✓${NC} Momentry Core"
|
||||
record_service "momentry_core" "up" "1" ""
|
||||
else
|
||||
echo -e "${RED}✗${NC} Momentry Core"
|
||||
record_service "momentry_core" "down" "0" "Binary not found or not executable"
|
||||
fi
|
||||
}
|
||||
```
|
||||
|
||||
### 4.2 檢查腳本範例完整實現
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# monitor/service/health_check.sh
|
||||
|
||||
export REDIS_PASSWORD="accusys"
|
||||
export SFTPGO_ADMIN_PASSWORD="Test3200Test3200"
|
||||
|
||||
check_postgresql
|
||||
check_redis
|
||||
check_sftpgo
|
||||
check_n8n
|
||||
check_qdrant
|
||||
check_momentry_core
|
||||
```
|
||||
|
||||
### 4.2 Redis Job 監控腳本
|
||||
|
||||
**檔案**: `monitor/service/redis_job_monitor.sh`
|
||||
|
||||
此腳本專門監控 Redis 中的 Job 狀態與即時進度:
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# Momentry Core Redis Job 監控
|
||||
|
||||
REDIS_PASSWORD="${REDIS_PASSWORD:-accusys}"
|
||||
REDIS_PREFIX="${REDIS_PREFIX:-momentry:}"# 可配置前綴,預設 momentry:
|
||||
|
||||
# 檢查 Job 狀態
|
||||
check_job_status() {
|
||||
local active_jobs=$(redis-cli -a "$REDIS_PASSWORD" SCARD "${REDIS_PREFIX}jobs:active" 2>/dev/null || echo "0")
|
||||
local completed_jobs=$(redis-cli -a "$REDIS_PASSWORD" SCARD "${REDIS_PREFIX}jobs:completed" 2>/dev/null || echo "0")
|
||||
local failed_jobs=$(redis-cli -a "$REDIS_PASSWORD" SCARD "${REDIS_PREFIX}jobs:failed" 2>/dev/null || echo "0")
|
||||
|
||||
echo "Momentry Jobs: Active=$active_jobs, Completed=$completed_jobs, Failed=$failed_jobs"
|
||||
echo "$active_jobs $completed_jobs $failed_jobs"
|
||||
}
|
||||
|
||||
# 檢查特定 Job 進度
|
||||
check_job_progress() {
|
||||
local job_uuid=$1
|
||||
if [ -z "$job_uuid" ]; then
|
||||
echo "Usage: $0 progress <uuid>"
|
||||
return 1
|
||||
fi
|
||||
|
||||
local progress=$(redis-cli -a "$REDIS_PASSWORD" HGETALL "${REDIS_PREFIX}job:$job_uuid" 2>/dev/null)
|
||||
if [ -n "$progress" ]; then
|
||||
echo "Job $job_uuid progress:"
|
||||
echo "$progress" | while read -r key value; do
|
||||
[ -n "$key" ] && echo " $key: $value"
|
||||
done
|
||||
else
|
||||
echo "No progress data for job $job_uuid"
|
||||
fi
|
||||
}
|
||||
|
||||
# 訂閱即時進度頻道
|
||||
subscribe_progress() {
|
||||
local job_uuid=$1
|
||||
if [ -z "$job_uuid" ]; then
|
||||
echo "Subscribing to all progress channels..."
|
||||
redis-cli -a "$REDIS_PASSWORD" PSUBSCRIBE "${REDIS_PREFIX}progress:*" 2>/dev/null
|
||||
else
|
||||
echo "Subscribing to job $job_uuid progress..."
|
||||
redis-cli -a "$REDIS_PASSWORD" SUBSCRIBE "${REDIS_PREFIX}progress:$job_uuid" 2>/dev/null
|
||||
fi
|
||||
}
|
||||
|
||||
# 列出所有活動 Job
|
||||
list_active_jobs() {
|
||||
echo "Active Jobs:"
|
||||
redis-cli -a "$REDIS_PASSWORD" SMEMBERS "${REDIS_PREFIX}jobs:active" 2>/dev/null | while read -r uuid; do
|
||||
[ -n "$uuid" ] && echo " - $uuid"
|
||||
done
|
||||
}
|
||||
|
||||
# 主程序
|
||||
case "$1" in
|
||||
status)
|
||||
check_job_status
|
||||
;;
|
||||
progress)
|
||||
check_job_progress "$2"
|
||||
;;
|
||||
subscribe)
|
||||
subscribe_progress "$2"
|
||||
;;
|
||||
list)
|
||||
list_active_jobs
|
||||
;;
|
||||
*)
|
||||
echo "Usage: $0 {status|progress <uuid>|subscribe [uuid]|list}"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
```
|
||||
|
||||
### 4.3 監控腳本排程
|
||||
|
||||
使用 cron 定期執行健康檢查:
|
||||
|
||||
```bash
|
||||
# crontab -e
|
||||
# 每 5 分鐘執行一次健康檢查
|
||||
*/5 * * * * /Users/accusys/momentry/scripts/health_check.sh >> /Users/accusys/momentry/log/health_check.log 2>&1
|
||||
```
|
||||
|
||||
### 4.4 監控數據記錄
|
||||
|
||||
健康檢查結果應寫入監控數據庫,建議的 `monitor_services` 表結構:
|
||||
|
||||
```sql
|
||||
CREATE TABLE monitor_services (
|
||||
id SERIAL PRIMARY KEY,
|
||||
service_name VARCHAR(50) NOT NULL,
|
||||
status VARCHAR(20) NOT NULL, -- up, down, warning
|
||||
error_message TEXT,
|
||||
checked_at TIMESTAMP DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX idx_monitor_services_checked ON monitor_services(checked_at);
|
||||
CREATE INDEX idx_monitor_services_name ON monitor_services(service_name);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. 配置更新
|
||||
|
||||
### 5.1 環境變數
|
||||
|
||||
必需監控相關環境變數配置在 `.env` 或系統中:
|
||||
|
||||
```bash
|
||||
# Redis 連接
|
||||
REDIS_URL=redis://localhost:6379
|
||||
REDIS_PASSWORD=accusys
|
||||
|
||||
# Redis Key Prefix (可選,預設: momentry:)
|
||||
REDIS_PREFIX=momentry:
|
||||
|
||||
# SFTPGo 管理員密碼 (用於 API 健康檢查)
|
||||
SFTPGO_ADMIN_PASSWORD=Test3200Test3200
|
||||
|
||||
# Momentry Core 路徑 (可選,如果不在標準位置)
|
||||
MOMENTRY_BINARY_PATH="/Users/accusys/momentry/target/release/momentry"
|
||||
```
|
||||
|
||||
### 5.2 監控配置範例
|
||||
|
||||
**檔案**: `monitor/config/monitor_config.yaml`
|
||||
|
||||
```yaml
|
||||
services:
|
||||
# Redis - 消息隊列與狀態存儲
|
||||
- name: "redis"
|
||||
type: "tcp"
|
||||
host: "localhost"
|
||||
port: 6379
|
||||
timeout: 3
|
||||
enabled: true
|
||||
|
||||
# PostgreSQL - 數據庫
|
||||
- name: "postgresql"
|
||||
type: "tcp"
|
||||
host: "localhost"
|
||||
port: 5432
|
||||
timeout: 5
|
||||
enabled: true
|
||||
check_sql: "SELECT 1;" # 可選:執行 SQL 驗證
|
||||
|
||||
# n8n - 工作流引擎
|
||||
- name: "n8n"
|
||||
type: "http"
|
||||
host: "localhost"
|
||||
port: 5678
|
||||
check_url: "http://localhost:5678/healthz"
|
||||
timeout: 5
|
||||
enabled: true
|
||||
|
||||
# SFTPGo - 文件上傳服務
|
||||
- name: "sftpgo"
|
||||
type: "http"
|
||||
host: "localhost"
|
||||
port: 8080
|
||||
check_url: "http://localhost:8080/healthz"
|
||||
timeout: 5
|
||||
enabled: true
|
||||
# 附加認證檢查(每小時一次)
|
||||
auth_check:
|
||||
endpoint: "/api/v2/token"
|
||||
method: "GET"
|
||||
username: "admin"
|
||||
password_env: "SFTPGO_ADMIN_PASSWORD"
|
||||
|
||||
# Qdrant - 向量數據庫
|
||||
- name: "qdrant"
|
||||
type: "http"
|
||||
host: "localhost"
|
||||
port: 6333
|
||||
check_url: "http://localhost:6333/collections"
|
||||
timeout: 5
|
||||
enabled: true
|
||||
|
||||
# Momentry Core CLI
|
||||
- name: "momentry_core"
|
||||
type: "cli"
|
||||
binary_path: "/Users/accusys/momentry/target/release/momentry"
|
||||
args: ["--version"]
|
||||
timeout: 3
|
||||
enabled: true
|
||||
```
|
||||
|
||||
### 5.3 備份配置更新
|
||||
|
||||
統一備份系統 (`backup_all.sh`) 已包含 SFTPGo 備份,無需額外配置。
|
||||
|
||||
備份保留策略 (預設):
|
||||
- **每日備份**: 保留 7 天
|
||||
- **每週備份**: 保留 4 週
|
||||
- **每月備份**: 保留 12 個月
|
||||
|
||||
備份存儲位置:
|
||||
- 配置文件: `/Users/accusys/momentry/backup/daily/<service>/`
|
||||
- 最新備份鏈接: `/Users/accusys/momentry/backup/latest/` (由 backup_monitor.sh 管理)
|
||||
|
||||
### 5.4 監控腳本配置
|
||||
|
||||
建立符號鏈接到監控腳本目錄:
|
||||
|
||||
```bash
|
||||
ln -sf /Users/accusys/momentry/scripts/backup_all.sh /usr/local/bin/backup_all
|
||||
ln -sf /Users/accusys/momentry/scripts/health_check.sh /usr/local/bin/health_check
|
||||
```
|
||||
|
||||
這樣可以在任何地方執行:
|
||||
```bash
|
||||
health_check
|
||||
backup_all status
|
||||
backup_all restore sftpgo 20260321_101928
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. 報警規則
|
||||
|
||||
| 層級 | 異常類型 | 等級 | 處理 |
|
||||
|------|----------|------|------|
|
||||
| Service | PostgreSQL 未運行 | Critical | 記錄 + 通知 |
|
||||
| Service | Redis 未運行 | Critical | 記錄 + 通知 |
|
||||
| Service | n8n API 無回應 | Critical | 記錄 + 通知 |
|
||||
| Service | SFTPGo API 無回應 | Critical | 記錄 + 通知 |
|
||||
| Service | Qdrant 未運行 | Critical | 記錄 + 通知 |
|
||||
| Service | Momentry CLI 缺失 | Critical | 記錄 + 通知 |
|
||||
| Database | 關鍵資料庫不存在 | Warning | 記錄 |
|
||||
| Backup | 備份失敗 | Critical | 記錄 |
|
||||
| Backup | 備份過期 | Warning | 記錄 |
|
||||
| Job | 處理失敗 | Warning | 記錄 |
|
||||
| Job | 處理超時 | Warning | 記錄 |
|
||||
|
||||
### 6.1 資料庫缺失處理
|
||||
|
||||
當檢查到以下資料庫不存在時,應記錄警告:
|
||||
- `momentry` - Momentry Core 主資料庫
|
||||
- `video_register` - 視頻註冊資料庫
|
||||
- `n8n` - n8n 工作流資料庫
|
||||
- `sftpgo` - SFTPGo 資料庫
|
||||
|
||||
**處理程序**:
|
||||
1. 確認是否為首次安裝(資料庫尚未建立)
|
||||
2. 檢查備份並執行還原
|
||||
3. 如果是意外刪除,立即從最新備份恢復
|
||||
|
||||
### 6.2 SFTPGo 監控特殊注意
|
||||
|
||||
SFTPGo 使用雙重認證檢查:
|
||||
1. **健康檢查**: `/healthz` (無需認證)
|
||||
2. **API 可用性**: `/api/v2/token` (需要 Basic Auth)
|
||||
|
||||
若 `/healthz` 正常但 `/api/v2/token` 失敗,可能是:
|
||||
- admin 密碼被重置
|
||||
- 數據庫連接問題
|
||||
- API 配置錯誤
|
||||
|
||||
應立即檢查:
|
||||
```bash
|
||||
tail -20 /Users/accusys/momentry/log/sftpgo.log
|
||||
tail -20 /Users/accusys/momentry/log/sftpgo.error.log
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. 數據庫記錄
|
||||
|
||||
### monitor_jobs 表
|
||||
|
||||
```sql
|
||||
CREATE TABLE monitor_jobs (
|
||||
id SERIAL PRIMARY KEY,
|
||||
uuid VARCHAR(16) NOT NULL,
|
||||
video_path VARCHAR(512),
|
||||
status VARCHAR(20),
|
||||
current_processor VARCHAR(20),
|
||||
progress_total INT,
|
||||
progress_current INT,
|
||||
error_count INT DEFAULT 0,
|
||||
last_error TEXT,
|
||||
started_at TIMESTAMP,
|
||||
updated_at TIMESTAMP,
|
||||
created_at TIMESTAMP DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX idx_monitor_jobs_uuid ON monitor_jobs(uuid);
|
||||
CREATE INDEX idx_monitor_jobs_status ON monitor_jobs(status);
|
||||
CREATE INDEX idx_monitor_jobs_created_at ON monitor_jobs(created_at);
|
||||
```
|
||||
|
||||
### processor_results 表
|
||||
|
||||
```sql
|
||||
CREATE TABLE processor_results (
|
||||
id SERIAL PRIMARY KEY,
|
||||
job_id INTEGER REFERENCES monitor_jobs(id) ON DELETE CASCADE,
|
||||
video_id BIGINT REFERENCES videos(id),
|
||||
processor VARCHAR(20) NOT NULL,
|
||||
status VARCHAR(20) NOT NULL DEFAULT 'pending',
|
||||
started_at TIMESTAMP,
|
||||
completed_at TIMESTAMP,
|
||||
error_message TEXT,
|
||||
created_at TIMESTAMP DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX idx_processor_results_job ON processor_results(job_id);
|
||||
CREATE INDEX idx_processor_results_video ON processor_results(video_id);
|
||||
CREATE INDEX idx_processor_results_status ON processor_results(status);
|
||||
```
|
||||
|
||||
**processor 狀態值**:
|
||||
- `pending` - 等待處理
|
||||
- `running` - 處理中
|
||||
- `completed` - 已完成
|
||||
- `failed` - 處理失敗
|
||||
- `skipped` - 跳過(已在其他處理中完成)
|
||||
|
||||
---
|
||||
|
||||
## 8. 環境變數
|
||||
|
||||
```bash
|
||||
# 輸出目錄
|
||||
MOMENTRY_OUTPUT_DIR=/path/to/output
|
||||
|
||||
# 備份
|
||||
MOMENTRY_BACKUP_ENABLED=true
|
||||
MOMENTRY_BACKUP_DIR=/Users/accusys/momentry/backup/momentry
|
||||
|
||||
# Redis
|
||||
REDIS_URL=redis://localhost:6379
|
||||
REDIS_PASSWORD=accusys
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 9. 待實作項目
|
||||
|
||||
| # | 項目 | 狀態 | 更新日期 |
|
||||
|---|------|------|---------|
|
||||
| 1 | 實作 Redis Pub/Sub 客戶端 | ✅ 已實作 | 2026-03-21 |
|
||||
| 2 | 修改 Python processors 使用 Redis | ✅ 已實作 | 2026-03-21 |
|
||||
| 3 | 設計並實現 health_check.sh | ✅ 已實作 | 2026-03-22 |
|
||||
| 4 | 創建 monitor_jobs 表 | ✅ 已實作 | 2026-03-21 |
|
||||
| 5 | SFTPGo 備份與還原流程 | ✅ 已實作 | 2026-03-22 |
|
||||
| 6 | SFTPGo API 管理工具 | ✅ 已實作 | 2026-03-22 |
|
||||
| 7 | SFTPGo Hook 自動註冊 | ✅ 已實作 | 2026-03-22 |
|
||||
| 8 | 文檔化監控規範 | ✅ 已實作 | 2026-03-22 |
|
||||
| 9 | Job Worker 輪詢機制 | ✅ 已實作 | 2026-03-25 |
|
||||
| 10 | processor_results 表 | ✅ 已實作 | 2026-03-25 |
|
||||
| 11 | Probe API 端點 | ✅ 已實作 | 2026-03-25 |
|
||||
| 12 | 整合測試 | 🔜 待實作 | - |
|
||||
| 13 | 生產環境部署驗證 | ⏳ 待開始 | - |
|
||||
|
||||
### Job Worker 監控 (2026-03-25 新增)
|
||||
|
||||
**Worker 服務狀態檢查**:
|
||||
```bash
|
||||
# 檢查 Worker 程序
|
||||
ps aux | grep momentry
|
||||
|
||||
# 查看 Worker 日誌
|
||||
tail -f /Users/accusys/momentry/log/worker.log
|
||||
```
|
||||
|
||||
**monitor_jobs 狀態查詢**:
|
||||
```bash
|
||||
# 查看待處理工作
|
||||
psql -U accusys -d momentry -c "SELECT * FROM monitor_jobs WHERE status = 'pending';"
|
||||
|
||||
# 查看執行中工作
|
||||
psql -U accusys -d momentry -c "SELECT * FROM monitor_jobs WHERE status = 'running';"
|
||||
|
||||
# 查看失敗工作
|
||||
psql -U accusys -d momentry -c "SELECT * FROM monitor_jobs WHERE status = 'failed';"
|
||||
```
|
||||
|
||||
**processor_results 狀態查詢**:
|
||||
```bash
|
||||
# 查看特定工作的處理器狀態
|
||||
psql -U accusys -d momentry -c "
|
||||
SELECT pr.*, mj.uuid
|
||||
FROM processor_results pr
|
||||
JOIN monitor_jobs mj ON pr.job_id = mj.id
|
||||
WHERE mj.uuid = 'a1b10138a6bbb0cd';
|
||||
"
|
||||
|
||||
# 查看所有失敗的處理器
|
||||
psql -U accusys -d momentry -c "
|
||||
SELECT pr.processor, COUNT(*) as failures
|
||||
FROM processor_results pr
|
||||
WHERE pr.status = 'failed'
|
||||
GROUP BY pr.processor;
|
||||
"
|
||||
```
|
||||
|
||||
**Redis 工作狀態**:
|
||||
```bash
|
||||
# 查看活躍工作
|
||||
redis-cli SMEMBERS momentry:jobs:active
|
||||
|
||||
# 查看工作詳情
|
||||
redis-cli HGETALL momentry:job:{uuid}
|
||||
```
|
||||
|
||||
### 已完成實作 (2026-03-22)
|
||||
|
||||
**監控系統**:
|
||||
- 完整健康檢查腳本設計: `docs/MOMENTRY_CORE_MONITORING.md`
|
||||
- 多層次服務監控 (Layer 2: Service, Layer 7: Backup)
|
||||
- Redis Job 監控腳本: `monitor/service/redis_job_monitor.sh`
|
||||
- SFTPGo 特殊監控 (API 認證檢查)
|
||||
|
||||
**SFTPGo 管理**:
|
||||
- 備份還原機制: `backup_all.sh` (第 325-546 行)
|
||||
- API 管理用戶與組 (完整文件於 `docs/INSTALL_SFTPGO.md`)
|
||||
- Hook 自動註冊流程: `/Users/accusys/sftpgo_test/register_hook.sh`
|
||||
- Demo 用戶與組完整測試環境
|
||||
|
||||
**文檔更新**:
|
||||
- `docs/INSTALL_SFTPGO.md`: 新增備份還原、API管理、Hook配置章節
|
||||
- `docs/MOMENTRY_CORE_MONITORING.md`: 完善監控規範
|
||||
|
||||
### 待驗證功能
|
||||
|
||||
- [ ] 端到端測試: SFTP 上傳 → Hook → Momentry 註冊 → n8n 工作流
|
||||
- [ ] Momentry Core API 搜索功能: `GET /api/v1/searchable`
|
||||
- [ ] 背景處理自動觸發: `cargo run -- process <uuid>`
|
||||
|
||||
---
|
||||
|
||||
## 10. 參考文檔
|
||||
|
||||
- [Redis Key 設計規範](./MOMENTRY_CORE_REDIS_KEYS.md)
|
||||
- [監控系統總覽](../monitor/MONITORING.md)
|
||||
- [備份規範](./SERVICE_ADDITION_GUIDE.md)
|
||||
- [SFTPGo 安裝與管理指南](./INSTALL_SFTPGO.md)
|
||||
- [API 參考文件](../docs/API_REFERENCE.md)
|
||||
- [n8n 整合指南](./N8N_INTEGRATION_GUIDE.md)
|
||||
@@ -1,283 +0,0 @@
|
||||
# Momentry Core Redis Key 設計規範
|
||||
|
||||
| 項目 | 內容 |
|
||||
|------|------|
|
||||
| 建立者 | Warren |
|
||||
| 建立時間 | 2026-03-17 |
|
||||
| 文件版本 | V1.0 |
|
||||
|
||||
---
|
||||
|
||||
## 版本歷史
|
||||
|
||||
| 版本 | 日期 | 目的 | 操作人 | 工具/模型 |
|
||||
|------|------|------|--------|-----------|
|
||||
| V1.0 | 2026-03-17 | 創建文件 | Warren | OpenCode / MiniMax M2.5 |
|
||||
| V1.1 | 2026-03-25 | 新增可配置 Redis Key Prefix | Warren | OpenCode / GLM-5 |
|
||||
|
||||
---
|
||||
|
||||
## 1. 概述
|
||||
|
||||
本文檔說明 momentry_core 如何使用 Redis 作為監控和狀態管理系統。
|
||||
|
||||
## 2. 可配置 Redis Key Prefix
|
||||
|
||||
### 2.1 環境變數
|
||||
|
||||
從 V1.1 開始,所有 Redis Keys 都支援自定義前綴:
|
||||
|
||||
```bash
|
||||
MOMENTRY_REDIS_PREFIX=momentry:
|
||||
```
|
||||
|
||||
此設定允許多個 momentry 實例共用同一個 Redis 伺服器,例如:
|
||||
- **生產環境**: `MOMENTRY_REDIS_PREFIX=momentry:`
|
||||
- **開發環境**: `MOMENTRY_REDIS_PREFIX=momentry_dev:`
|
||||
|
||||
### 2.2 Key 格式
|
||||
|
||||
所有 Key 都遵循以下格式:
|
||||
|
||||
```
|
||||
{prefix}{key_type}:{uuid}
|
||||
```
|
||||
|
||||
範例 (生產環境):
|
||||
```
|
||||
momentry:job:5dea6618a606e7c7
|
||||
momentry:jobs:active
|
||||
momentry:health:current
|
||||
```
|
||||
|
||||
範例 (開發環境):
|
||||
```
|
||||
momentry_dev:job:5dea6618a606e7c7
|
||||
momentry_dev:jobs:active
|
||||
momentry_dev:health:current
|
||||
```
|
||||
|
||||
### 2.3 預設值
|
||||
|
||||
| Binary | 預設 Port | 預設 Redis Prefix |
|
||||
|--------|-----------|-------------------|
|
||||
| `momentry` (生產) | 3002 | `momentry:` |
|
||||
| `momentry_playground` (開發) | 3003 | `momentry_dev:` |
|
||||
|
||||
## 3. UUID 使用時機
|
||||
|
||||
### 3.1 全局 Keys(無 UUID)
|
||||
|
||||
- 單一實例全局狀態
|
||||
- 聚合統計數據
|
||||
|
||||
### 3.2 Per-Video Keys(UUID 必要)
|
||||
|
||||
- 每個視頻獨立處理狀態
|
||||
- 即時進度追蹤
|
||||
|
||||
### 3.3 Per-Processor Keys(UUID + Processor 必要)
|
||||
|
||||
- 每個 processor 獨立狀態
|
||||
|
||||
## 4. Key 命名空間
|
||||
|
||||
```
|
||||
momentry
|
||||
├── health: # 健康檢查
|
||||
│ ├── current # 當前狀態 (TTL: 60s)
|
||||
│ └── services # 依賴服務狀態
|
||||
├── config: # 配置
|
||||
├── stats: # 聚合統計
|
||||
│ ├── total_jobs # 總 Jobs 數
|
||||
│ ├── processed_today # 今日處理數
|
||||
│ ├── cpu:current # 當前 CPU 使用
|
||||
│ └── memory:current # 當前 Memory 使用
|
||||
├── jobs: # Jobs 管理
|
||||
│ ├── active # Set: 運行中 UUIDs
|
||||
│ ├── completed # Set: 完成 UUIDs
|
||||
│ └── failed # Set: 失敗 UUIDs
|
||||
├── job:{uuid} # Per-Video Job 狀態 (TTL: 24h)
|
||||
│ ├── status # 狀態 String
|
||||
│ ├── video_path # 視頻路徑
|
||||
│ ├── current_processor # 當前 processor
|
||||
│ ├── progress_total # 總進度
|
||||
│ ├── progress_current # 當前進度
|
||||
│ ├── started_at # 開始時間
|
||||
│ ├── updated_at # 最後更新
|
||||
│ └── processor:{name} # Per-Processor 狀態
|
||||
│ ├── status
|
||||
│ ├── progress
|
||||
│ ├── current
|
||||
│ ├── total
|
||||
│ └── started_at
|
||||
├── progress:{uuid} # Pub/Sub 頻道 (即時進度)
|
||||
├── result:{uuid} # 處理結果 Hash
|
||||
├── output:{uuid} # 輸出路徑
|
||||
├── metrics:{uuid} # Per-Video 指標
|
||||
│ ├── cpu # CPU 歷史 List (100條, TTL: 1h)
|
||||
│ ├── memory # Memory 歷史 List (100條, TTL: 1h)
|
||||
│ └── duration # 處理時長
|
||||
└── log:{uuid} # 處理日誌 String
|
||||
```
|
||||
|
||||
## 5. Key 詳細說明
|
||||
|
||||
### 全局 Keys
|
||||
|
||||
| Key | Type | TTL | 說明 |
|
||||
|-----|------|-----|------|
|
||||
| `momentry:health:current` | String | 60s | 當前健康狀態 |
|
||||
| `momentry:health:services` | Hash | 60s | 依賴服務健康狀態 |
|
||||
| `momentry:stats:total_jobs` | String | - | 總 Jobs 數 |
|
||||
| `momentry:stats:processed_today` | String | 86400s | 今日處理數 |
|
||||
| `momentry:stats:cpu:current` | String | 10s | 當前 CPU 使用 |
|
||||
| `momentry:stats:memory:current` | String | 10s | 當前 Memory 使用 |
|
||||
| `momentry:jobs:active` | Set | - | 運行中 Job UUIDs |
|
||||
| `momentry:jobs:completed` | Set | - | 完成 Job UUIDs |
|
||||
| `momentry:jobs:failed` | Set | - | 失敗 Job UUIDs |
|
||||
|
||||
### Per-Video Keys
|
||||
|
||||
| Key | Type | TTL | 說明 |
|
||||
|-----|------|-----|------|
|
||||
| `momentry:job:{uuid}` | Hash | 24h | Job 完整狀態 |
|
||||
| `momentry:job:{uuid}:status` | String | 24h | Job 狀態 |
|
||||
| `momentry:progress:{uuid}` | Pub/Sub | - | 即時進度頻道 |
|
||||
| `momentry:result:{uuid}` | Hash | 24h | 處理結果 |
|
||||
| `momentry:output:{uuid}` | String | 24h | 輸出路徑 |
|
||||
| `momentry:metrics:{uuid}:cpu` | List | 1h | CPU 歷史 (100條) |
|
||||
| `momentry:metrics:{uuid}:memory` | List | 1h | Memory 歷史 (100條) |
|
||||
| `momentry:metrics:{uuid}:duration` | String | 24h | 處理時長 |
|
||||
| `momentry:log:{uuid}` | String | 24h | 處理日誌 |
|
||||
|
||||
### Per-Processor Keys
|
||||
|
||||
| Key | Type | TTL | 說明 |
|
||||
|-----|------|-----|------|
|
||||
| `momentry:job:{uuid}:processor:{name}` | Hash | 24h | Processor 狀態 |
|
||||
| `momentry:job:{uuid}:processor:{name}:status` | String | 24h | 狀態 |
|
||||
| `momentry:job:{uuid}:processor:{name}:progress` | String | 24h | 進度百分比 |
|
||||
| `momentry:job:{uuid}:processor:{name}:current` | String | 24h | 當前項目 |
|
||||
| `momentry:job:{uuid}:processor:{name}:total` | String | 24h | 總項目數 |
|
||||
| `momentry:job:{uuid}:processor:{name}:started_at` | String | 24h | 開始時間 |
|
||||
|
||||
## 6. TTL 策略
|
||||
|
||||
| Key 類型 | TTL | 原因 |
|
||||
|----------|-----|------|
|
||||
| Health | 60s | 需要定期更新 |
|
||||
| Job | 24h | 處理完成後保留一天 |
|
||||
| Processor | 24h | 處理完成後保留一天 |
|
||||
| Metrics | 1h | 只保留近期歷史 |
|
||||
| Progress Pub/Sub | - | 不持久,僅即時訊息 |
|
||||
| Stats | 無 | 持久統計 |
|
||||
|
||||
## 7. 訊息格式
|
||||
|
||||
### Pub/Sub 訊息 (progress:{uuid})
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "info | progress | complete | error",
|
||||
"processor": "yolo | ocr | face | pose | cut | asr | asrx",
|
||||
"timestamp": 1700000000,
|
||||
"data": {
|
||||
"message": "Processing frame 5000",
|
||||
"current": 5000,
|
||||
"total": 14315
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Job 狀態 Hash
|
||||
|
||||
```json
|
||||
{
|
||||
"uuid": "5dea6618a606e7c7",
|
||||
"video_path": "/path/to/video.mp4",
|
||||
"status": "running",
|
||||
"current_processor": "yolo",
|
||||
"progress_total": 70,
|
||||
"progress_current": 50,
|
||||
"started_at": 1700000000,
|
||||
"updated_at": 1700000100,
|
||||
"error_count": 0,
|
||||
"last_error": ""
|
||||
}
|
||||
```
|
||||
|
||||
### Processor 狀態 Hash
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "yolo",
|
||||
"status": "running",
|
||||
"progress": 70,
|
||||
"current_frame": 10000,
|
||||
"total_frames": 14315,
|
||||
"started_at": 1700000000,
|
||||
"updated_at": 1700000100
|
||||
}
|
||||
```
|
||||
|
||||
## 8. 實作函數 (Rust)
|
||||
|
||||
所有 Redis Key 生成函數使用 `REDIS_KEY_PREFIX` 靜態變數:
|
||||
|
||||
```rust
|
||||
use crate::core::config::REDIS_KEY_PREFIX;
|
||||
|
||||
fn global_key(key: &str) -> String {
|
||||
format!("{}{}", REDIS_KEY_PREFIX, key)
|
||||
}
|
||||
|
||||
fn job_key(uuid: &str) -> String {
|
||||
format!("{}job:{}", REDIS_KEY_PREFIX, uuid)
|
||||
}
|
||||
|
||||
fn processor_key(uuid: &str, processor: &str) -> String {
|
||||
format!("{}job:{}:processor:{}", REDIS_KEY_PREFIX, uuid, processor)
|
||||
}
|
||||
|
||||
fn progress_channel(uuid: &str) -> String {
|
||||
format!("{}progress:{}", REDIS_KEY_PREFIX, uuid)
|
||||
}
|
||||
|
||||
fn metrics_key(uuid: &str, metric: &str) -> String {
|
||||
format!("{}metrics:{}:{}", REDIS_KEY_PREFIX, uuid, metric)
|
||||
}
|
||||
|
||||
fn jobs_set_key(status: &str) -> String {
|
||||
format!("{}jobs:{}", REDIS_KEY_PREFIX, status)
|
||||
}
|
||||
```
|
||||
|
||||
**注意**: `REDIS_KEY_PREFIX` 定義於 `src/core/config.rs`,由環境變數 `MOMENTRY_REDIS_PREFIX` 控制。
|
||||
|
||||
## 9. 環境變數
|
||||
|
||||
```bash
|
||||
# Redis 連接
|
||||
REDIS_URL=redis://localhost:6379
|
||||
REDIS_PASSWORD=accusys
|
||||
REDIS_DB=0
|
||||
|
||||
# Redis Key Prefix (可選,預設: momentry:)
|
||||
MOMENTRY_REDIS_PREFIX=momentry:
|
||||
|
||||
# 生產環境範例 (.env)
|
||||
MOMENTRY_SERVER_PORT=3002
|
||||
MOMENTRY_REDIS_PREFIX=momentry:
|
||||
|
||||
# 開發環境範例 (.env.development)
|
||||
MOMENTRY_SERVER_PORT=3003
|
||||
MOMENTRY_REDIS_PREFIX=momentry_dev:
|
||||
```
|
||||
|
||||
## 11. 監控腳本
|
||||
|
||||
使用 Redis 進行監控的腳本應參考:
|
||||
|
||||
- `monitor/service/momentry_redis_monitor.sh` - Redis 健康檢查
|
||||
- `monitor/service/momentry_job_monitor.sh` - Job 狀態監控
|
||||
@@ -1,334 +0,0 @@
|
||||
# Momentry Core 影片 RAG 系統說明稿
|
||||
|
||||
| 項目 | 內容 |
|
||||
|------|------|
|
||||
| 建立者 | Warren |
|
||||
| 建立時間 | 2026-03-22 |
|
||||
| 文件版本 | V1.1 |
|
||||
|
||||
---
|
||||
|
||||
## 版本歷史
|
||||
|
||||
| 版本 | 日期 | 目的 | 操作人 | 工具/模型 |
|
||||
|------|------|------|--------|-----------|
|
||||
| V1.0 | 2026-03-22 | 創建文件 | Warren | OpenCode / MiniMax M2.5 |
|
||||
| V1.1 | 2026-03-25 | 更新API回應格式 (media_url→file_path) 與認證標頭 | OpenCode | deepseek-reasoner |
|
||||
|
||||
---
|
||||
|
||||
## 系統架構
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ 使用者 │
|
||||
│ (marcom 團隊) │
|
||||
└─────────────────┬───────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ WordPress 入口 │
|
||||
│ (wp.momentry.ddns.net) │
|
||||
└─────────────────┬───────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ n8n 自動化 │
|
||||
│ (localhost:5678) │
|
||||
│ │
|
||||
│ [Webhook] → [HTTP Request] → [處理結果] → [回覆用戶] │
|
||||
└─────────────────┬───────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ Momentry Core API │
|
||||
│ (localhost:3002) │
|
||||
│ │
|
||||
│ POST /api/v1/search → 語意搜尋 │
|
||||
│ POST /api/v1/n8n/search → n8n 專用格式 │
|
||||
│ GET /api/v1/videos → 影片列表 │
|
||||
└─────────────────┬───────────────────────────────────────────┘
|
||||
│
|
||||
┌─────────┴──────────┐
|
||||
▼ ▼
|
||||
┌───────────────┐ ┌───────────────┐
|
||||
│ PostgreSQL │ │ Qdrant │
|
||||
│ (chunks) │ │ (vectors) │
|
||||
└───────────────┘ └───────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 資料流程
|
||||
|
||||
```
|
||||
1. 上傳影片 → SFTPGo
|
||||
2. 影片註冊 → PostgreSQL
|
||||
3. ASR 處理 → 產生字幕區塊
|
||||
4. 儲存 chunks → PostgreSQL
|
||||
5. 向量化 → Qdrant
|
||||
6. 搜尋查詢 → API
|
||||
7. 回傳結果 → n8n → 用戶
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 示範影片
|
||||
|
||||
| 項目 | 內容 |
|
||||
|------|------|
|
||||
| 檔案名稱 | Old_Time_Movie_Show_-_Charade_1963.HD.mov |
|
||||
| UUID | a1b10138a6bbb0cd |
|
||||
| 時長 | 6879 秒(約 1.9 小時) |
|
||||
| 區塊數 | 3,886 個 |
|
||||
| 向量數 | 3,688 個 |
|
||||
|
||||
---
|
||||
|
||||
## API 端點
|
||||
|
||||
### 1. 語意搜尋
|
||||
|
||||
```
|
||||
POST http://localhost:3002/api/v1/search
|
||||
```
|
||||
|
||||
**請求:**
|
||||
```json
|
||||
{
|
||||
"query": "charade",
|
||||
"limit": 5,
|
||||
"uuid": "a1b10138a6bbb0cd"
|
||||
}
|
||||
```
|
||||
|
||||
> **注意**:
|
||||
> 1. **API 認證**: 所有 `/api/v1/*` 端點需要 `X-API-Key` 標頭
|
||||
> 2. **檔案路徑轉換**: API 現在返回 `file_path`(檔案系統路徑),需要轉換為可訪問的 URL(例如透過 SFTPGo 分享連結)
|
||||
|
||||
---
|
||||
|
||||
### 2. n8n 專用格式
|
||||
|
||||
```
|
||||
POST http://localhost:3002/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,
|
||||
"file_path": "/Users/accusys/momentry/var/sftpgo/data/demo/video.mp4"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 實作範例
|
||||
|
||||
### n8n Workflow 設計
|
||||
|
||||
```
|
||||
┌─────────────┐
|
||||
│ Webhook │ ← 接收用戶搜尋請求
|
||||
└──────┬──────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────┐
|
||||
│ HTTP Request│ → POST /api/v1/n8n/search
|
||||
└──────┬──────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────┐
|
||||
│ Code │ → 處理回傳結果
|
||||
└──────┬──────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────┐
|
||||
│ Telegram │ → 回覆給用戶
|
||||
│ (或 LINE) │
|
||||
└─────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Step-by-Step n8n Workflow
|
||||
|
||||
### Step 1: 建立 Webhook
|
||||
|
||||
1. n8n 開新 Workflow
|
||||
2. 新增 node: **Webhook**
|
||||
3. 設定 path: `video-search`
|
||||
4. 複製 Webhook URL
|
||||
|
||||
---
|
||||
|
||||
### Step 2: 設定 HTTP Request
|
||||
|
||||
1. 新增 node: **HTTP Request**
|
||||
2. 設定:
|
||||
```
|
||||
Method: POST
|
||||
URL: http://localhost:3002/api/v1/n8n/search
|
||||
Body Content Type: JSON
|
||||
Headers: X-API-Key (需設定)
|
||||
```
|
||||
|
||||
3. Body:
|
||||
```json
|
||||
{
|
||||
"query": "={{ $json.body }}",
|
||||
"limit": 5
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Step 3: 處理結果 (Code)
|
||||
|
||||
```javascript
|
||||
const hits = $input.first().json.hits;
|
||||
|
||||
if (!hits || hits.length === 0) {
|
||||
return {
|
||||
json: { message: "找不到相關結果" }
|
||||
};
|
||||
}
|
||||
|
||||
const results = hits.map((hit, index) => ({
|
||||
number: index + 1,
|
||||
text: hit.text,
|
||||
time: `${hit.start}s - ${hit.end}s`,
|
||||
score: Math.round(hit.score * 100) + "%",
|
||||
// 注意: API 現在返回 file_path(檔案系統路徑),需要轉換為可訪問的 URL
|
||||
url: hit.file_path + "#t=" + hit.start + "," + hit.end // 需實作檔案路徑轉換為 URL
|
||||
}));
|
||||
|
||||
return { json: { results } };
|
||||
```
|
||||
|
||||
> **注意**:
|
||||
> 1. **API 認證**: 所有 `/api/v1/*` 端點需要 `X-API-Key` 標頭
|
||||
> 2. **檔案路徑轉換**: API 現在返回 `file_path`(檔案系統路徑),需要轉換為可訪問的 URL(例如透過 SFTPGo 分享連結)
|
||||
|
||||
---
|
||||
|
||||
### Step 4: 格式化輸出
|
||||
|
||||
**Telegram 格式:**
|
||||
```
|
||||
🎬 搜尋結果: "{{ $json.query }}"
|
||||
|
||||
1️⃣ "fun plot twists, Woody Dialog and charming performances..."
|
||||
⏱ 48.8s - 55.4s
|
||||
📊 相關度: 53%
|
||||
|
||||
2️⃣ "Don't you like me to say that a pretty girl..."
|
||||
⏱ 4745.6s - 4748.6s
|
||||
📊 相關度: 52%
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 測試指令
|
||||
|
||||
### curl 測試
|
||||
|
||||
```bash
|
||||
# 語意搜尋
|
||||
curl -X POST http://localhost:3002/api/v1/search \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "X-API-Key: YOUR_API_KEY" \
|
||||
-d '{"query": "charade", "limit": 3}'
|
||||
|
||||
# n8n 格式
|
||||
curl -X POST http://localhost:3002/api/v1/n8n/search \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "X-API-Key: YOUR_API_KEY" \
|
||||
-d '{"query": "charade", "limit": 3}'
|
||||
|
||||
# 影片列表
|
||||
curl -H "X-API-Key: YOUR_API_KEY" http://localhost:3002/api/v1/videos
|
||||
|
||||
# 特定影片區塊
|
||||
curl -H "X-API-Key: YOUR_API_KEY" http://localhost:3002/api/v1/videos/a1b10138a6bbb0cd/chunks
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 實際搜尋範例
|
||||
|
||||
| 搜尋詞 | 結果摘要 |
|
||||
|--------|----------|
|
||||
| `charade` | "fun plot twists, Woody Dialog and charming performances..." |
|
||||
| `woody` | "Well, you thick skull hair, brain half-witted..." |
|
||||
| `classic movie` | "Hello and welcome to the old-time movie show..." |
|
||||
| `charming` | "fun plot twists, Woody Dialog and charming performances..." |
|
||||
|
||||
---
|
||||
|
||||
## 資料庫狀態
|
||||
|
||||
| 資料庫 | 資料筆數 | 狀態 |
|
||||
|--------|----------|------|
|
||||
| PostgreSQL (videos) | 4 | ✅ |
|
||||
| PostgreSQL (chunks) | 3,950 | ✅ |
|
||||
| PostgreSQL (vectors) | 1,870 | ✅ |
|
||||
| Qdrant (vectors) | 3,688 | ✅ |
|
||||
| Redis (job cache) | 4 keys | ✅ |
|
||||
|
||||
---
|
||||
|
||||
## 下一步
|
||||
|
||||
1. **建立 SFTPGo 分享連結**
|
||||
- 開啟 http://localhost:8080
|
||||
- 登入 demo / demopassword123
|
||||
- 建立影片分享連結
|
||||
|
||||
2. **測試 n8n Workflow**
|
||||
- 匯入 Postman Collection
|
||||
- 建立 Webhook
|
||||
- 測試搜尋
|
||||
|
||||
3. **整合到 WordPress**
|
||||
- 建立表單接收用戶輸入
|
||||
- 呼叫 n8n Webhook
|
||||
- 顯示搜尋結果
|
||||
|
||||
---
|
||||
|
||||
## 快速開始
|
||||
|
||||
```bash
|
||||
# 1. 測試搜尋 API
|
||||
curl -X POST http://localhost:3002/api/v1/search \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"query": "charade", "limit": 3}'
|
||||
|
||||
# 2. 查看影片列表
|
||||
curl http://localhost:3002/api/v1/videos
|
||||
|
||||
# 3. 查看 n8n 是否運行
|
||||
curl http://localhost:5678
|
||||
```
|
||||
@@ -1,127 +0,0 @@
|
||||
{
|
||||
"info": {
|
||||
"name": "Momentry Core API",
|
||||
"description": "Video RAG API for Momentry Core - Video search and management",
|
||||
"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
|
||||
},
|
||||
"variable": [
|
||||
{
|
||||
"key": "baseUrl",
|
||||
"value": "http://localhost:3002/api/v1"
|
||||
}
|
||||
],
|
||||
"item": [
|
||||
{
|
||||
"name": "Search",
|
||||
"item": [
|
||||
{
|
||||
"name": "Semantic Search",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"header": [
|
||||
{
|
||||
"key": "Content-Type",
|
||||
"value": "application/json"
|
||||
}
|
||||
],
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"query\": \"charade\",\n \"limit\": 5\n}"
|
||||
},
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/search",
|
||||
"host": ["{{baseUrl}}"],
|
||||
"path": ["search"]
|
||||
},
|
||||
"description": "Semantic search across video chunks using vector embeddings"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Search with UUID Filter",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"header": [
|
||||
{
|
||||
"key": "Content-Type",
|
||||
"value": "application/json"
|
||||
}
|
||||
],
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"query\": \"charade\",\n \"limit\": 5,\n \"uuid\": \"a1b10138a6bbb0cd\"\n}"
|
||||
},
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/search",
|
||||
"host": ["{{baseUrl}}"],
|
||||
"path": ["search"]
|
||||
},
|
||||
"description": "Search within a specific video by UUID"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "n8n Integration Search",
|
||||
"request": {
|
||||
"method": "POST",
|
||||
"header": [
|
||||
{
|
||||
"key": "Content-Type",
|
||||
"value": "application/json"
|
||||
}
|
||||
],
|
||||
"body": {
|
||||
"mode": "raw",
|
||||
"raw": "{\n \"query\": \"charade\",\n \"limit\": 5\n}"
|
||||
},
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/n8n/search",
|
||||
"host": ["{{baseUrl}}"],
|
||||
"path": ["n8n", "search"]
|
||||
},
|
||||
"description": "Search formatted for n8n workflow integration"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Videos",
|
||||
"item": [
|
||||
{
|
||||
"name": "List All Videos",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/videos",
|
||||
"host": ["{{baseUrl}}"],
|
||||
"path": ["videos"]
|
||||
},
|
||||
"description": "Get list of all registered videos"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Get Video by UUID",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/videos/a1b10138a6bbb0cd",
|
||||
"host": ["{{baseUrl}}"],
|
||||
"path": ["videos", "a1b10138a6bbb0cd"]
|
||||
},
|
||||
"description": "Get details for a specific video"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Get Video Chunks",
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/videos/a1b10138a6bbb0cd/chunks",
|
||||
"host": ["{{baseUrl}}"],
|
||||
"path": ["videos", "a1b10138a6bbb0cd", "chunks"]
|
||||
},
|
||||
"description": "Get all chunks for a video"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,106 +0,0 @@
|
||||
# n8n REST API Fix Summary
|
||||
|
||||
## Issue
|
||||
n8n REST API was returning 404 errors for all endpoints (`/api/v1/workflows`, `/rest/workflows`, etc.)
|
||||
|
||||
## Root Cause
|
||||
Port 5678 was occupied by the **n8n worker** process, preventing the main n8n instance from starting properly.
|
||||
|
||||
## Solution
|
||||
|
||||
### 1. Identified Port Conflict
|
||||
- Worker process was listening on port 5678 (same as main instance)
|
||||
- Main n8n couldn't start because port was in use
|
||||
|
||||
### 2. Fixed Worker Configuration
|
||||
Updated `/Library/LaunchDaemons/com.momentry.n8n.worker.plist`:
|
||||
- Added `N8N_PORT=5680` to worker environment variables
|
||||
- Workers shouldn't need HTTP ports, but this prevents port conflict
|
||||
|
||||
### 3. Restarted Services
|
||||
```bash
|
||||
# Kill all n8n processes
|
||||
sudo pkill -9 -f n8n
|
||||
|
||||
# Start main n8n (now successfully binds to port 5678)
|
||||
sudo launchctl enable system/com.momentry.n8n.main
|
||||
sudo launchctl bootstrap system /Library/LaunchDaemons/com.momentry.n8n.main.plist
|
||||
```
|
||||
|
||||
## Current Status
|
||||
|
||||
### n8n Instance
|
||||
- **URL**: http://localhost:5678
|
||||
- **Version**: 2.3.5
|
||||
- **Status**: Running ✅
|
||||
- **API Enabled**: Yes ✅
|
||||
|
||||
### API Key
|
||||
```
|
||||
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJlNjdiY2UzOS1iY2RkLTRjMjEtYmMwYy0yODNhYmI3ZjVjMjMiLCJpc3MiOiJuOG4iLCJhdWQiOiJwdWJsaWMtYXBpIiwiaWF0IjoxNzc0MTk4NzgwfQ.zke_Qc-saILl_tcwXm2K3J4slCmaXnzCfxVbdVPPvCE
|
||||
```
|
||||
|
||||
### MCP Configuration
|
||||
File: `~/.config/opencode/opencode.json`
|
||||
```json
|
||||
{
|
||||
"$schema": "https://opencode.ai/config.json",
|
||||
"mcp": {
|
||||
"gitea": {
|
||||
"type": "local",
|
||||
"enabled": true,
|
||||
"command": [
|
||||
"/opt/homebrew/bin/gitea-mcp-server",
|
||||
"-token", "<GITEA_TOKEN>",
|
||||
"-host", "http://localhost:3000"
|
||||
]
|
||||
},
|
||||
"n8n": {
|
||||
"type": "local",
|
||||
"enabled": true,
|
||||
"command": ["/opt/homebrew/bin/mcp-n8n"],
|
||||
"environment": {
|
||||
"N8N_BASE_URL": "http://localhost:5678",
|
||||
"N8N_API_KEY": "<N8N_API_KEY>"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Verified Endpoints
|
||||
|
||||
### List Workflows
|
||||
```bash
|
||||
curl -H "X-N8N-API-KEY: <API_KEY>" http://localhost:5678/api/v1/workflows
|
||||
```
|
||||
**Result**: ✅ 30 workflows returned
|
||||
|
||||
### List Executions
|
||||
```bash
|
||||
curl -H "X-N8N-API-KEY: <API_KEY>" http://localhost:5678/api/v1/executions
|
||||
```
|
||||
**Result**: ✅ 100 executions returned
|
||||
|
||||
## Next Steps
|
||||
|
||||
1. **Start n8n Worker** (optional for MCP):
|
||||
Workers handle job processing but aren't required for API access.
|
||||
|
||||
2. **Test MCP Integration**:
|
||||
Restart OpenCode to load the MCP configuration and test n8n integration.
|
||||
|
||||
3. **Verify Workflow Management**:
|
||||
- Create workflow via API
|
||||
- Execute workflow
|
||||
- Monitor execution status
|
||||
|
||||
## Files Modified
|
||||
- `/Library/LaunchDaemons/com.momentry.n8n.worker.plist` - Added N8N_PORT=5680
|
||||
|
||||
## API Documentation
|
||||
- Base URL: `http://localhost:5678/api/v1`
|
||||
- Authentication: Header `X-N8N-API-KEY: <token>`
|
||||
- Available endpoints: workflows, executions, credentials, users, etc.
|
||||
|
||||
See full API reference: https://docs.n8n.io/api/
|
||||
249
docs/N8N_DEMO.md
249
docs/N8N_DEMO.md
@@ -1,249 +0,0 @@
|
||||
# n8n 整合範例
|
||||
|
||||
| 項目 | 內容 |
|
||||
|------|------|
|
||||
| 建立者 | Warren |
|
||||
| 建立時間 | 2026-03-18 |
|
||||
| 文件版本 | V1.1 |
|
||||
|
||||
---
|
||||
|
||||
## 版本歷史
|
||||
|
||||
| 版本 | 日期 | 目的 | 操作人 | 工具/模型 |
|
||||
|------|------|------|--------|-----------|
|
||||
| V1.0 | 2026-03-18 | 創建文件 | Warren | OpenCode / MiniMax M2.5 |
|
||||
| V1.1 | 2026-03-25 | 更新API回應格式 (media_url→file_path) 與認證標頭 | OpenCode | deepseek-reasoner |
|
||||
|
||||
---
|
||||
|
||||
## 基本設定
|
||||
|
||||
### API 端點
|
||||
- **Base URL:** `http://localhost:3002/api/v1`
|
||||
- **Method:** `POST`
|
||||
- **Content-Type:** `application/json`
|
||||
- **Authentication:** `X-API-Key: YOUR_API_KEY` (所有 `/api/v1/*` 端點皆需要)
|
||||
|
||||
---
|
||||
|
||||
## Workflow 1: 基礎搜尋
|
||||
|
||||
### Trigger: Manual / Webhook
|
||||
|
||||
```
|
||||
[Manual Trigger]
|
||||
↓
|
||||
[HTTP Request] → POST http://localhost:3002/api/v1/search
|
||||
↓
|
||||
[Set] → 設定搜尋詞 "charade"
|
||||
↓
|
||||
[Code] → 處理回傳結果
|
||||
↓
|
||||
[Respond]
|
||||
```
|
||||
|
||||
### HTTP Request 設定
|
||||
```json
|
||||
{
|
||||
"url": "http://localhost:3002/api/v1/search",
|
||||
"method": "POST",
|
||||
"body": {
|
||||
"query": "={{ $json.searchTerm }}",
|
||||
"limit": 5
|
||||
},
|
||||
"options": {
|
||||
"headers": {
|
||||
"Content-Type": "application/json",
|
||||
"X-API-Key": "YOUR_API_KEY"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Code (處理結果)
|
||||
```javascript
|
||||
const results = $input.first().json.results;
|
||||
|
||||
const videoUrl = "https://wp.momentry.ddns.net/Old_Time_Movie_Show_-_Charade_1963.HD.mov";
|
||||
|
||||
return results.map(r => ({
|
||||
chunk_id: r.chunk_id,
|
||||
text: r.text,
|
||||
start: r.start_time,
|
||||
end: r.end_time,
|
||||
score: r.score,
|
||||
video_url: `${videoUrl}#t=${r.start_time},${r.end_time}`
|
||||
}));
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Workflow 2: n8n 專用格式
|
||||
|
||||
使用 `/n8n/search` 端點(已包含 file_path)
|
||||
|
||||
### HTTP Request
|
||||
```json
|
||||
{
|
||||
"url": "http://localhost:3002/api/v1/n8n/search",
|
||||
"method": "POST",
|
||||
"body": {
|
||||
"query": "={{ $json.searchTerm }}",
|
||||
"limit": 5
|
||||
},
|
||||
"options": {
|
||||
"headers": {
|
||||
"Content-Type": "application/json",
|
||||
"X-API-Key": "YOUR_API_KEY"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 回傳格式
|
||||
```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,
|
||||
"file_path": "/Users/accusys/momentry/var/sftpgo/data/demo/video.mp4"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
> **注意**: API 現在返回 `file_path`(檔案系統路徑)而非 `media_url`(網頁 URL)。如需在網頁中播放影片,請將檔案路徑轉換為可訪問的 URL(例如透過 SFTPGo 分享連結)。
|
||||
|
||||
---
|
||||
|
||||
## Workflow 3: 訊息機器人整合
|
||||
|
||||
### Telegram Bot 範例
|
||||
|
||||
```
|
||||
[Webhook: Telegram]
|
||||
↓
|
||||
[Extract: /search charade]
|
||||
↓
|
||||
[HTTP Request] → POST /api/v1/search
|
||||
↓
|
||||
[Format Response]
|
||||
↓
|
||||
[Telegram: Send Message]
|
||||
```
|
||||
|
||||
### 回傳格式
|
||||
```
|
||||
🎬 搜尋結果: "charade"
|
||||
|
||||
1. "fun plot twists, Woody Dialog and charming performances..."
|
||||
⏱ 48.8s - 55.4s
|
||||
📊 分數: 0.526
|
||||
|
||||
2. "Don't you like me to say that a pretty girl..."
|
||||
⏱ 4745.6s - 4748.6s
|
||||
📊 分數: 0.525
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Workflow 4: 多影片搜尋
|
||||
|
||||
### 取得所有影片
|
||||
```
|
||||
[HTTP Request]
|
||||
GET http://localhost:3002/api/v1/videos
|
||||
```
|
||||
|
||||
### 依 UUID 篩選
|
||||
```json
|
||||
{
|
||||
"query": "charade",
|
||||
"limit": 5,
|
||||
"uuid": "a1b10138a6bbb0cd"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Workflow 5: 定時更新
|
||||
|
||||
```
|
||||
[Cron: 每小時]
|
||||
↓
|
||||
[HTTP Request] → GET /api/v1/videos
|
||||
↓
|
||||
[Loop Over Items]
|
||||
↓
|
||||
[Check: 新影片?]
|
||||
↓
|
||||
[Process: 執行 vectorize]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 實用場景
|
||||
|
||||
### 1. 客服機器人
|
||||
用戶問「這部片在哪一段有談到 charade?」
|
||||
→ 搜尋 API → 回傳時戳 → 直接播放該片段
|
||||
|
||||
### 2. 內容推薦
|
||||
根據用戶輸入的關鍵字,找到相關影片片段
|
||||
|
||||
### 3. 自動化剪輯
|
||||
搜尋多個片段 → 組合成精華影片
|
||||
|
||||
---
|
||||
|
||||
## 錯誤處理
|
||||
|
||||
```javascript
|
||||
const response = $input.first();
|
||||
|
||||
if (!response.json.results || response.json.results.length === 0) {
|
||||
return {
|
||||
success: false,
|
||||
message: "找不到相關結果"
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
count: response.json.results.length,
|
||||
data: response.json.results
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 測試用 cURL
|
||||
|
||||
```bash
|
||||
# 基本搜尋
|
||||
curl -X POST http://localhost:3002/api/v1/search \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "X-API-Key: YOUR_API_KEY" \
|
||||
-d '{"query": "charade", "limit": 3}'
|
||||
|
||||
# n8n 格式
|
||||
curl -X POST http://localhost:3002/api/v1/n8n/search \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "X-API-Key: YOUR_API_KEY" \
|
||||
-d '{"query": "charade", "limit": 3}'
|
||||
|
||||
# 取得影片列表
|
||||
curl -H "X-API-Key: YOUR_API_KEY" http://localhost:3002/api/v1/videos
|
||||
|
||||
# 取得特定影片的區塊
|
||||
curl -H "X-API-Key: YOUR_API_KEY" http://localhost:3002/api/v1/videos/a1b10138a6bbb0cd/chunks
|
||||
```
|
||||
@@ -1,355 +0,0 @@
|
||||
# n8n Video RAG Demo - API 執行記錄
|
||||
|
||||
| 項目 | 內容 |
|
||||
|------|------|
|
||||
| 建立者 | Warren |
|
||||
| 建立時間 | 2026-03-22 |
|
||||
| 文件版本 | V1.1 |
|
||||
| 目標 | 完整執行 n8n Video RAG Workflow 並記錄所有 API 呼叫 |
|
||||
|
||||
---
|
||||
|
||||
## 版本歷史
|
||||
|
||||
| 版本 | 日期 | 目的 | 操作人 | 工具/模型 |
|
||||
|------|------|------|--------|-----------|
|
||||
| V1.0 | 2026-03-22 | 創建文件 | Warren | OpenCode |
|
||||
| V1.1 | 2026-03-26 | 更新 API 範例,新增 X-API-Key 驗證標頭 | OpenCode | deepseek-reasoner |
|
||||
|
||||
---
|
||||
|
||||
## Phase 1: SFTPGo 準備
|
||||
|
||||
### Step 1.1: 取得 Demo User Token
|
||||
|
||||
**API 呼叫:**
|
||||
```bash
|
||||
curl -X GET "http://localhost:8080/api/v2/user/token" \
|
||||
-u "demo:demopassword123"
|
||||
```
|
||||
|
||||
**Request:**
|
||||
```
|
||||
GET /api/v2/user/token
|
||||
Authorization: Basic ZG9tbzpkZW1vcGFzc3dvcmQxMjM=
|
||||
```
|
||||
|
||||
**Response (200 OK):**
|
||||
```json
|
||||
{
|
||||
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
|
||||
"expires_at": "2026-03-22T07:05:57Z"
|
||||
}
|
||||
```
|
||||
|
||||
**Token 有效期限:** 20 分鐘
|
||||
|
||||
**Session Token (Demo User):**
|
||||
```
|
||||
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsiQVBJVXNlciIsIjo6MSJdLCJleHAiOjE3NzQxNjMxNTcsImlhdCI6MTc3NDE2MTk1NywianRpIjoiZDZ2cDA5YWcyZnIwMnY3aTlybDAiLCJuYmYiOjE3NzQxNjE5NDcsInN1YiI6IjE3NzQxNjE5NTM0OTMiLCJ1c2VybmFtZSI6ImRlbW8ifQ.yw0UCv8sQXXCkOr7qmK2ejLzuoA8IDrmC9bpgFE4R_Q
|
||||
```
|
||||
|
||||
**結果:** ✅ 成功
|
||||
|
||||
---
|
||||
|
||||
### Step 1.2: 上傳測試影片到 SFTPGo
|
||||
|
||||
**影片選擇:** `Old_Time_Movie_Show_-_Charade_1963.HD.mov` (2.3 GB)
|
||||
- 路徑: `/Users/accusys/test_video/Old_Time_Movie_Show_-_Charade_1963.HD.mov`
|
||||
- ASR Segments: 1,917 (已預處理)
|
||||
|
||||
**API 呼叫:**
|
||||
```bash
|
||||
TOKEN="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
|
||||
curl -X POST "http://localhost:8080/api/v2/user/files" \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
-F "path=/demo" \
|
||||
-F "mkdir_parents=true" \
|
||||
-F "filenames=@/Users/accusys/test_video/Old_Time_Movie_Show_-_Charade_1963.HD.mov"
|
||||
```
|
||||
|
||||
**Request:**
|
||||
```
|
||||
POST /api/v2/user/files
|
||||
Authorization: Bearer <token>
|
||||
Content-Type: multipart/form-data
|
||||
|
||||
path: /demo
|
||||
mkdir_parents: true
|
||||
filenames: @/path/to/Old_Time_Movie_Show_-_Charade_1963.HD.mov
|
||||
```
|
||||
|
||||
**Response (201 Created):**
|
||||
```json
|
||||
{"message":"Upload completed"}
|
||||
```
|
||||
|
||||
**上傳統計:**
|
||||
- 檔案大小: 2,361,629,896 bytes (2.3 GB)
|
||||
- 上傳時間: 7 秒
|
||||
- 平均速度: ~337 MB/s
|
||||
|
||||
**結果:** ✅ 成功
|
||||
|
||||
---
|
||||
|
||||
### Step 1.3: 建立分享連結
|
||||
|
||||
**API 呼叫:**
|
||||
```bash
|
||||
curl -X POST "http://localhost:8080/api/v2/user/shares" \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"name": "Charade_1963_Demo",
|
||||
"paths": ["/Old_Time_Movie_Show_-_Charade_1963.HD.mov"],
|
||||
"scope": 1,
|
||||
"expires_at": 0
|
||||
}'
|
||||
```
|
||||
|
||||
**Request:**
|
||||
```json
|
||||
POST /api/v2/user/shares
|
||||
Authorization: Bearer <token>
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"name": "Charade_1963_Demo",
|
||||
"paths": ["/Old_Time_Movie_Show_-_Charade_1963.HD.mov"],
|
||||
"scope": 1,
|
||||
"expires_at": 0
|
||||
}
|
||||
```
|
||||
|
||||
**Response (200 OK):**
|
||||
```json
|
||||
{"message":"Share created"}
|
||||
```
|
||||
|
||||
**結果:** ✅ 成功
|
||||
|
||||
---
|
||||
|
||||
### Step 1.4: 驗證上傳結果
|
||||
|
||||
**API 呼叫 - 列出分享:**
|
||||
```bash
|
||||
curl -X GET "http://localhost:8080/api/v2/user/shares" \
|
||||
-H "Authorization: Bearer $TOKEN"
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
[
|
||||
{
|
||||
"id": "CjmQfrkXY5qDtC46WVZY2S",
|
||||
"name": "Charade_1963_Demo",
|
||||
"scope": 1,
|
||||
"paths": [
|
||||
"/Old_Time_Movie_Show_-_Charade_1963.HD.mov"
|
||||
],
|
||||
"username": "demo",
|
||||
"created_at": 1774162072853,
|
||||
"updated_at": 1774162072853,
|
||||
"password": ""
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
**分享連結:**
|
||||
- Share ID: `CjmQfrkXY5qDtC46WVZY2S`
|
||||
- Browse URL: `http://localhost:8080/web/client/pubshares/CjmQfrkXY5qDtC46WVZY2S/browse`
|
||||
|
||||
**本地目錄驗證:**
|
||||
```
|
||||
/Users/accusys/sftpgo_test/demo/
|
||||
└── Old_Time_Movie_Show_-_Charade_1963.HD.mov (2,361,629,896 bytes)
|
||||
```
|
||||
|
||||
**結果:** ✅ 成功
|
||||
|
||||
---
|
||||
|
||||
## Phase 2: Momentry 註冊
|
||||
|
||||
### Step 2.1: 健康檢查
|
||||
|
||||
**API 呼叫:**
|
||||
```bash
|
||||
curl -X GET "http://localhost:3002/health"
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```
|
||||
(待填寫)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Step 2.2: 註冊影片
|
||||
|
||||
**API 呼叫:**
|
||||
```bash
|
||||
curl -X POST "http://localhost:3002/api/v1/register" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"path": "/Users/accusys/sftpgo_test/demo/Old_Time_Movie_Show_-_Charade_1963.HD.mov"
|
||||
}'
|
||||
```
|
||||
|
||||
**Request:**
|
||||
```json
|
||||
POST /api/v1/register
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"path": "/Users/accusys/sftpgo_test/demo/Old_Time_Movie_Show_-_Charade_1963.HD.mov"
|
||||
}
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```
|
||||
(待填寫)
|
||||
{
|
||||
"uuid": "...",
|
||||
"video_id": ...,
|
||||
"file_name": "...",
|
||||
"duration": ...,
|
||||
"width": ...,
|
||||
"height": ...
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Phase 3: 處理進度追蹤
|
||||
|
||||
### Step 3.1: 查詢處理進度 (新版 API)
|
||||
|
||||
**API 呼叫:**
|
||||
```bash
|
||||
curl -X GET "http://localhost:3002/api/v1/progress/{uuid}"
|
||||
```
|
||||
|
||||
**Response (新版 - 包含影片資訊與系統資源):**
|
||||
```json
|
||||
{
|
||||
"uuid": "a1b10138a6bbb0cd",
|
||||
"user": null,
|
||||
"group": null,
|
||||
"file_name": "Old_Time_Movie_Show_-_Charade_1963.HD.mov",
|
||||
"duration": 6879.33,
|
||||
"overall_progress": 28,
|
||||
"cpu_percent": 3.7,
|
||||
"gpu_percent": null,
|
||||
"memory_percent": 0.1,
|
||||
"memory_mb": 19328,
|
||||
"processors": [
|
||||
{"name": "asr", "status": "complete", "current": 1867, "total": 0, "progress": 100, "message": "1867 segments"},
|
||||
{"name": "cut", "status": "complete", "current": 1331, "total": 1331, "progress": 100, "message": "1331 scenes"},
|
||||
{"name": "asrx", "status": "error", "current": 0, "total": 0, "progress": 0, "message": "0 segments"},
|
||||
{"name": "yolo", "status": "progress", "current": 69400, "total": 412343, "progress": 16, "message": "frame 69400"},
|
||||
{"name": "ocr", "status": "pending", "current": 0, "total": 0, "progress": 0, "message": ""},
|
||||
{"name": "face", "status": "pending", "current": 0, "total": 0, "progress": 0, "message": ""},
|
||||
{"name": "pose", "status": "pending", "current": 0, "total": 0, "progress": 0, "message": ""}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**欄位說明:**
|
||||
| 欄位 | 說明 |
|
||||
|------|------|
|
||||
| uuid | 影片唯一識別碼 |
|
||||
| user | 處理所屬用戶 (如已設定) |
|
||||
| group | 處理所屬群組 (如已設定) |
|
||||
| file_name | 影片檔案名稱 |
|
||||
| duration | 影片時長 (秒) |
|
||||
| overall_progress | 整體進度 (百分比) |
|
||||
| cpu_percent | CPU 使用率 (%) |
|
||||
| gpu_percent | GPU 使用率 (%),無 GPU 則為 null |
|
||||
| memory_percent | 記憶體使用率 (%) |
|
||||
| memory_mb | 記憶體使用量 (MB) |
|
||||
| processors | 各處理器狀態陣列 |
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Phase 4: 自然語言檢索
|
||||
|
||||
### Step 4.1: RAG 搜尋
|
||||
|
||||
**API 呼叫:**
|
||||
```bash
|
||||
curl -X POST "http://localhost:3002/api/v1/search" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"query": "What is the movie about?",
|
||||
"limit": 10,
|
||||
"uuid": "..."
|
||||
}'
|
||||
```
|
||||
|
||||
**Request:**
|
||||
```json
|
||||
POST /api/v1/search
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"query": "What is the movie about?",
|
||||
"limit": 10,
|
||||
"uuid": "<uuid from registration>"
|
||||
}
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```
|
||||
(待填寫)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Step 4.2: n8n 搜尋 (含 file_path)
|
||||
|
||||
**API 呼叫:**
|
||||
```bash
|
||||
curl -X POST "http://localhost:3002/api/v1/n8n/search" \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "X-API-Key: demo_api_key_12345" \
|
||||
-d '{
|
||||
"query": "What is the movie about?",
|
||||
"limit": 10,
|
||||
"uuid": "..."
|
||||
}'
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```
|
||||
(待填寫)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 憑證彙整
|
||||
|
||||
| 服務 | 項目 | 值 |
|
||||
|------|------|------|
|
||||
| SFTPGo | API Base | `http://localhost:8080/api/v2` |
|
||||
| SFTPGo | Demo User | `demo` |
|
||||
| SFTPGo | Demo Password | `demopassword123` (已重設) |
|
||||
| SFTPGo | Demo Home | `/Users/accusys/sftpgo_test/demo` |
|
||||
| SFTPGo | Token Endpoint | `/api/v2/user/token` |
|
||||
| SFTPGo | Share ID | `CjmQfrkXY5qDtC46WVZY2S` |
|
||||
| Momentry | Server | `http://localhost:3002` |
|
||||
| Momentry | MEDIA_BASE_URL | `https://wp.momentry.ddns.net` |
|
||||
|
||||
---
|
||||
|
||||
## 版本歷史
|
||||
|
||||
| 日期 | 版本 | 變更 |
|
||||
|------|------|------|
|
||||
| 2026-03-22 | v1.0 | 初始建立文件 |
|
||||
| 2026-03-22 | v1.1 | 成功取得 Demo Token |
|
||||
| 2026-03-22 | v1.2 | Phase 1 完成 (上傳 Charade 2.3GB) |
|
||||
@@ -1,690 +0,0 @@
|
||||
# n8n Video RAG Workflow - Node 設計
|
||||
|
||||
| 項目 | 內容 |
|
||||
|------|------|
|
||||
| 建立者 | Warren |
|
||||
| 建立時間 | 2026-03-22 |
|
||||
| 文件版本 | V1.1 |
|
||||
|
||||
---
|
||||
|
||||
## 版本歷史
|
||||
|
||||
| 版本 | 日期 | 目的 | 操作人 | 工具/模型 |
|
||||
|------|------|------|--------|-----------|
|
||||
| V1.0 | 2026-03-22 | 創建文件 | Warren | OpenCode / MiniMax M2.5 |
|
||||
| V1.1 | 2026-03-25 | 更新API回應格式 (media_url→file_path) 與認證標頭 | OpenCode | deepseek-reasoner |
|
||||
|
||||
---
|
||||
|
||||
## 完整 Workflow 架構
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────────────┐
|
||||
│ n8n Workflow: Video RAG Demo │
|
||||
├─────────────────────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ ┌─────────────────────────────────────────────────────────────────────┐ │
|
||||
│ │ Phase 1: SFTPGo 準備 (全部在 n8n Node 內執行) │ │
|
||||
│ │ │ │
|
||||
│ │ ① Webhook Trigger │ │
|
||||
│ │ ↓ │ │
|
||||
│ │ ② Set Variables (解析 file_name, query) │ │
|
||||
│ │ ↓ │ │
|
||||
│ │ ③ Get SFTPGo Token │ │
|
||||
│ │ ↓ │ │
|
||||
│ │ ④ Upload to SFTPGo │ │
|
||||
│ │ ↓ │ │
|
||||
│ │ ⑤ Create Share Link │ │
|
||||
│ │ ↓ │ │
|
||||
│ │ ⑥ Verify Upload (List Files + List Shares) │ │
|
||||
│ └─────────────────────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ ┌─────────────────────────────────────────────────────────────────────┐ │
|
||||
│ │ Phase 2: Momentry 註冊 (只處理 ASR, ASRX, STORY) │ │
|
||||
│ │ │ │
|
||||
│ │ ⑦ Register Video (modules=asr,asrx,story) │ │
|
||||
│ └─────────────────────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ ┌─────────────────────────────────────────────────────────────────────┐ │
|
||||
│ │ Phase 3: Progress Loop (n8n Logs 記錄) │ │
|
||||
│ │ │ │
|
||||
│ │ ⑧ Wait 10s ─────────────────────────────────────────────────┐ │ │
|
||||
│ │ ↓ │ │
|
||||
│ │ ⑨ Check Progress (API) │ │
|
||||
│ │ ↓ │ │
|
||||
│ │ ⑩ Log Progress (Code Node → n8n Logs) │ │
|
||||
│ │ ↓ │ │
|
||||
│ │ ⑪ Is Complete? (IF) │ │
|
||||
│ │ │ │ │
|
||||
│ │ ├── NO ──────────────────────────────── Loop Back ─────────┘ │ │
|
||||
│ │ └── YES ────────────────────────────────────────────── Exit ──┘ │ │
|
||||
│ └─────────────────────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ ┌─────────────────────────────────────────────────────────────────────┐ │
|
||||
│ │ Phase 4: 搜尋與回應 │ │
|
||||
│ │ │ │
|
||||
│ │ ⑫ Hybrid Search (Vector + BM25) │ │
|
||||
│ │ ↓ │ │
|
||||
│ │ ⑬ Build Response │ │
|
||||
│ │ ↓ │ │
|
||||
│ │ ⑭ Respond to Webhook │ │
|
||||
│ └─────────────────────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 模組說明
|
||||
|
||||
| 模組 | 用途 | 輸出 |
|
||||
|------|------|------|
|
||||
| `asr` | 語音轉文字 (Whisper) | 字幕/文字稿 |
|
||||
| `asrx` | 說話者分離 (WhisperX) | 誰在什麼時候說什麼 |
|
||||
| `story` | 故事線生成 (Parent-Child Chunks) | 敘事結構 + 父子區塊關聯 |
|
||||
|
||||
**注意**: 只處理語音和故事相關模組,跳過 YOLO、OCR、Face、Pose 等視覺分析。
|
||||
┌─────────────────────────────────────────────────────────────────────────────┐
|
||||
│ n8n Workflow: Video RAG Demo │
|
||||
├─────────────────────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ ┌─────────────────────────────────────────────────────────────────────┐ │
|
||||
│ │ Phase 1: SFTPGo 準備 (全部在 n8n Node 內執行) │ │
|
||||
│ │ │ │
|
||||
│ │ ① Webhook Trigger │ │
|
||||
│ │ ↓ │ │
|
||||
│ │ ② Set Variables (解析 file_name, query) │ │
|
||||
│ │ ↓ │ │
|
||||
│ │ ③ Get SFTPGo Token │ │
|
||||
│ │ ↓ │ │
|
||||
│ │ ④ Upload to SFTPGo │ │
|
||||
│ │ ↓ │ │
|
||||
│ │ ⑤ Create Share Link │ │
|
||||
│ │ ↓ │ │
|
||||
│ │ ⑥ Verify Upload (List Files + List Shares) │ │
|
||||
│ └─────────────────────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ ┌─────────────────────────────────────────────────────────────────────┐ │
|
||||
│ │ Phase 2: Momentry 註冊 │ │
|
||||
│ │ │ │
|
||||
│ │ ⑦ Register Video │ │
|
||||
│ └─────────────────────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ ┌─────────────────────────────────────────────────────────────────────┐ │
|
||||
│ │ Phase 3: Progress Loop (n8n Logs 記錄) │ │
|
||||
│ │ │ │
|
||||
│ │ ⑧ Wait 10s ─────────────────────────────────────────────────┐ │ │
|
||||
│ │ ↓ │ │ │
|
||||
│ │ ⑨ Check Progress (API) │ │ │
|
||||
│ │ ↓ │ │ │
|
||||
│ │ ⑩ Log Progress (Code Node → n8n Logs) │ │ │
|
||||
│ │ ↓ │ │ │
|
||||
│ │ ⑪ Is Complete? (IF) │ │ │
|
||||
│ │ │ │ │ │
|
||||
│ │ ├── NO ──────────────────────────────── Loop Back ─────────┘ │ │
|
||||
│ │ └── YES ────────────────────────────────────────────── Exit ──┘ │ │
|
||||
│ └─────────────────────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ ┌─────────────────────────────────────────────────────────────────────┐ │
|
||||
│ │ Phase 4: 搜尋與回應 │ │
|
||||
│ │ │ │
|
||||
│ │ ⑫ Natural Language Search │ │
|
||||
│ │ ↓ │ │
|
||||
│ │ ⑬ Get File Path (含 file_path) │ │
|
||||
│ │ ↓ │ │
|
||||
│ │ ⑭ Build Response │ │
|
||||
│ │ ↓ │ │
|
||||
│ │ ⑮ Respond to Webhook │ │
|
||||
│ └─────────────────────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Node 詳細配置
|
||||
|
||||
### Node ①: Webhook Trigger (觸發器)
|
||||
|
||||
```yaml
|
||||
Node Name: "Webhook Trigger"
|
||||
Node Type: "Webhook"
|
||||
|
||||
Configuration:
|
||||
HTTP Method: POST
|
||||
Path: "video-rag"
|
||||
Response Mode: "Response Node"
|
||||
Response Node: "Respond to Webhook"
|
||||
|
||||
Input JSON Example:
|
||||
{
|
||||
"file_name": "Old_Time_Movie_Show_-_Charade_1963.HD.mov",
|
||||
"query": "What is the movie about?"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Node ②: Set Variables (變數設定)
|
||||
|
||||
```yaml
|
||||
Node Name: "Set Variables"
|
||||
Node Type: "Set"
|
||||
|
||||
Configuration:
|
||||
Keep Only Set: true
|
||||
|
||||
Variables:
|
||||
- Name: "file_name"
|
||||
Value: "{{ $json.body.file_name }}"
|
||||
|
||||
- Name: "query"
|
||||
Value: "{{ $json.body.query }}"
|
||||
|
||||
- Name: "sftpgo_path"
|
||||
Value: "/{{ $json.body.file_name }}"
|
||||
|
||||
- Name: "register_path"
|
||||
Value: "/Users/accusys/sftpgo_test/demo/{{ $json.body.file_name }}"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Node ③: Get SFTPGo Token (取得權杖)
|
||||
|
||||
```yaml
|
||||
Node Name: "Get SFTPGo Token"
|
||||
Node Type: "HTTP Request"
|
||||
|
||||
Configuration:
|
||||
Method: GET
|
||||
URL: "http://localhost:8080/api/v2/user/token"
|
||||
Authentication: "Basic Auth"
|
||||
User: "demo"
|
||||
Password: "demopassword123"
|
||||
|
||||
Output:
|
||||
{
|
||||
"access_token": "eyJhbGci...",
|
||||
"expires_at": "2026-03-22T07:00:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Node ④: Upload to SFTPGo (上傳檔案)
|
||||
|
||||
```yaml
|
||||
Node Name: "Upload to SFTPGo"
|
||||
Node Type: "HTTP Request"
|
||||
|
||||
Configuration:
|
||||
Method: POST
|
||||
URL: "http://localhost:8080/api/v2/user/files"
|
||||
Authentication: "Bearer Token"
|
||||
Bearer Token: "{{ $json.access_token }}"
|
||||
|
||||
Body Content Type: "Form-Data Multipart"
|
||||
|
||||
Body:
|
||||
path: /demo
|
||||
mkdir_parents: true
|
||||
filenames: @{{ $json.file_name }}
|
||||
|
||||
Output:
|
||||
{"message":"Upload completed"}
|
||||
```
|
||||
|
||||
**檔案來源選項:**
|
||||
1. **Webhook 接收**: 從 Webhook 的 binary data 取得
|
||||
2. **固定路徑**: 指定本地檔案路徑
|
||||
3. **URL 下載**: 先下載遠端檔案再上傳
|
||||
|
||||
---
|
||||
|
||||
### Node ⑤: Create Share Link (建立分享連結)
|
||||
|
||||
```yaml
|
||||
Node Name: "Create Share Link"
|
||||
Node Type: "HTTP Request"
|
||||
|
||||
Configuration:
|
||||
Method: POST
|
||||
URL: "http://localhost:8080/api/v2/user/shares"
|
||||
Authentication: "Bearer Token"
|
||||
Bearer Token: "{{ $json.access_token }}"
|
||||
|
||||
Body Content Type: "JSON"
|
||||
|
||||
Body:
|
||||
{
|
||||
"name": "{{ $json.file_name }}_share",
|
||||
"paths": ["/{{ $json.file_name }}"],
|
||||
"scope": 1,
|
||||
"expires_at": 0
|
||||
}
|
||||
|
||||
Output:
|
||||
{
|
||||
"id": "CjmQfrkXY5qDtC46WVZY2S",
|
||||
"name": "Charade_share"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Node ⑥: Verify Upload (驗證上傳)
|
||||
|
||||
```yaml
|
||||
Node Name: "Verify Upload - List Shares"
|
||||
Node Type: "HTTP Request"
|
||||
|
||||
Configuration:
|
||||
Method: GET
|
||||
URL: "http://localhost:8080/api/v2/user/shares"
|
||||
Authentication: "Bearer Token"
|
||||
Bearer Token: "{{ $json.access_token }}"
|
||||
|
||||
Output:
|
||||
[
|
||||
{
|
||||
"id": "CjmQfrkXY5qDtC46WVZY2S",
|
||||
"name": "Charade_share",
|
||||
"paths": ["/Old_Time_Movie_Show_-_Charade_1963.HD.mov"]
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Node ⑦: Register Video (註冊影片)
|
||||
|
||||
**說明**: 只註冊 ASR、ASRX、STORY 模組處理
|
||||
|
||||
```yaml
|
||||
Node Name: "Register Video"
|
||||
Node Type: "HTTP Request"
|
||||
|
||||
Configuration:
|
||||
Method: POST
|
||||
URL: "http://localhost:3002/api/v1/register"
|
||||
|
||||
Body Content Type: "JSON"
|
||||
|
||||
Body:
|
||||
{
|
||||
"path": "{{ $json.register_path }}",
|
||||
"modules": "asr,asrx,story"
|
||||
}
|
||||
|
||||
Output:
|
||||
{
|
||||
"uuid": "a1b10138a6bbb0cd",
|
||||
"video_id": 7,
|
||||
"file_name": "Old_Time_Movie_Show_-_Charade_1963.HD.mov",
|
||||
"duration": 6879.33,
|
||||
"width": 1920,
|
||||
"height": 1080
|
||||
}
|
||||
```
|
||||
|
||||
**可用模組**:
|
||||
| 模組 | 說明 |
|
||||
|------|------|
|
||||
| `asr` | 語音轉文字 (Whisper) |
|
||||
| `asrx` | 說話者分離 (WhisperX) |
|
||||
| `story` | 故事線生成 (Parent-Child) |
|
||||
| `yolo` | 物體偵測 (可選) |
|
||||
| `cut` | 場景偵測 (可選) |
|
||||
| `ocr` | 文字辨識 (可選) |
|
||||
| `face` | 人臉偵測 (可選) |
|
||||
| `pose` | 姿態估計 (可選) |
|
||||
|
||||
---
|
||||
|
||||
### Node ⑧: Wait 10 Seconds (輪詢間隔)
|
||||
|
||||
```yaml
|
||||
Node Name: "Wait 10 Seconds"
|
||||
Node Type: "Wait"
|
||||
|
||||
Configuration:
|
||||
Amount: 10
|
||||
Unit: "Seconds"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Node ⑨: Check Progress (檢查進度)
|
||||
|
||||
```yaml
|
||||
Node Name: "Check Progress"
|
||||
Node Type: "HTTP Request"
|
||||
|
||||
Configuration:
|
||||
Method: GET
|
||||
URL: "http://localhost:3002/api/v1/progress/{{ $('Register Video').item.json.uuid }}"
|
||||
|
||||
Output:
|
||||
{
|
||||
"uuid": "a1b10138a6bbb0cd",
|
||||
"processors": [
|
||||
{"name": "asr", "status": "complete", "message": "1867 segments"},
|
||||
{"name": "asrx", "status": "progress", "message": "ASRX_TRANSCRIBING"},
|
||||
{"name": "story", "status": "pending", "message": ""}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
> **注意**: API 現在返回 `file_path`(檔案系統路徑)而非 `media_url`(網頁 URL)。如需在網頁中播放影片,請將檔案路徑轉換為可訪問的 URL(例如透過 SFTPGo 分享連結)。
|
||||
|
||||
---
|
||||
|
||||
### Node ⑩: Log Progress (記錄進度)
|
||||
|
||||
```yaml
|
||||
Node Name: "Log Progress"
|
||||
Node Type: "Code"
|
||||
|
||||
Configuration:
|
||||
Language: "JavaScript"
|
||||
|
||||
Code:
|
||||
```javascript
|
||||
const progress = $input.first().json;
|
||||
const processors = progress.processors;
|
||||
|
||||
const totalProcessors = processors.length;
|
||||
const completedProcessors = processors.filter(p => p.status === 'complete').length;
|
||||
const overallProgress = Math.round((completedProcessors / totalProcessors) * 100);
|
||||
|
||||
const currentProcessor = processors.find(p =>
|
||||
p.status === 'progress' || p.status === 'info'
|
||||
);
|
||||
|
||||
const progressMessage = `
|
||||
═══════════════════════════════════════════════
|
||||
📹 Video RAG Processing: ${overallProgress}%
|
||||
UUID: ${progress.uuid}
|
||||
|
||||
${processors.map(p => {
|
||||
const icon = p.status === 'complete' ? '✅' :
|
||||
p.status === 'progress' || p.status === 'info' ? '🔄' : '⏳';
|
||||
return ` ${icon} ${p.name.padEnd(6)} ${p.message || p.status}`;
|
||||
}).join('\n')}
|
||||
|
||||
${currentProcessor ? `Current: ${currentProcessor.name}` : 'All complete!'}
|
||||
═══════════════════════════════════════════════
|
||||
`.trim();
|
||||
|
||||
console.log(progressMessage);
|
||||
|
||||
return {
|
||||
json: {
|
||||
uuid: progress.uuid,
|
||||
overall_progress: overallProgress,
|
||||
completed_processors: completedProcessors,
|
||||
total_processors: totalProcessors,
|
||||
current_processor: currentProcessor?.name || 'idle',
|
||||
processors: processors,
|
||||
log_message: progressMessage
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
Output:
|
||||
{
|
||||
"uuid": "a1b10138a6bbb0cd",
|
||||
"overall_progress": 33,
|
||||
"log_message": "📹 Video RAG Processing: 33%..."
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Node ⑪: Is Complete? (判斷分支)
|
||||
|
||||
```yaml
|
||||
Node Name: "Is Complete?"
|
||||
Node Type: "IF"
|
||||
|
||||
Configuration:
|
||||
Condition:
|
||||
$json.processors.every(p => p.status === 'complete')
|
||||
|
||||
Connections:
|
||||
TRUE (完成): → Node ⑫ Natural Language Search
|
||||
FALSE (未完成): → Node ⑧ Wait 10 Seconds (Loop)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Node ⑫: Natural Language Search (RAG 搜尋)
|
||||
|
||||
```yaml
|
||||
Node Name: "Natural Language Search"
|
||||
Node Type: "HTTP Request"
|
||||
|
||||
Configuration:
|
||||
Method: POST
|
||||
URL: "http://localhost:3002/api/v1/search"
|
||||
|
||||
Body Content Type: "JSON"
|
||||
|
||||
Body:
|
||||
{
|
||||
"query": "{{ $('Set Variables').item.json.query }}",
|
||||
"limit": 10,
|
||||
"uuid": "{{ $('Register Video').item.json.uuid }}"
|
||||
}
|
||||
|
||||
Output:
|
||||
{
|
||||
"results": [
|
||||
{
|
||||
"uuid": "a1b10138a6bbb0cd",
|
||||
"chunk_id": "c_001",
|
||||
"text": "Hello and welcome to the old-time movie show...",
|
||||
"score": 0.92
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Node ⑫B: Hybrid Search (Vector + BM25)
|
||||
|
||||
**說明**: 使用混合搜尋,結合向量相似度和全文檢索
|
||||
|
||||
```yaml
|
||||
Node Name: "Hybrid Search"
|
||||
Node Type: "HTTP Request"
|
||||
|
||||
Configuration:
|
||||
Method: POST
|
||||
URL: "http://localhost:3002/api/v1/search/hybrid"
|
||||
|
||||
Body Content Type: "JSON"
|
||||
|
||||
Body:
|
||||
{
|
||||
"query": "{{ $('Set Variables').item.json.query }}",
|
||||
"limit": 10,
|
||||
"uuid": "{{ $('Register Video').item.json.uuid }}",
|
||||
"vector_weight": 0.7,
|
||||
"bm25_weight": 0.3
|
||||
}
|
||||
|
||||
Output:
|
||||
{
|
||||
"query": "What is the movie about?",
|
||||
"results": [
|
||||
{
|
||||
"uuid": "a1b10138a6bbb0cd",
|
||||
"chunk_id": "c_001",
|
||||
"chunk_type": "sentence",
|
||||
"start_time": 0.0,
|
||||
"end_time": 5.0,
|
||||
"text": "Hello and welcome to the old-time movie show...",
|
||||
"vector_score": 0.85,
|
||||
"bm25_score": 0.75,
|
||||
"combined_score": 0.80
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**權重建議**:
|
||||
| 查詢類型 | vector_weight | bm25_weight |
|
||||
|----------|---------------|-------------|
|
||||
| 主題查詢 | 0.8 | 0.2 |
|
||||
| 事實查找 | 0.5 | 0.5 |
|
||||
| 平衡查詢 | 0.7 | 0.3 |
|
||||
|
||||
---
|
||||
|
||||
### Node ⑬: Get Media URL (取得媒體連結)
|
||||
|
||||
```yaml
|
||||
Node Name: "Get Media URL"
|
||||
Node Type: "HTTP Request"
|
||||
|
||||
Configuration:
|
||||
Method: POST
|
||||
URL: "http://localhost:3002/api/v1/n8n/search"
|
||||
|
||||
Body Content Type: "JSON"
|
||||
|
||||
Body:
|
||||
{
|
||||
"query": "{{ $('Set Variables').item.json.query }}",
|
||||
"limit": 10,
|
||||
"uuid": "{{ $('Register Video').item.json.uuid }}"
|
||||
}
|
||||
|
||||
Output:
|
||||
{
|
||||
"count": 10,
|
||||
"hits": [
|
||||
{
|
||||
"id": "c_001",
|
||||
"vid": "a1b10138a6bbb0cd",
|
||||
"text": "Hello and welcome to the old-time movie show...",
|
||||
"score": 0.92,
|
||||
"file_path": "/Users/accusys/momentry/var/sftpgo/data/demo/video.mp4"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Node ⑭: Build Response (組合結果)
|
||||
|
||||
```yaml
|
||||
Node Name: "Build Response"
|
||||
Node Type: "Set"
|
||||
|
||||
Configuration:
|
||||
Keep Only Set: true
|
||||
|
||||
Variables:
|
||||
- Name: "ok"
|
||||
Value: true
|
||||
|
||||
- Name: "uuid"
|
||||
Value: "{{ $('Register Video').item.json.uuid }}"
|
||||
|
||||
- Name: "file_name"
|
||||
Value: "{{ $('Set Variables').item.json.file_name }}"
|
||||
|
||||
- Name: "query"
|
||||
Value: "{{ $('Set Variables').item.json.query }}"
|
||||
|
||||
- Name: "count"
|
||||
Value: "{{ $('Get Media URL').item.json.count }}"
|
||||
|
||||
- Name: "results"
|
||||
Value: "{{ $('Get Media URL').item.json.hits }}"
|
||||
|
||||
- Name: "overall_progress"
|
||||
Value: "{{ $('Log Progress').item.json.overall_progress }}"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Node ⑮: Respond to Webhook (回傳結果)
|
||||
|
||||
```yaml
|
||||
Node Name: "Respond to Webhook"
|
||||
Node Type: "Respond to Webhook"
|
||||
|
||||
Configuration:
|
||||
Respond With: "JSON"
|
||||
|
||||
Response Body:
|
||||
{
|
||||
"ok": true,
|
||||
"uuid": "{{ $json.uuid }}",
|
||||
"file_name": "{{ $json.file_name }}",
|
||||
"query": "{{ $json.query }}",
|
||||
"count": {{ $json.count }},
|
||||
"results": {{ $json.results }},
|
||||
"overall_progress": {{ $json.overall_progress }},
|
||||
"message": "Video RAG completed successfully"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 快速複製所需資訊
|
||||
|
||||
### SFTPGo 設定
|
||||
| 項目 | 值 |
|
||||
|------|-----|
|
||||
| API Base | `http://localhost:8080/api/v2` |
|
||||
| Demo User | `demo` |
|
||||
| Demo Password | `demopassword123` |
|
||||
| Demo Home | `/Users/accusys/sftpgo_test/demo` |
|
||||
| Token Endpoint | `/api/v2/user/token` |
|
||||
| Upload Endpoint | `/api/v2/user/files` |
|
||||
| Share Endpoint | `/api/v2/user/shares` |
|
||||
|
||||
### Momentry 設定
|
||||
| 項目 | 值 |
|
||||
|------|-----|
|
||||
| API Base | `http://localhost:3002` |
|
||||
| Authentication | `X-API-Key` header (所有 `/api/v1/*` 端點) |
|
||||
| Register | `POST /api/v1/register` |
|
||||
| Progress | `GET /api/v1/progress/{uuid}` |
|
||||
| Search | `POST /api/v1/search` |
|
||||
| n8n Search | `POST /api/v1/n8n/search` |
|
||||
| Hybrid Search | `POST /api/v1/search/hybrid` |
|
||||
| Media Base | `https://wp.momentry.ddns.net` (僅供參考,API 返回 `file_path` 而非 URL) |
|
||||
|
||||
### Demo 測試資料
|
||||
|
||||
**Charade (1963) Demo Video**
|
||||
- UUID: `a1b10138a6bbb0cd`
|
||||
- 位置: `/Users/accusys/test_video/Old_Time_Movie_Show_-_Charade_1963.HD.mov`
|
||||
- 時長: 6872 秒 (~1.9 小時)
|
||||
|
||||
**已處理檔案**:
|
||||
| 檔案 | 大小 | 內容 |
|
||||
|------|------|------|
|
||||
| `asr.json` | 210KB | 1867 語音區段 |
|
||||
| `cut.json` | 220KB | 1331 場景 |
|
||||
| `story.json` | 1.8MB | 641 父子區塊 |
|
||||
| `transcript.txt` | 40KB | 可讀文字稿 |
|
||||
|
||||
**Output 目錄**: `/Users/accusys/momentry_core_0.1/output`
|
||||
|
||||
---
|
||||
|
||||
## 版本歷史
|
||||
|
||||
| 日期 | 版本 | 變更 |
|
||||
|------|------|------|
|
||||
| 2026-03-22 | v1.0 | 初始建立 |
|
||||
| 2026-03-22 | v1.1 | 新增 Hybrid Search (Vector + BM25) 節點 |
|
||||
| 2026-03-22 | v1.2 | 簡化為只處理 ASR、ASRX、STORY 模組 |
|
||||
@@ -1,269 +0,0 @@
|
||||
# n8n HTTP Request Node 設定指南
|
||||
|
||||
| 項目 | 內容 |
|
||||
|------|------|
|
||||
| 建立者 | OpenCode |
|
||||
| 建立時間 | 2026-03-26 |
|
||||
| 文件版本 | V1.1 |
|
||||
|
||||
---
|
||||
|
||||
## 版本歷史
|
||||
|
||||
| 版本 | 日期 | 目的 | 操作人 | 工具/模型 |
|
||||
|------|------|------|--------|-----------|
|
||||
| V1.0 | 2026-03-23 | 創建文件 | Warren | OpenCode / MiniMax M2.5 |
|
||||
| V1.1 | 2026-03-26 | 新增 API Key 驗證說明,更新 curl 範例 | OpenCode | deepseek-reasoner |
|
||||
|
||||
---
|
||||
|
||||
> **API URL 說明**:
|
||||
> - **本地測試**: `http://localhost:3002`
|
||||
> - **n8n workflow**: `https://api.momentry.ddns.net`
|
||||
>
|
||||
> ⚠️ 在 n8n 中請使用 `api.momentry.ddns.net`,不要使用 `localhost:3002`
|
||||
|
||||
---
|
||||
|
||||
## 錯誤排除
|
||||
|
||||
### 錯誤訊息: "Your request is invalid or could not be processed by the service"
|
||||
|
||||
這通常表示 HTTP Request Node 的設定不正確。
|
||||
|
||||
---
|
||||
|
||||
## 正確的 Node 設定方式
|
||||
|
||||
### 方法 1: 使用 JSON Body (推薦)
|
||||
|
||||
```
|
||||
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 }}"
|
||||
│ }
|
||||
├── Send Headers: ✓ (checked)
|
||||
└── Header Parameters:
|
||||
└── X-API-Key: {{ $env.MOMENTRY_API_KEY }}
|
||||
```
|
||||
|
||||
### 方法 2: 使用 Raw Body + Headers
|
||||
|
||||
```
|
||||
Node: HTTP Request
|
||||
├── URL: https://api.momentry.ddns.net/api/v1/n8n/search
|
||||
├── Method: POST
|
||||
├── Authentication: None
|
||||
├── Send Body: ✓ (checked)
|
||||
├── Specify Body: Using JSON
|
||||
├── JSON Body:
|
||||
│ {
|
||||
│ "query": "charade",
|
||||
│ "limit": 3
|
||||
│ }
|
||||
├── Send Headers: ✓ (checked)
|
||||
└── Header Parameters:
|
||||
├── Content-Type: application/json
|
||||
└── X-API-Key: {{ $env.MOMENTRY_API_KEY }}
|
||||
```
|
||||
|
||||
### 方法 3: 最簡單的 Hardcoded 測試
|
||||
|
||||
```
|
||||
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
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 常見錯誤與解決
|
||||
|
||||
### ❌ 錯誤 1: Body 格式錯誤
|
||||
|
||||
**錯誤設定:**
|
||||
```
|
||||
Body Parameters:
|
||||
query = {{ $json.query }}
|
||||
limit = {{ $json.limit }}
|
||||
```
|
||||
|
||||
**正確設定:**
|
||||
```
|
||||
Content Type: JSON
|
||||
Body:
|
||||
{
|
||||
"query": "={{ $json.query }}",
|
||||
"limit": "={{ $json.limit }}"
|
||||
}
|
||||
```
|
||||
|
||||
### ❌ 錯誤 2: 缺少引號
|
||||
|
||||
**錯誤:**
|
||||
```json
|
||||
{
|
||||
query: "charade",
|
||||
limit: 3
|
||||
}
|
||||
```
|
||||
|
||||
**正確:**
|
||||
```json
|
||||
{
|
||||
"query": "charade",
|
||||
"limit": 3
|
||||
}
|
||||
```
|
||||
|
||||
### ❌ 錯誤 3: URL 錯誤
|
||||
|
||||
**錯誤:**
|
||||
```
|
||||
URL: http://localhost:3002/api/v1/n8n/search
|
||||
```
|
||||
|
||||
**正確:**
|
||||
```
|
||||
URL: https://api.momentry.ddns.net/api/v1/n8n/search
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 測試步驟
|
||||
|
||||
### 步驟 1: 創建最簡單的測試
|
||||
|
||||
1. 新建工作流程
|
||||
2. 添加 **Manual Trigger** Node
|
||||
3. 添加 **HTTP Request** Node
|
||||
4. 設定如下:
|
||||
- URL: `https://api.momentry.ddns.net/api/v1/n8n/search`
|
||||
- Method: POST
|
||||
- Send Body: ✓
|
||||
- Content Type: JSON
|
||||
- Body: `{"query": "charade", "limit": 2}`
|
||||
|
||||
### 步驟 2: 執行測試
|
||||
|
||||
1. 點擊 **Execute Workflow**
|
||||
2. 查看 HTTP Request Node 的輸出
|
||||
3. 應該看到 JSON 回應
|
||||
|
||||
### 步驟 3: 確認成功
|
||||
|
||||
成功的回應應該包含:
|
||||
```json
|
||||
{
|
||||
"query": "charade",
|
||||
"count": 2,
|
||||
"hits": [...]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 直接複製使用的工作流程 JSON
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "Video Search - Working Example",
|
||||
"nodes": [
|
||||
{
|
||||
"parameters": {},
|
||||
"name": "When clicking \"Execute Workflow\"",
|
||||
"type": "n8n-nodes-base.manualTrigger",
|
||||
"typeVersion": 1,
|
||||
"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
|
||||
},
|
||||
"options": {}
|
||||
},
|
||||
"name": "Search Video API",
|
||||
"type": "n8n-nodes-base.httpRequest",
|
||||
"typeVersion": 4.1,
|
||||
"position": [450, 300]
|
||||
}
|
||||
],
|
||||
"connections": {
|
||||
"When clicking \"Execute Workflow\"": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Search Video API",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
"executionOrder": "v1"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**導入方式:**
|
||||
1. 在 n8n UI 中,點擊左上角的 Menu
|
||||
2. 選擇 **Import from File**
|
||||
3. 選擇上面的 JSON 文件
|
||||
|
||||
---
|
||||
|
||||
## 驗證 API 可用性
|
||||
|
||||
在終端機測試:
|
||||
```bash
|
||||
# 需要 API Key 驗證 (設定環境變數或直接替換)
|
||||
export MOMENTRY_API_KEY="muser_68600856036340bcafc01930eb4bd839_1774418104_97221b69"
|
||||
|
||||
curl -X POST https://api.momentry.ddns.net/api/v1/n8n/search \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "X-API-Key: $MOMENTRY_API_KEY" \
|
||||
-d '{"query":"charade","limit":2}'
|
||||
```
|
||||
|
||||
如果 curl 成功但 n8n 失敗,問題在於 n8n HTTP Request Node 的設定。
|
||||
|
||||
---
|
||||
|
||||
## 需要幫助?
|
||||
|
||||
如果仍然無法工作:
|
||||
1. 開啟工作流程
|
||||
2. 點擊 HTTP Request Node
|
||||
3. 點擊右上角的 **Execute Node** 單獨執行
|
||||
4. 查看錯誤訊息的詳細內容
|
||||
5. 檢查 Network tab 中的 request/response
|
||||
|
||||
---
|
||||
|
||||
## 相關文件
|
||||
|
||||
- [API_INDEX.md](./API_INDEX.md) - 文件總覽(起點)
|
||||
- [API_N8N_GUIDE.md](./API_N8N_GUIDE.md) - n8n 快速使用指南
|
||||
- [API_ENDPOINTS.md](./API_ENDPOINTS.md) - 端點完整說明
|
||||
@@ -1,575 +0,0 @@
|
||||
# Momentry n8n 整合使用手冊
|
||||
|
||||
| 項目 | 內容 |
|
||||
|------|------|
|
||||
| 建立者 | Warren |
|
||||
| 建立時間 | 2026-03-23 |
|
||||
| 文件版本 | V1.1 |
|
||||
|
||||
---
|
||||
|
||||
## 版本歷史
|
||||
|
||||
| 版本 | 日期 | 目的 | 操作人 | 工具/模型 |
|
||||
|------|------|------|--------|-----------|
|
||||
| V1.0 | 2026-03-22 | 創建 n8n 整合手冊 | Warren | OpenCode |
|
||||
| V1.1 | 2026-03-23 | 新增 API Key 驗證與完整工作流範例 | Warren | OpenCode |
|
||||
|
||||
---
|
||||
|
||||
**目標讀者**: n8n 使用者、DevOps
|
||||
|
||||
---
|
||||
|
||||
## 目錄
|
||||
|
||||
1. [概述](#1-概述)
|
||||
2. [前置作業](#2-前置作業)
|
||||
3. [建立 n8n API Key](#3-建立-n8n-api-key)
|
||||
4. [在 n8n 中使用 Momentry API](#4-在-n8n-中使用-momentry-api)
|
||||
5. [工作流範例](#5-工作流範例)
|
||||
6. [常見問題](#6-常見問題)
|
||||
|
||||
---
|
||||
|
||||
## 1. 概述
|
||||
|
||||
### 1.1 什麼是 n8n?
|
||||
|
||||
n8n 是一個開源的工作流自動化工具,可以連接各種服務和 API。
|
||||
|
||||
### 1.2 為什麼需要整合?
|
||||
|
||||
| 場景 | 說明 |
|
||||
|------|------|
|
||||
| 自動化影片處理 | 新影片上傳時自動觸發處理流程 |
|
||||
| 監控告警 | API Key 異常時發送通知 |
|
||||
| 定時備份 | 定期備份 API Key 資料 |
|
||||
| 跨系統同步 | 與其他系統同步 API Key 狀態 |
|
||||
|
||||
### 1.3 架構圖
|
||||
|
||||
```
|
||||
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
|
||||
│ 觸發器 │────▶│ n8n 工作流 │────▶│ Momentry │
|
||||
│ (Webhook/ │ │ (處理邏輯) │ │ API │
|
||||
│ Cron) │ └─────────────┘ └─────────────┘
|
||||
└─────────────┘ │ │
|
||||
│ │
|
||||
▼ ▼
|
||||
┌─────────────┐ ┌─────────────┐
|
||||
│ 通知 │ │ 動作 │
|
||||
│ (Slack/Email)│ │ (建立/查詢) │
|
||||
└─────────────┘ └─────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 1.4 API URL 選擇
|
||||
|
||||
| 環境 | URL | 說明 |
|
||||
|------|-----|------|
|
||||
| **本地測試** | `http://localhost:3002` | 直接訪問 API |
|
||||
| **n8n workflow** | `https://api.momentry.ddns.net` | 通過反向代理 |
|
||||
|
||||
> ⚠️ **重要**: 在 n8n HTTP Request Node 中,請使用 `https://api.momentry.ddns.net` 而非 `localhost:3002`,因為 n8n 需要從外部訪問 API。
|
||||
|
||||
**本地測試時**:
|
||||
```bash
|
||||
curl http://localhost:3002/health
|
||||
```
|
||||
|
||||
**n8n Workflow 中**:
|
||||
```
|
||||
URL: https://api.momentry.ddns.net/api/v1/n8n/search
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. 前置作業
|
||||
|
||||
### 2.1 確認服務狀態
|
||||
|
||||
```bash
|
||||
# 檢查 Momentry API
|
||||
curl http://localhost:3002/health
|
||||
|
||||
# 檢查 n8n
|
||||
curl https://n8n.momentry.ddns.net/api/v1/workflows \
|
||||
-H "X-N8N-API-KEY: your-api-key"
|
||||
```
|
||||
|
||||
### 2.2 取得 n8n API Key
|
||||
|
||||
#### 方式 A: 透過 Momentry CLI
|
||||
|
||||
```bash
|
||||
# 建立 n8n API Key
|
||||
momentry n8n create \
|
||||
--api-key "your-existing-n8n-api-key" \
|
||||
--label "momentry-integration" \
|
||||
--expires-in-days 90
|
||||
|
||||
# 輸出:
|
||||
# ✅ n8n API Key created successfully!
|
||||
# API Key: eyJhbGciOiJIUzI1NiIs...
|
||||
```
|
||||
|
||||
#### 方式 B: 透過 n8n 介面
|
||||
|
||||
1. 登入 n8n: https://n8n.momentry.ddns.net
|
||||
2. 前往 Settings → n8n API
|
||||
3. 點擊「Create an API Key」
|
||||
4. 複製 API Key
|
||||
|
||||
### 2.3 設定環境變數
|
||||
|
||||
在 n8n 中設定以下環境變數:
|
||||
|
||||
| 變數名稱 | 值 | 說明 |
|
||||
|----------|-----|------|
|
||||
| `MOMENTRY_API_URL` | `http://localhost:3002` | Momentry API URL |
|
||||
| `MOMENTRY_API_KEY` | `your-api-key` | Momentry API Key |
|
||||
|
||||
---
|
||||
|
||||
## 3. 建立 n8n API Key
|
||||
|
||||
### 3.1 CLI 命令
|
||||
|
||||
```bash
|
||||
# 基本建立
|
||||
momentry n8n create \
|
||||
--api-key <existing-n8n-key> \
|
||||
--label <key-name>
|
||||
|
||||
# 設定過期時間
|
||||
momentry n8n create \
|
||||
--api-key <existing-n8n-key> \
|
||||
--label "ci-pipeline" \
|
||||
--expires-in-days 30
|
||||
```
|
||||
|
||||
### 3.2 範例:建立監控用 Key
|
||||
|
||||
```bash
|
||||
momentry n8n create \
|
||||
--api-key "n8n_api_1234567890" \
|
||||
--label "monitoring-key" \
|
||||
--expires-in-days 180
|
||||
|
||||
# 輸出:
|
||||
# ✅ n8n API Key created successfully!
|
||||
# Key ID: abc123-def456
|
||||
# Label: monitoring-key
|
||||
# API Key: eyJhbGciOiJIUz...
|
||||
```
|
||||
|
||||
### 3.3 列出已建立的 Key
|
||||
|
||||
```bash
|
||||
momentry n8n list --api-key <existing-n8n-key>
|
||||
|
||||
# 輸出:
|
||||
# 📋 n8n API Keys
|
||||
# ┌────────────────────────────────────────────────────────────────────────────┐
|
||||
# │ Label │ ID │
|
||||
# ├────────────────────────────────────────────────────────────────────────────┤
|
||||
# │ monitoring-key │ abc123-def456 │
|
||||
# │ ci-pipeline │ xyz789-abc123 │
|
||||
# └────────────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. 在 n8n 中使用 Momentry API
|
||||
|
||||
> ⚠️ **注意**: API Key 管理端點 (`/api/v1/api-keys/*`) 目前仍在規劃中,尚未實作。以下為規劃中的 API 說明。
|
||||
|
||||
### 4.1 設定 HTTP Request 節點
|
||||
|
||||
```
|
||||
Method: POST
|
||||
URL: {{ $env.MOMENTRY_API_URL }}/api/v1/api-keys
|
||||
Headers:
|
||||
X-API-Key: {{ $env.MOMENTRY_API_KEY }}
|
||||
Content-Type: application/json
|
||||
Body:
|
||||
{
|
||||
"name": "auto-generated-key",
|
||||
"key_type": "service",
|
||||
"ttl_days": 90
|
||||
}
|
||||
```
|
||||
|
||||
### 4.2 可用的 API 端點
|
||||
|
||||
> ⚠️ **API Key 管理端點為規劃功能,目前尚未實作**
|
||||
|
||||
#### 已實作端點
|
||||
|
||||
| 方法 | 端點 | 說明 |
|
||||
|------|------|------|
|
||||
| GET | `/health` | 健康檢查 |
|
||||
| POST | `/api/v1/search` | 語意搜尋 |
|
||||
| POST | `/api/v1/n8n/search` | n8n 格式搜尋 |
|
||||
| GET | `/api/v1/videos` | 列出所有影片 |
|
||||
| GET | `/api/v1/lookup` | 查詢影片 |
|
||||
| GET | `/api/v1/progress/:uuid` | 處理進度 |
|
||||
|
||||
#### 規劃中端點 *(尚未實作)*
|
||||
|
||||
| 方法 | 端點 | 說明 |
|
||||
|------|------|------|
|
||||
| GET | `/api/v1/api-keys` | 列出所有 API Keys |
|
||||
| POST | `/api/v1/api-keys` | 建立新的 API Key |
|
||||
| DELETE | `/api/v1/api-keys/{id}` | 刪除 API Key |
|
||||
| POST | `/api/v1/api-keys/{id}/rotate` | 請求 Key 輪換 |
|
||||
|
||||
---
|
||||
|
||||
## 5. 工作流範例
|
||||
|
||||
### 範例 1:定時檢查 API Key 狀態
|
||||
|
||||
**目的**: 每天早上 9 點檢查即將過期的 API Keys
|
||||
|
||||
```
|
||||
┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐
|
||||
│ Schedule │────▶│ HTTP │────▶│ Filter │────▶│ Slack │
|
||||
│ (每天 9AM) │ │ Request │ │ (7天內過期) │ │ 通知 │
|
||||
└─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘
|
||||
```
|
||||
|
||||
**HTTP Request 設定**:
|
||||
|
||||
```json
|
||||
{
|
||||
"method": "GET",
|
||||
"url": "{{ $env.MOMENTRY_API_URL }}/api/v1/api-keys",
|
||||
"headers": {
|
||||
"X-API-Key": "{{ $env.MOMENTRY_API_KEY }}"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Filter 設定**:
|
||||
|
||||
```javascript
|
||||
// 檢查是否在 7 天內過期
|
||||
const expiresAt = new Date($json.expires_at);
|
||||
const now = new Date();
|
||||
const sevenDaysLater = new Date(now.getTime() + 7 * 24 * 60 * 60 * 1000);
|
||||
|
||||
return expiresAt <= sevenDaysLater && expiresAt > now;
|
||||
```
|
||||
|
||||
**Slack 通知格式**:
|
||||
|
||||
```
|
||||
⚠️ API Key 即將過期提醒
|
||||
|
||||
以下 API Key 將在 7 天內過期:
|
||||
{{ $json.map(k => `• ${k.name} (${k.key_id}) - 過期時間: ${k.expires_at}`).join('\n') }}
|
||||
|
||||
請及時處理!
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 範例 2:新影片上傳時自動建立 API Key
|
||||
|
||||
**目的**: 當有新影片上傳時,自動為該影片建立專用 API Key
|
||||
|
||||
```
|
||||
┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐
|
||||
│ Webhook │────▶│ HTTP │────▶│ Set │────▶│ Respond │
|
||||
│ 觸發器 │ │ Request │ │ (組裝回應) │ │ Webhook │
|
||||
└─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘
|
||||
```
|
||||
|
||||
**Webhook 設定**:
|
||||
|
||||
```
|
||||
Method: POST
|
||||
Path: /new-video
|
||||
```
|
||||
|
||||
**HTTP Request 設定**:
|
||||
|
||||
```json
|
||||
{
|
||||
"method": "POST",
|
||||
"url": "{{ $env.MOMENTRY_API_URL }}/api/v1/api-keys",
|
||||
"headers": {
|
||||
"X-API-Key": "{{ $env.MOMENTRY_API_KEY }}"
|
||||
},
|
||||
"body": {
|
||||
"name": "video-{{ $json.video_id }}",
|
||||
"key_type": "service",
|
||||
"permissions": ["read"],
|
||||
"ttl_days": 30
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**回應格式**:
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"video_id": "{{ $json.video_id }}",
|
||||
"api_key_id": "{{ $json.key_id }}",
|
||||
"message": "API Key 已建立"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 範例 3:API Key 異常告警
|
||||
|
||||
**目的**: 監控 API Key 異常使用,發送告警通知
|
||||
|
||||
```
|
||||
┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐
|
||||
│ Webhook │────▶│ Switch │────▶│ Email │ │ Slack │
|
||||
│ (異常通知) │ │ (嚴重程度) │────▶│ 通知 │ │ 通知 │
|
||||
└─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘
|
||||
```
|
||||
|
||||
**Webhook 觸發設定** (在 Momentry 中):
|
||||
|
||||
```bash
|
||||
export WEBHOOK_URL="https://n8n.momentry.ddns.net/webhook/anomaly-alert"
|
||||
export WEBHOOK_EVENTS="anomaly_detected,rate_limited,ip_blocked"
|
||||
```
|
||||
|
||||
**Switch 節點設定**:
|
||||
|
||||
```javascript
|
||||
// 根據嚴重程度分流
|
||||
switch ($json.data.severity) {
|
||||
case 'critical':
|
||||
return 0; // Email + Slack
|
||||
case 'high':
|
||||
return 1; // Slack only
|
||||
default:
|
||||
return 2; // Log only
|
||||
}
|
||||
```
|
||||
|
||||
**Email 通知格式**:
|
||||
|
||||
```
|
||||
Subject: [{{ $json.data.severity }}] API Key 異常告警 - {{ $json.data.key_id }}
|
||||
|
||||
Dear Admin,
|
||||
|
||||
偵測到 API Key 異常使用:
|
||||
|
||||
Key ID: {{ $json.data.key_id }}
|
||||
異常類型: {{ $json.data.anomaly_type }}
|
||||
嚴重程度: {{ $json.data.severity }}
|
||||
時間: {{ $json.timestamp }}
|
||||
|
||||
請立即檢查系統狀態。
|
||||
|
||||
Best regards,
|
||||
Momentry Monitoring
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 範例 4:定時備份 API Key 統計
|
||||
|
||||
**目的**: 每週一早上備份 API Key 統計報表
|
||||
|
||||
```
|
||||
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
|
||||
│ Schedule │────▶│ HTTP │────▶│ Google │
|
||||
│ (每週一) │ │ Request │ │ Sheets │
|
||||
└─────────────┘ └─────────────┘ └─────────────┘
|
||||
```
|
||||
|
||||
**HTTP Request 設定**:
|
||||
|
||||
```json
|
||||
{
|
||||
"method": "GET",
|
||||
"url": "{{ $env.MOMENTRY_API_URL }}/api/v1/api-keys/stats",
|
||||
"headers": {
|
||||
"X-API-Key": "{{ $env.MOMENTRY_API_KEY }}"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Google Sheets 設定**:
|
||||
|
||||
```
|
||||
Spreadsheet: Momentry API Key Reports
|
||||
Sheet: Weekly Stats
|
||||
Append Row:
|
||||
- Date: {{ $now }}
|
||||
- Total Keys: {{ $json.total_keys }}
|
||||
- Active Keys: {{ $json.active_keys }}
|
||||
- Expired Keys: {{ $json.expired_keys }}
|
||||
- Anomalies (24h): {{ $json.anomalies_last_24h }}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 範例 5:自動輪換即將過期的 Key
|
||||
|
||||
**目的**: 自動為即將過期的 Key 請求輪換
|
||||
|
||||
```
|
||||
┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐
|
||||
│ Schedule │────▶│ HTTP │────▶│ Filter │────▶│ HTTP │
|
||||
│ (每天) │ │ (取得 Keys) │ │ (即將過期) │ │ (請求輪換) │
|
||||
└─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘
|
||||
```
|
||||
|
||||
**第一個 HTTP Request (取得 Keys)**:
|
||||
|
||||
```json
|
||||
{
|
||||
"method": "GET",
|
||||
"url": "{{ $env.MOMENTRY_API_URL }}/api/v1/api-keys",
|
||||
"headers": {
|
||||
"X-API-Key": "{{ $env.MOMENTRY_API_KEY }}"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Filter 設定**:
|
||||
|
||||
```javascript
|
||||
// 14 天內過期且未請求輪換的 Key
|
||||
const expiresAt = new Date($json.expires_at);
|
||||
const now = new Date();
|
||||
const fourteenDaysLater = new Date(now.getTime() + 14 * 24 * 60 * 60 * 1000);
|
||||
|
||||
return expiresAt <= fourteenDaysLater &&
|
||||
!$json.rotation_required &&
|
||||
$json.status === 'active';
|
||||
```
|
||||
|
||||
**第二個 HTTP Request (請求輪換)**:
|
||||
|
||||
```json
|
||||
{
|
||||
"method": "POST",
|
||||
"url": "{{ $env.MOMENTRY_API_URL }}/api/v1/api-keys/{{ $json.key_id }}/rotate",
|
||||
"headers": {
|
||||
"X-API-Key": "{{ $env.MOMENTRY_API_KEY }}"
|
||||
},
|
||||
"body": {
|
||||
"reason": "auto_rotation_approaching_expiry"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. 常見問題
|
||||
|
||||
### Q1: n8n 無法連接到 Momentry API
|
||||
|
||||
**檢查項目**:
|
||||
|
||||
```bash
|
||||
# 1. 確認 API 是否運行
|
||||
curl http://localhost:3002/health
|
||||
|
||||
# 2. 確認 API Key 是否有效
|
||||
momentry api-key validate --key "your-key"
|
||||
|
||||
# 3. 檢查防火牆設定
|
||||
nc -zv localhost 3002
|
||||
```
|
||||
|
||||
### Q2: API Key 建立失敗
|
||||
|
||||
**可能原因**:
|
||||
- API Key 已存在
|
||||
- 權限不足
|
||||
- 格式錯誤
|
||||
|
||||
**解決方式**:
|
||||
|
||||
```bash
|
||||
# 檢查現有 Keys
|
||||
momentry api-key list
|
||||
|
||||
# 使用不同的 label
|
||||
momentry n8n create --api-key "..." --label "unique-label-$(date +%s)"
|
||||
```
|
||||
|
||||
### Q3: Webhook 通知沒有收到
|
||||
|
||||
**檢查項目**:
|
||||
|
||||
```bash
|
||||
# 1. 確認 Webhook URL 設定
|
||||
echo $WEBHOOK_URL
|
||||
|
||||
# 2. 測試 Webhook
|
||||
curl -X POST $WEBHOOK_URL \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"test": true}'
|
||||
|
||||
# 3. 檢查 n8n 工作流是否啟用
|
||||
# 登入 n8n → 檢查工作流狀態
|
||||
```
|
||||
|
||||
### Q4: 如何撤銷 n8n API Key
|
||||
|
||||
```bash
|
||||
# 列出所有 Keys
|
||||
momentry n8n list --api-key "your-admin-key"
|
||||
|
||||
# 刪除指定 Key
|
||||
momentry n8n delete \
|
||||
--api-key "your-admin-key" \
|
||||
--label "key-to-delete"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 附錄
|
||||
|
||||
### A. n8n 工作流匯出
|
||||
|
||||
將上述範例工作流匯入 n8n:
|
||||
|
||||
1. 複製工作流 JSON
|
||||
2. 登入 n8n
|
||||
3. 點擊「+」→「Import from File」
|
||||
4. 貼上 JSON 並儲存
|
||||
|
||||
### B. 環境變數總覽
|
||||
|
||||
```bash
|
||||
# Momentry
|
||||
MOMENTRY_API_URL=http://localhost:3002
|
||||
MOMENTRY_API_KEY=your-api-key
|
||||
|
||||
# n8n
|
||||
N8N_URL=https://n8n.momentry.ddns.net
|
||||
N8N_API_KEY=your-n8n-api-key
|
||||
|
||||
# Webhook
|
||||
WEBHOOK_URL=https://n8n.momentry.ddns.net/webhook/alerts
|
||||
WEBHOOK_SECRET=your-secret
|
||||
WEBHOOK_EVENTS=anomaly_detected,key_expired,key_revoked
|
||||
```
|
||||
|
||||
### C. 相關文件
|
||||
|
||||
| 文件 | 說明 |
|
||||
|------|------|
|
||||
| [API_INDEX.md](./API_INDEX.md) | 文件總覽(起點) |
|
||||
| [API_N8N_GUIDE.md](./API_N8N_GUIDE.md) | n8n 快速使用指南 |
|
||||
| `docs/API_KEY_MANAGEMENT.md` | API Key 管理系統設計 |
|
||||
| `docs/API_KEY_ARCHITECTURE.md` | 系統架構圖 |
|
||||
| `docs/API_KEY_INTEGRATION_TESTS.md` | 整合測試文件 |
|
||||
@@ -1,227 +0,0 @@
|
||||
# OpenCode n8n MCP 整合設定
|
||||
|
||||
| 項目 | 內容 |
|
||||
|------|------|
|
||||
| 建立者 | Warren |
|
||||
| 建立時間 | 2026-03-23 |
|
||||
| 文件版本 | V1.0 |
|
||||
|
||||
---
|
||||
|
||||
## 版本歷史
|
||||
|
||||
| 版本 | 日期 | 目的 | 操作人 | 工具/模型 |
|
||||
|------|------|------|--------|-----------|
|
||||
| V1.0 | 2026-03-23 | 創建 n8n MCP 整合設定文件 | Warren | OpenCode |
|
||||
|
||||
---
|
||||
|
||||
> 建立時間: 2026-03-23
|
||||
> 更新時間: 2026-03-23
|
||||
|
||||
---
|
||||
|
||||
## n8n MCP 工具列表 (43 個)
|
||||
|
||||
### Workflows (10)
|
||||
| 工具 | 說明 |
|
||||
|------|------|
|
||||
| `n8n_list_workflows` | 列出所有 workflows |
|
||||
| `n8n_get_workflow` | 取得 workflow 詳情 |
|
||||
| `n8n_create_workflow` | 建立新 workflow |
|
||||
| `n8n_update_workflow` | 更新 workflow |
|
||||
| `n8n_delete_workflow` | 刪除 workflow |
|
||||
| `n8n_activate_workflow` | 啟用 workflow |
|
||||
| `n8n_deactivate_workflow` | 停用 workflow |
|
||||
| `n8n_execute_workflow` | 執行 workflow |
|
||||
| `n8n_get_workflow_tags` | 取得 workflow 標籤 |
|
||||
| `n8n_update_workflow_tags` | 更新 workflow 標籤 |
|
||||
|
||||
### Executions (3)
|
||||
| 工具 | 說明 |
|
||||
|------|------|
|
||||
| `n8n_list_executions` | 列出執行記錄 |
|
||||
| `n8n_get_execution` | 取得執行詳情 |
|
||||
| `n8n_delete_execution` | 刪除執行記錄 |
|
||||
|
||||
### Data Tables (8)
|
||||
| 工具 | 說明 |
|
||||
|------|------|
|
||||
| `n8n_list_datatables` | 列出資料表 |
|
||||
| `n8n_create_datatable` | 建立資料表 |
|
||||
| `n8n_get_datatable` | 取得資料表結構 |
|
||||
| `n8n_get_datatable_rows` | 取得資料表列 |
|
||||
| `n8n_insert_datatable_rows` | 插入資料列 |
|
||||
| `n8n_update_datatable_rows` | 更新資料列 |
|
||||
| `n8n_upsert_datatable_row` | 插入或更新資料列 |
|
||||
| `n8n_delete_datatable_rows` | 刪除資料列 |
|
||||
|
||||
### Tags (5)
|
||||
| 工具 | 說明 |
|
||||
|------|------|
|
||||
| `n8n_list_tags` | 列出所有標籤 |
|
||||
| `n8n_get_tag` | 取得標籤 |
|
||||
| `n8n_create_tag` | 建立標籤 |
|
||||
| `n8n_update_tag` | 更新標籤 |
|
||||
| `n8n_delete_tag` | 刪除標籤 |
|
||||
|
||||
### Credentials (4)
|
||||
| 工具 | 說明 |
|
||||
|------|------|
|
||||
| `n8n_list_credentials` | 列出憑證 |
|
||||
| `n8n_create_credential` | 建立憑證 |
|
||||
| `n8n_delete_credential` | 刪除憑證 |
|
||||
| `n8n_get_credential_schema` | 取得憑證 schema |
|
||||
|
||||
### Users (3)
|
||||
| 工具 | 說明 |
|
||||
|------|------|
|
||||
| `n8n_list_users` | 列出使用者 |
|
||||
| `n8n_get_user` | 取得使用者 |
|
||||
| `n8n_delete_user` | 刪除使用者 |
|
||||
|
||||
### Variables (3)
|
||||
| 工具 | 說明 |
|
||||
|------|------|
|
||||
| `n8n_list_variables` | 列出變數 |
|
||||
| `n8n_create_variable` | 建立變數 |
|
||||
| `n8n_delete_variable` | 刪除變數 |
|
||||
|
||||
### 其他 (7)
|
||||
| 工具 | 說明 |
|
||||
|------|------|
|
||||
| `n8n_list_projects` | 列出專案 |
|
||||
| `n8n_create_project` | 建立專案 |
|
||||
| `n8n_update_project` | 更新專案 |
|
||||
| `n8n_delete_project` | 刪除專案 |
|
||||
| `n8n_generate_audit` | 產生安全審計報告 |
|
||||
| `n8n_health_check` | 健康檢查 |
|
||||
| `n8n_trigger_webhook` | 觸發 webhook |
|
||||
|
||||
---
|
||||
|
||||
## 安裝步驟
|
||||
|
||||
### 1. 安裝 n8n MCP Server
|
||||
|
||||
```bash
|
||||
npm install -g @nextoolsolutions/mcp-n8n
|
||||
```
|
||||
|
||||
驗證:
|
||||
```bash
|
||||
which mcp-n8n
|
||||
# /opt/homebrew/bin/mcp-n8n
|
||||
```
|
||||
|
||||
### 2. 取得 n8n API Key
|
||||
|
||||
1. 開啟 n8n UI: `http://localhost:5678`
|
||||
2. 登入後點擊右上角 **Settings** → **API**
|
||||
3. 點擊 **Create New API Key**
|
||||
4. 複製產生的 key
|
||||
|
||||
### 3. 設定 OpenCode MCP 設定檔
|
||||
|
||||
建立或編輯 `~/.config/opencode/opencode.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"$schema": "https://opencode.ai/config.json",
|
||||
"mcp": {
|
||||
"gitea": {
|
||||
"type": "local",
|
||||
"enabled": true,
|
||||
"command": [
|
||||
"/opt/homebrew/bin/gitea-mcp-server",
|
||||
"-token", "<GITEA_TOKEN>",
|
||||
"-host", "http://localhost:3000"
|
||||
]
|
||||
},
|
||||
"n8n": {
|
||||
"type": "local",
|
||||
"enabled": true,
|
||||
"command": ["/opt/homebrew/bin/mcp-n8n"],
|
||||
"environment": {
|
||||
"N8N_BASE_URL": "http://localhost:5678",
|
||||
"N8N_API_KEY": "<N8N_API_KEY>"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4. 驗證 MCP 運作
|
||||
|
||||
重啟 OpenCode,確認 n8n MCP tools 可用。
|
||||
|
||||
---
|
||||
|
||||
## n8n API 端點
|
||||
|
||||
n8n v2 REST API 路徑為 `/rest/`(不是 `/api/v1/`)
|
||||
|
||||
| 端點 | 方法 | 說明 |
|
||||
|------|------|------|
|
||||
| `/rest/workflows` | GET | 列出 workflows |
|
||||
| `/rest/workflows/:id` | GET | 取得 workflow |
|
||||
| `/rest/workflows` | POST | 建立 workflow |
|
||||
| `/rest/workflows/:id` | PUT | 更新 workflow |
|
||||
| `/rest/workflows/:id` | DELETE | 刪除 workflow |
|
||||
| `/rest/workflows/:id/activate` | POST | 啟用 workflow |
|
||||
| `/rest/workflows/:id/deactivate` | POST | 停用 workflow |
|
||||
| `/rest/workflows/:id/execute` | POST | 執行 workflow |
|
||||
|
||||
**認證方式:**
|
||||
```bash
|
||||
curl -H "X-N8N-API-KEY: YOUR_API_KEY" \
|
||||
http://localhost:5678/rest/workflows
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 疑難排解
|
||||
|
||||
### API 404 問題
|
||||
|
||||
如果 API 傳回 404,檢查:
|
||||
|
||||
1. **n8n 是否運行中**
|
||||
```bash
|
||||
curl http://localhost:5678
|
||||
```
|
||||
|
||||
2. **n8n 初始設定(重要!)**
|
||||
- 第一次使用必須在瀏覽器完成初始化
|
||||
- 開啟 `http://localhost:5678`
|
||||
- 按照畫面指示建立管理員帳號
|
||||
- 完成後才能使用 API
|
||||
|
||||
3. **API Key 是否正確**
|
||||
```bash
|
||||
curl -H "X-N8N-API-KEY: YOUR_KEY" \
|
||||
http://localhost:5678/rest/workflows
|
||||
```
|
||||
|
||||
### n8n 初始設定(第一次使用)
|
||||
|
||||
1. 開啟瀏覽器: `http://localhost:5678`
|
||||
2. 輸入 email 和密碼建立管理員帳號
|
||||
3. 完成後進入 Settings → API
|
||||
4. 建立 API Key 並複製
|
||||
|
||||
### CLI Import vs PostgreSQL
|
||||
|
||||
n8n 使用 PostgreSQL 儲存資料:
|
||||
- CLI `n8n import:workflow` 可能寫入 SQLite
|
||||
- 手動在 UI import 會寫入 PostgreSQL
|
||||
|
||||
建議直接使用 UI 或 MCP import。
|
||||
|
||||
---
|
||||
|
||||
## 相關文件
|
||||
|
||||
- [OPENCODE_GUIDE.md](./OPENCODE_GUIDE.md) - OpenCode 使用規範
|
||||
- [INSTALL_N8N.md](./INSTALL_N8N.md) - n8n 安裝指南
|
||||
- [N8N_DEMO_WORKFLOW.md](./N8N_DEMO_WORKFLOW.md) - n8n Workflow 範例
|
||||
@@ -1,194 +0,0 @@
|
||||
# n8n MCP 整合測試報告
|
||||
|
||||
| 項目 | 內容 |
|
||||
|------|------|
|
||||
| 建立者 | Warren |
|
||||
| 建立時間 | 2026-03-23 |
|
||||
| 文件版本 | V1.0 |
|
||||
|
||||
---
|
||||
|
||||
## 版本歷史
|
||||
|
||||
| 版本 | 日期 | 目的 | 操作人 | 工具/模型 |
|
||||
|------|------|------|--------|-----------|
|
||||
| V1.0 | 2026-03-23 | 創建測試報告 | Warren | OpenCode |
|
||||
|
||||
---
|
||||
|
||||
## 測試日期
|
||||
2026-03-23
|
||||
|
||||
## 測試環境
|
||||
- **n8n Version**: 2.3.5
|
||||
- **n8n URL**: http://localhost:5678
|
||||
- **MCP Server**: @nextoolsolutions/mcp-n8n v2.0.0
|
||||
- **OpenCode Config**: ~/.config/opencode/opencode.json
|
||||
|
||||
## 測試結果
|
||||
|
||||
### ✅ 所有測試通過
|
||||
|
||||
| 測試項目 | 狀態 | 詳細說明 |
|
||||
|---------|------|---------|
|
||||
| MCP 伺服器初始化 | ✅ 通過 | Protocol version 2024-11-05 |
|
||||
| 工具列表載入 | ✅ 通過 | 43 個工具可用 |
|
||||
| 工具呼叫 (list_workflows) | ✅ 通過 | 成功返回 5 個 workflows |
|
||||
| API 連線 | ✅ 通過 | http://localhost:5678 |
|
||||
|
||||
## 可用工具 (43 個)
|
||||
|
||||
### Workflows (10)
|
||||
- `n8n_list_workflows` - 列出所有工作流程
|
||||
- `n8n_get_workflow` - 取得工作流程詳情
|
||||
- `n8n_create_workflow` - 建立新工作流程
|
||||
- `n8n_update_workflow` - 更新工作流程
|
||||
- `n8n_delete_workflow` - 刪除工作流程
|
||||
- `n8n_activate_workflow` - 啟動工作流程
|
||||
- `n8n_deactivate_workflow` - 停止工作流程
|
||||
- `n8n_execute_workflow` - 執行工作流程
|
||||
- `n8n_get_workflow_tags` - 取得工作流程標籤
|
||||
- `n8n_update_workflow_tags` - 更新工作流程標籤
|
||||
|
||||
### Executions (3)
|
||||
- `n8n_list_executions` - 列出執行記錄
|
||||
- `n8n_get_execution` - 取得執行詳情
|
||||
- `n8n_delete_execution` - 刪除執行記錄
|
||||
|
||||
### Data Tables (6)
|
||||
- `n8n_list_datatables` - 列出資料表
|
||||
- `n8n_create_datatable` - 建立資料表
|
||||
- `n8n_get_datatable` - 取得資料表
|
||||
- `n8n_get_datatable_rows` - 查詢資料表資料
|
||||
- `n8n_insert_datatable_rows` - 插入資料
|
||||
- `n8n_update_datatable_rows` - 更新資料
|
||||
- `n8n_upsert_datatable_row` - 更新或插入
|
||||
- `n8n_delete_datatable_rows` - 刪除資料
|
||||
|
||||
### Tags (5)
|
||||
- `n8n_list_tags` - 列出標籤
|
||||
- `n8n_get_tag` - 取得標籤
|
||||
- `n8n_create_tag` - 建立標籤
|
||||
- `n8n_update_tag` - 更新標籤
|
||||
- `n8n_delete_tag` - 刪除標籤
|
||||
|
||||
### Credentials (4)
|
||||
- `n8n_list_credentials` - 列出認證
|
||||
- `n8n_create_credential` - 建立認證
|
||||
- `n8n_delete_credential` - 刪除認證
|
||||
- `n8n_get_credential_schema` - 取得認證結構
|
||||
|
||||
### Users (3)
|
||||
- `n8n_list_users` - 列出使用者
|
||||
- `n8n_get_user` - 取得使用者
|
||||
- `n8n_delete_user` - 刪除使用者
|
||||
|
||||
### Variables (3)
|
||||
- `n8n_list_variables` - 列出變數
|
||||
- `n8n_create_variable` - 建立變數
|
||||
- `n8n_delete_variable` - 刪除變數
|
||||
|
||||
### Projects (4)
|
||||
- `n8n_list_projects` - 列出專案
|
||||
- `n8n_create_project` - 建立專案
|
||||
- `n8n_update_project` - 更新專案
|
||||
- `n8n_delete_project` - 刪除專案
|
||||
|
||||
### System (3)
|
||||
- `n8n_generate_audit` - 產生安全稽核報告
|
||||
- `n8n_health_check` - 健康檢查
|
||||
- `n8n_trigger_webhook` - 觸發 Webhook
|
||||
|
||||
## 配置檔案
|
||||
|
||||
### ~/.config/opencode/opencode.json
|
||||
```json
|
||||
{
|
||||
"$schema": "https://opencode.ai/config.json",
|
||||
"mcp": {
|
||||
"gitea": {
|
||||
"type": "local",
|
||||
"enabled": true,
|
||||
"command": [
|
||||
"/opt/homebrew/bin/gitea-mcp-server",
|
||||
"-token", "<GITEA_TOKEN>",
|
||||
"-host", "http://localhost:3000"
|
||||
]
|
||||
},
|
||||
"n8n": {
|
||||
"type": "local",
|
||||
"enabled": true,
|
||||
"command": ["/opt/homebrew/bin/mcp-n8n"],
|
||||
"environment": {
|
||||
"N8N_BASE_URL": "http://localhost:5678",
|
||||
"N8N_API_KEY": "<N8N_API_KEY>"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 測試範例
|
||||
|
||||
### 列出工作流程
|
||||
```bash
|
||||
# 使用 curl
|
||||
curl -H "X-N8N-API-KEY: <API_KEY>" http://localhost:5678/api/v1/workflows
|
||||
|
||||
# 使用 MCP
|
||||
echo '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"n8n_list_workflows","arguments":{"limit":5}}}' | mcp-n8n
|
||||
```
|
||||
|
||||
### 建立工作流程
|
||||
```bash
|
||||
# 使用 curl
|
||||
curl -X POST \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "X-N8N-API-KEY: <API_KEY>" \
|
||||
-d '{"name":"My Workflow","nodes":[],"connections":{}}' \
|
||||
http://localhost:5678/api/v1/workflows
|
||||
|
||||
# 使用 MCP
|
||||
echo '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"n8n_create_workflow","arguments":{"workflow":{"name":"My Workflow","nodes":[],"connections":{}}}}' | mcp-n8n
|
||||
```
|
||||
|
||||
## 現有工作流程 (範例)
|
||||
|
||||
測試中成功讀取的工作流程:
|
||||
|
||||
1. **Diagnostic: Environment Test** (ID: 4vaf6dKznkTccuyC)
|
||||
- 狀態: Active
|
||||
- 用途: 環境測試與時間同步驗證
|
||||
|
||||
2. **Simple Test Webhook v2** (ID: 38bbM14sGo0eVCuW)
|
||||
- 狀態: Active
|
||||
- 用途: Webhook 測試
|
||||
|
||||
3. **HL Chat Searching - RAG Only** (ID: 6Y9c7mGtye4DjuENR5Kbg)
|
||||
- 狀態: Inactive
|
||||
- 用途: RAG 聊天搜尋整合
|
||||
|
||||
4. **HL Embedding with AccusysDB** (ID: 61nRs3BeNGlBtuYJFLSFn)
|
||||
- 狀態: Inactive
|
||||
- 用途: Qdrant 向量資料庫嵌入
|
||||
|
||||
5. **HL Embedding with AccusysDB (local)** (ID: 017oYPE7cDpvybAn)
|
||||
- 狀態: Archived
|
||||
- 用途: 本地測試版本
|
||||
|
||||
## 結論
|
||||
|
||||
✅ **n8n MCP 整合測試全部通過!**
|
||||
|
||||
MCP 伺服器已成功配置並運作,可以透過 OpenCode 使用所有 43 個 n8n 管理工具。
|
||||
|
||||
### 建議用途
|
||||
1. **自動化工作流程管理** - 使用 AI 協助建立、修改、監控工作流程
|
||||
2. **批次執行** - 透過 MCP 批量管理工作流程
|
||||
3. **監控與稽核** - 自動化執行記錄檢視與安全稽核
|
||||
4. **整合測試** - 與 Momentry Core Video RAG 整合測試
|
||||
|
||||
### 下一步
|
||||
- 使用 OpenCode 建立 Video RAG 整合工作流程
|
||||
- 設定自動化監控與告警
|
||||
- 建立工作流程模板庫
|
||||
@@ -1,152 +0,0 @@
|
||||
# Momentry Video RAG - n8n 工作流程設定完成
|
||||
|
||||
## ✅ 最終成功版本
|
||||
|
||||
| 項目 | 內容 |
|
||||
|------|------|
|
||||
| **工作流程名稱** | Video Search - Working v3 |
|
||||
| **ID** | 4vQo8I4SXEaR5E1A |
|
||||
| **狀態** | ✅ SUCCESS |
|
||||
| **執行 ID** | 1620 |
|
||||
|
||||
---
|
||||
|
||||
## 成功關鍵
|
||||
|
||||
### HTTP Request Node 正確設定
|
||||
```json
|
||||
{
|
||||
"url": "https://api.momentry.ddns.net/api/v1/n8n/search",
|
||||
"method": "POST",
|
||||
"sendBody": true,
|
||||
"specifyBody": "json",
|
||||
"jsonBody": "{\"query\":\"charade\",\"limit\":3}",
|
||||
"options": {}
|
||||
}
|
||||
```
|
||||
|
||||
**重點**:
|
||||
- ✅ `specifyBody`: "json" (不是 "body")
|
||||
- ✅ `jsonBody`: 字串格式 (不是物件)
|
||||
- ✅ 使用 `"{\"query\":\"..."}` 轉義引號
|
||||
|
||||
---
|
||||
|
||||
## 所有可用工作流程
|
||||
|
||||
| 工作流程 | ID | 狀態 | 說明 |
|
||||
|---------|-----|------|------|
|
||||
| Video Search - Working v3 | 4vQo8I4SXEaR5E1A | ✅ 成功 | **推薦使用** |
|
||||
| Video Search - HTTP Only | tZbljQCFZDOJ4C0s | ❌ 失敗 | body 格式錯誤 |
|
||||
| Video Search - Debug Simple | e2CMjonwILMUYjp0 | ⚠️ 待測 | Code Node 版本 |
|
||||
| Video Search - Instant | zC5K3TbFzWGAh0la | ❌ 失敗 | `$httpRequest` 不可用 |
|
||||
|
||||
---
|
||||
|
||||
## 如何使用
|
||||
|
||||
### 方法 1: 直接執行
|
||||
```bash
|
||||
# 開啟工作流程
|
||||
open https://n8n.momentry.ddns.net/workflow/4vQo8I4SXEaR5E1A
|
||||
```
|
||||
|
||||
然後:
|
||||
1. 點擊 **"Execute Workflow"** ▶️
|
||||
2. 點擊 **"Show Result"** 節點
|
||||
3. 查看 JSON 結果
|
||||
|
||||
### 方法 2: 修改搜尋關鍵字
|
||||
1. 點擊 **"Search API"** 節點
|
||||
2. 修改 `jsonBody`:
|
||||
```json
|
||||
"{\"query\":\"您的關鍵字\",\"limit\":5}"
|
||||
```
|
||||
3. 儲存並重新執行
|
||||
|
||||
---
|
||||
|
||||
## API 端點
|
||||
|
||||
### Momentry Core API
|
||||
```
|
||||
POST https://api.momentry.ddns.net/api/v1/n8n/search
|
||||
Content-Type: application/json
|
||||
|
||||
Body:
|
||||
{
|
||||
"query": "charade",
|
||||
"limit": 3,
|
||||
"uuid": "可選的影片UUID"
|
||||
}
|
||||
```
|
||||
|
||||
### 直接測試
|
||||
```bash
|
||||
curl -X POST https://api.momentry.ddns.net/api/v1/n8n/search \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"query":"charade","limit":3}'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 已建立的文件
|
||||
|
||||
| 文件 | 路徑 | 內容 |
|
||||
|------|------|------|
|
||||
| API URL 範例 | `docs/API_URL_EXAMPLES.md` | 完整 URL 和 curl 指令 |
|
||||
| HTTP Request 指南 | `docs/N8N_HTTP_REQUEST_GUIDE.md` | Node 設定說明 |
|
||||
| 輸出查看指南 | `docs/N8N_VIEW_OUTPUT_GUIDE.md` | 如何查看結果 |
|
||||
| MCP 測試報告 | `docs/N8N_MCP_TEST_REPORT.md` | 43 個 MCP 工具 |
|
||||
| API 修復總結 | `docs/N8N_API_FIX_SUMMARY.md` | 問題修復過程 |
|
||||
| 工作流程 JSON | `docs/n8n_workflow_video_rag_mcp.json` | 原始工作流程 |
|
||||
| 測試腳本 | `docs/test_all.sh` | 自動測試腳本 |
|
||||
|
||||
---
|
||||
|
||||
## 服務狀態
|
||||
|
||||
✅ **Momentry Core**: https://api.momentry.ddns.net (Port 3002)
|
||||
✅ **n8n**: https://n8n.momentry.ddns.net (Port 5678)
|
||||
✅ **MCP 整合**: 43 個工具可用
|
||||
|
||||
---
|
||||
|
||||
## 下一步建議
|
||||
|
||||
### 1. 建立帶有參數的工作流程
|
||||
修改現有工作流程,讓 query 和 limit 可以動態輸入:
|
||||
- 添加 Webhook Node 接收外部請求
|
||||
- 或使用 Set Node 設定變數
|
||||
|
||||
### 2. 建立完整的 RAG 流程
|
||||
結合 OpenAI:
|
||||
- 搜尋影片片段
|
||||
- 使用 GPT 生成回答
|
||||
- 回傳格式化的 RAG 結果
|
||||
|
||||
### 3. 自動化監控
|
||||
- 建立定時執行的工作流程
|
||||
- 監控 API 健康狀態
|
||||
- 發送 Telegram/Email 通知
|
||||
|
||||
---
|
||||
|
||||
## 問題排除
|
||||
|
||||
如果再次遇到 "Your request is invalid":
|
||||
1. 檢查 `specifyBody` 必須設為 `"json"`
|
||||
2. `jsonBody` 必須是字串格式,不是物件
|
||||
3. 確保使用正確的 JSON 轉義: `{\"key\":\"value\"}`
|
||||
|
||||
---
|
||||
|
||||
## 完成!🎉
|
||||
|
||||
所有設定已完成:
|
||||
- ✅ n8n REST API 修復並運作正常
|
||||
- ✅ MCP 整合完成 (43 個工具)
|
||||
- ✅ Momentry Core API 可外部存取
|
||||
- ✅ 成功的工作流程已創建並測試
|
||||
|
||||
您可以開始使用 n8n 自動化管理 Momentry Core 了!
|
||||
@@ -1,301 +0,0 @@
|
||||
# n8n Video Search 工作流程 - 成功設定指南
|
||||
|
||||
| 項目 | 內容 |
|
||||
|------|------|
|
||||
| 建立者 | Warren |
|
||||
| 建立時間 | 2026-03-22 |
|
||||
| 文件版本 | V1.1 |
|
||||
| 適用版本 | n8n 2.3.5 |
|
||||
|
||||
---
|
||||
|
||||
## 版本歷史
|
||||
|
||||
| 版本 | 日期 | 目的 | 操作人 | 工具/模型 |
|
||||
|------|------|------|--------|-----------|
|
||||
| V1.0 | 2026-03-22 | 創建文件 | Warren | OpenCode |
|
||||
| V1.1 | 2026-03-26 | 更新 API 範例,新增 X-API-Key 驗證標頭 | OpenCode | deepseek-reasoner |
|
||||
|
||||
---
|
||||
|
||||
## ✅ 成功案例
|
||||
|
||||
| 項目 | 內容 |
|
||||
|------|------|
|
||||
| **工作流程名稱** | Video Search - Working v3 |
|
||||
| **ID** | 4vQo8I4SXEaR5E1A |
|
||||
| **狀態** | ✅ 成功運作 |
|
||||
| **測試結果** | 成功搜尋 "charade",返回 3 個結果 |
|
||||
|
||||
---
|
||||
|
||||
## 正確的 HTTP Request Node 設定
|
||||
|
||||
### 成功的設定方式
|
||||
|
||||
```json
|
||||
{
|
||||
"url": "https://api.momentry.ddns.net/api/v1/n8n/search",
|
||||
"method": "POST",
|
||||
"sendBody": true,
|
||||
"specifyBody": "json",
|
||||
"jsonBody": "{\"query\":\"charade\",\"limit\":3}",
|
||||
"options": {
|
||||
"headers": {
|
||||
"X-API-Key": "demo_api_key_12345"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 關鍵設定說明
|
||||
|
||||
| 設定項目 | 正確值 | 錯誤值 | 說明 |
|
||||
|---------|--------|--------|------|
|
||||
| **specifyBody** | `"json"` | `"body"` | 必須選擇 `"json"` |
|
||||
| **jsonBody** | 字串 `"{...}"` | 物件 `{}` | 必須是 JSON 字串格式 |
|
||||
| **轉義** | `\"query\"` | `"query"` | 引號需要轉義 |
|
||||
|
||||
---
|
||||
|
||||
## 工作流程架構
|
||||
|
||||
```
|
||||
┌─────────────────────────┐
|
||||
│ Manual Trigger │ ← 點擊 "Execute Workflow"
|
||||
└───────────┬─────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────┐
|
||||
│ HTTP Request Node │ ← 呼叫 Momentry API
|
||||
│ - specifyBody: "json" │
|
||||
│ - jsonBody: "{...}" │
|
||||
└───────────┬─────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────┐
|
||||
│ Code Node │ ← 格式化輸出
|
||||
│ - console.log() │
|
||||
│ - return json │
|
||||
└─────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 完整的 Workflow JSON
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "Video Search - Working v3",
|
||||
"nodes": [
|
||||
{
|
||||
"parameters": {},
|
||||
"name": "When clicking \"Execute Workflow\"",
|
||||
"type": "n8n-nodes-base.manualTrigger",
|
||||
"typeVersion": 1,
|
||||
"position": [250, 300]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"url": "https://api.momentry.ddns.net/api/v1/n8n/search",
|
||||
"method": "POST",
|
||||
"sendBody": true,
|
||||
"specifyBody": "json",
|
||||
"jsonBody": "{\"query\":\"charade\",\"limit\":3}",
|
||||
"options": {
|
||||
"headers": {
|
||||
"X-API-Key": "demo_api_key_12345"
|
||||
}
|
||||
}
|
||||
},
|
||||
"name": "Search API",
|
||||
"type": "n8n-nodes-base.httpRequest",
|
||||
"typeVersion": 4.1,
|
||||
"position": [450, 300]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"jsCode": "const data = $input.first().json;\nconsole.log('Response:', JSON.stringify(data, null, 2));\nreturn [{ json: data }];"
|
||||
},
|
||||
"name": "Show Result",
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 2,
|
||||
"position": [650, 300]
|
||||
}
|
||||
],
|
||||
"connections": {
|
||||
"When clicking \"Execute Workflow\"": {
|
||||
"main": [[{"node": "Search API", "type": "main", "index": 0}]]
|
||||
},
|
||||
"Search API": {
|
||||
"main": [[{"node": "Show Result", "type": "main", "index": 0}]]
|
||||
}
|
||||
},
|
||||
"settings": {"executionOrder": "v1"},
|
||||
"staticData": null
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 使用步驟
|
||||
|
||||
### 步驟 1: 導入工作流程
|
||||
1. 開啟 n8n UI: `https://n8n.momentry.ddns.net`
|
||||
2. 點擊 **Add Workflow** (+)
|
||||
3. 點擊 **Import from File**
|
||||
4. 選擇上面的 JSON 檔案
|
||||
|
||||
### 步驟 2: 執行測試
|
||||
1. 點擊 **"Execute Workflow"** 按鈕 (▶️)
|
||||
2. 等待執行完成
|
||||
3. 點擊 **"Show Result"** 節點
|
||||
4. 查看右側 **JSON** 面板
|
||||
|
||||
### 步驟 3: 修改搜尋關鍵字
|
||||
1. 點擊 **"Search API"** 節點
|
||||
2. 修改 `jsonBody`:
|
||||
```json
|
||||
"{\"query\":\"您的關鍵字\",\"limit\":5}"
|
||||
```
|
||||
3. 點擊 **Save** (Ctrl+S)
|
||||
4. 重新執行
|
||||
|
||||
---
|
||||
|
||||
## 成功的回應範例
|
||||
|
||||
```json
|
||||
{
|
||||
"query": "charade",
|
||||
"count": 3,
|
||||
"hits": [
|
||||
{
|
||||
"id": "sentence_0006",
|
||||
"vid": "a1b10138a6bbb0cd",
|
||||
"start": 48.8,
|
||||
"end": 55.44,
|
||||
"title": "Chunk sentence_0006",
|
||||
"text": "fun plot twists, Woody Dialog and charming performances...",
|
||||
"score": 0.526,
|
||||
"file_path": "/Users/accusys/momentry/var/sftpgo/data/demo/Old_Time_Movie_Show_-_Charade_1963.HD.mov"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 常見錯誤與解決
|
||||
|
||||
### ❌ 錯誤 1: "Your request is invalid"
|
||||
|
||||
**原因**: `specifyBody` 設為 `"body"` 而不是 `"json"`
|
||||
|
||||
**解決**:
|
||||
```json
|
||||
✅ "specifyBody": "json"
|
||||
❌ "specifyBody": "body"
|
||||
```
|
||||
|
||||
### ❌ 錯誤 2: "$httpRequest is not defined"
|
||||
|
||||
**原因**: Code Node 中使用 `$httpRequest`,但您的 n8n 版本不支援
|
||||
|
||||
**解決**: 使用 **HTTP Request Node** 代替 Code Node
|
||||
|
||||
### ❌ 錯誤 3: Body 格式錯誤
|
||||
|
||||
**原因**: `body` 使用物件格式 `{query: "..."}`
|
||||
|
||||
**解決**: 使用 `jsonBody` 字串格式 `{"query":"..."}`
|
||||
|
||||
### ❌ 錯誤 4: JSON 引號未轉義
|
||||
|
||||
**原因**: `"{query: "charade"}"` - 引號衝突
|
||||
|
||||
**解決**: `\"` 轉義 `"{\"query\":\"charade\"}"`
|
||||
|
||||
---
|
||||
|
||||
## 測試指令
|
||||
|
||||
### 直接測試 API
|
||||
```bash
|
||||
curl -X POST https://api.momentry.ddns.net/api/v1/n8n/search \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "X-API-Key: demo_api_key_12345" \
|
||||
-d '{"query":"charade","limit":3}'
|
||||
```
|
||||
|
||||
### 驗證服務狀態
|
||||
```bash
|
||||
# 檢查 Momentry Core
|
||||
curl -H "X-API-Key: demo_api_key_12345" https://api.momentry.ddns.net/api/v1/videos
|
||||
|
||||
# 檢查 n8n
|
||||
curl http://localhost:5678/api/v1/workflows \
|
||||
-H "X-N8N-API-KEY: <your_api_key>"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 服務資訊
|
||||
|
||||
| 服務 | URL | 說明 |
|
||||
|------|-----|------|
|
||||
| **n8n UI** | https://n8n.momentry.ddns.net | 工作流程管理 |
|
||||
| **Momentry API** | https://api.momentry.ddns.net | 影片搜尋 API |
|
||||
| **工作流程** | https://n8n.momentry.ddns.net/workflow/4vQo8I4SXEaR5E1A | 成功案例 |
|
||||
|
||||
---
|
||||
|
||||
## 進階使用
|
||||
|
||||
### 添加 Webhook 觸發器
|
||||
|
||||
如果你想從外部呼叫這個工作流程:
|
||||
|
||||
1. 在第一個節點前添加 **Webhook** Node
|
||||
2. 設定:
|
||||
```
|
||||
Method: POST
|
||||
Path: video-search
|
||||
Response Mode: Last Node
|
||||
```
|
||||
3. 將 Webhook 連接到 Search API
|
||||
4. 儲存並執行
|
||||
5. 使用生成的 Webhook URL 呼叫:
|
||||
```bash
|
||||
curl -X POST <webhook_url> \
|
||||
-d '{"query":"charade","limit":3}'
|
||||
```
|
||||
|
||||
### 使用動態變數
|
||||
|
||||
修改 jsonBody 使用表達式:
|
||||
```json
|
||||
"{\"query\":\"={{ $json.query }}\",\"limit\":{{ $json.limit }}}"
|
||||
```
|
||||
|
||||
然後在前面添加 Set Node 設定變數。
|
||||
|
||||
---
|
||||
|
||||
## 相關文件
|
||||
|
||||
- `docs/N8N_SETUP_COMPLETE.md` - 完整設定總結
|
||||
- `docs/N8N_HTTP_REQUEST_GUIDE.md` - HTTP Request 詳細指南
|
||||
- `docs/API_URL_EXAMPLES.md` - API URL 範例
|
||||
|
||||
---
|
||||
|
||||
## 完成!🎉
|
||||
|
||||
您現在擁有一個可以成功搜尋影片的 n8n 工作流程!
|
||||
|
||||
**關鍵成功要素**:
|
||||
1. ✅ 使用 `specifyBody: "json"`
|
||||
2. ✅ 使用 `jsonBody` 字串格式
|
||||
3. ✅ 正確轉義 JSON 引號
|
||||
4. ✅ 使用外部 API URL (`https://api.momentry.ddns.net`)
|
||||
@@ -1,141 +0,0 @@
|
||||
# n8n 工作流程輸出查看指南
|
||||
|
||||
## 問題:"Node executed successfully but no output data"
|
||||
|
||||
這是正常的!在 n8n 中,你需要**點擊節點**才能看到輸出資料。
|
||||
|
||||
---
|
||||
|
||||
## 如何查看輸出資料
|
||||
|
||||
### 方法 1: 點擊節點查看
|
||||
|
||||
1. 執行工作流程後(點擊 Execute Workflow)
|
||||
2. **點擊任何一個節點**(Node)
|
||||
3. 在右側面板會顯示該節點的輸出
|
||||
4. 查看 **JSON** 分頁看到完整資料
|
||||
|
||||
### 方法 2: 查看執行記錄
|
||||
|
||||
1. 執行工作流程
|
||||
2. 點擊左側的 **"Executions"** 選單
|
||||
3. 找到最近的執行記錄
|
||||
4. 點擊展開查看每個節點的輸出
|
||||
|
||||
### 方法 3: 使用 Respond to Webhook
|
||||
|
||||
如果你想直接看到結果,添加一個 Respond to Webhook 節點:
|
||||
|
||||
```javascript
|
||||
// 在最後一個節點之後添加:
|
||||
Node: Respond to Webhook
|
||||
├── Response Mode: Last Node
|
||||
└── Response Body: {{ JSON.stringify($json) }}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 新增的工作流程
|
||||
|
||||
### ✅ Video Search - Debug Simple
|
||||
- **ID**: e2CMjonwILMUYjp0
|
||||
- **URL**: https://n8n.momentry.ddns.net/workflow/e2CMjonwILMUYjp0
|
||||
|
||||
這個版本保證有輸出!
|
||||
|
||||
### 執行步驟
|
||||
|
||||
1. 開啟工作流程
|
||||
2. 點擊 **"Execute Workflow"** ▶️
|
||||
3. 等待執行完成(約 3-5 秒)
|
||||
4. **點擊最後一個節點** "Step 3 - Show Results"
|
||||
5. 查看右側的 **JSON** 分頁
|
||||
|
||||
### 預期看到的輸出
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "SUCCESS",
|
||||
"query": "charade",
|
||||
"totalResults": 2,
|
||||
"firstHit": {
|
||||
"text": "fun plot twists, Woody Dialog and charming perform...",
|
||||
"time": "48.8s - 55.44s",
|
||||
"score": "53%"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
或如果失敗:
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "FAILED",
|
||||
"error": "error message here"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 截圖說明
|
||||
|
||||
### 執行後的畫面
|
||||
```
|
||||
┌─────────────────────────────────────┐
|
||||
│ When clicking "Execute Workflow" │ ✅ (綠色勾)
|
||||
├─────────────────────────────────────┤
|
||||
│ Step 1 - Set Query │ ✅ (綠色勾)
|
||||
├─────────────────────────────────────┤
|
||||
│ Step 2 - Call API │ ✅ (綠色勾) ← 點擊這裡
|
||||
├─────────────────────────────────────┤
|
||||
│ Step 3 - Show Results │ ✅ (綠色勾) ← 或這個
|
||||
└─────────────────────────────────────┘
|
||||
|
||||
[右側面板 - 點擊節點後顯示]
|
||||
┌─────────────────────────────────────┐
|
||||
│ Node: Step 2 - Call API │
|
||||
│ │
|
||||
│ [JSON] [Table] [Schema] │ ← 點擊 JSON
|
||||
│ │
|
||||
│ { │
|
||||
│ "success": true, │
|
||||
│ "query": "charade", │
|
||||
│ ... │
|
||||
│ } │
|
||||
└─────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 快速測試
|
||||
|
||||
如果不想用瀏覽器,直接在這裡執行:
|
||||
|
||||
```bash
|
||||
# 開啟簡化版工作流程
|
||||
open https://n8n.momentry.ddns.net/workflow/e2CMjonwILMUYjp0
|
||||
```
|
||||
|
||||
然後:
|
||||
1. 點擊 **Execute Workflow**
|
||||
2. 點擊 **"Step 3 - Show Results"** 節點
|
||||
3. 看右側 JSON 面板
|
||||
|
||||
---
|
||||
|
||||
## 如果仍然看不到資料
|
||||
|
||||
檢查:
|
||||
1. ✅ 工作流程已儲存
|
||||
2. ✅ 點擊了正確的節點(有綠色勾的)
|
||||
3. ✅ 右側面板已展開(點擊 JSON 分頁)
|
||||
4. ✅ 沒有紅色錯誤訊息
|
||||
|
||||
---
|
||||
|
||||
## 聯絡支援
|
||||
|
||||
如果還是有問題,請告訴我:
|
||||
1. 你點擊的是哪個節點?
|
||||
2. 右側面板顯示什麼?
|
||||
3. 有沒有紅色錯誤訊息?
|
||||
@@ -1,169 +0,0 @@
|
||||
# Momentry Video RAG MCP Workflow
|
||||
|
||||
## 工作流程資訊
|
||||
|
||||
- **名稱**: Momentry Video RAG MCP
|
||||
- **ID**: WlVvpX2OeKK83QOK
|
||||
- **Webhook Path**: `video-rag-mcp`
|
||||
- **狀態**: ✅ Active (已啟動)
|
||||
- **建立時間**: 2026-03-22
|
||||
|
||||
## 工作流程架構
|
||||
|
||||
```
|
||||
┌─────────────────┐ ┌──────────────────────┐ ┌───────────────────┐ ┌─────────────────┐
|
||||
│ Webhook │────▶│ Search Momentry │────▶│ Process RAG │────▶│ Respond to │
|
||||
│ Trigger │ │ Core │ │ Results │ │ Webhook │
|
||||
└─────────────────┘ └──────────────────────┘ └───────────────────┘ └─────────────────┘
|
||||
│
|
||||
│ POST http://localhost:5678/webhook/video-rag-mcp
|
||||
│
|
||||
▼
|
||||
{
|
||||
"query": "搜尋關鍵字",
|
||||
"limit": 5,
|
||||
"uuid": "可選的影片UUID"
|
||||
}
|
||||
```
|
||||
|
||||
## Node 說明
|
||||
|
||||
### 1. Webhook Trigger
|
||||
- **類型**: Webhook
|
||||
- **Method**: POST
|
||||
- **Path**: `video-rag-mcp`
|
||||
- **Response Mode**: Last Node (等待最後一個節點完成後回應)
|
||||
|
||||
### 2. Search Momentry Core
|
||||
- **類型**: HTTP Request
|
||||
- **URL**: `http://localhost:3002/api/v1/n8n/search`
|
||||
- **Method**: POST
|
||||
- **Body**:
|
||||
```json
|
||||
{
|
||||
"query": "搜尋關鍵字",
|
||||
"limit": 5,
|
||||
"uuid": "可選的影片UUID"
|
||||
}
|
||||
```
|
||||
- **Timeout**: 30秒
|
||||
|
||||
### 3. Process RAG Results
|
||||
- **類型**: Code (JavaScript)
|
||||
- **功能**:
|
||||
- 處理 Momentry Core 搜尋結果
|
||||
- 格式化 hits 為結構化資料
|
||||
- 建立 RAG context(用於 LLM 問答)
|
||||
- 計算相關度百分比
|
||||
|
||||
**輸出格式**:
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"query": "搜尋關鍵字",
|
||||
"totalFound": 5,
|
||||
"context": "[1] 文本內容... (Video: 影片標題, Time: 10s-20s)\n\n[2] ...",
|
||||
"results": [
|
||||
{
|
||||
"index": 1,
|
||||
"id": "chunk_id",
|
||||
"title": "影片標題",
|
||||
"text": "文本內容",
|
||||
"startTime": 10,
|
||||
"endTime": 20,
|
||||
"relevance": "85%",
|
||||
"videoUuid": "uuid",
|
||||
"mediaUrl": "影片URL",
|
||||
"deepLink": "影片URL#t=10,20"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Respond to Webhook
|
||||
- **類型**: Respond to Webhook
|
||||
- **Response**: JSON 格式結果
|
||||
- **Status Code**: 200
|
||||
|
||||
## 使用方式
|
||||
|
||||
### 直接呼叫 Webhook
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:5678/webhook/video-rag-mcp \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"query": "charade",
|
||||
"limit": 5
|
||||
}'
|
||||
```
|
||||
|
||||
### 指定特定影片搜尋
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:5678/webhook/video-rag-mcp \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"query": "audrey hepburn",
|
||||
"limit": 3,
|
||||
"uuid": "a1b10138a6bbb0cd"
|
||||
}'
|
||||
```
|
||||
|
||||
### 在 n8n 工作流程中使用
|
||||
|
||||
可以將此 Webhook 作為子工作流程觸發器,或使用 HTTP Request Node 呼叫:
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "Call Video RAG",
|
||||
"type": "n8n-nodes-base.httpRequest",
|
||||
"parameters": {
|
||||
"url": "http://localhost:5678/webhook/video-rag-mcp",
|
||||
"method": "POST",
|
||||
"body": {
|
||||
"query": "={{ $json.searchTerm }}",
|
||||
"limit": 5
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## RAG Context 用途
|
||||
|
||||
工作流程產生的 `context` 欄位可直接用於 LLM 提示:
|
||||
|
||||
```javascript
|
||||
// Example: 使用 context 進行問答
|
||||
const prompt = `
|
||||
基於以下影片片段資訊回答問題:
|
||||
|
||||
${context}
|
||||
|
||||
問題:${userQuestion}
|
||||
|
||||
請根據上述內容提供準確的答案。
|
||||
`;
|
||||
```
|
||||
|
||||
## 相關文件
|
||||
|
||||
- [Momentry Core API 文件](./API_ACCESS.md)
|
||||
- [n8n MCP 測試報告](./N8N_MCP_TEST_REPORT.md)
|
||||
- [N8N_DEMO_WORKFLOW.md](./N8N_DEMO_WORKFLOW.md) - 完整工作流程設計
|
||||
|
||||
## MCP 建立指令
|
||||
|
||||
此工作流程是透過 MCP 工具建立的:
|
||||
|
||||
```bash
|
||||
# 使用 MCP 建立工作流程
|
||||
node create_workflow.js | mcp-n8n
|
||||
|
||||
# 使用 MCP 啟動工作流程
|
||||
echo '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"n8n_activate_workflow","arguments":{"workflowId":"WlVvpX2OeKK83QOK"}}}' | mcp-n8n
|
||||
```
|
||||
|
||||
## 工作流程檔案
|
||||
|
||||
- 原始檔案: `docs/n8n_workflow_video_rag_mcp.json`
|
||||
450
docs/NODEJS.md
450
docs/NODEJS.md
@@ -1,450 +0,0 @@
|
||||
# Node.js 開發指南
|
||||
|
||||
| 項目 | 內容 |
|
||||
|------|------|
|
||||
| 建立者 | Warren |
|
||||
| 建立時間 | 2026-03-16 |
|
||||
| 文件版本 | V1.0 |
|
||||
|
||||
---
|
||||
|
||||
## 版本歷史
|
||||
|
||||
| 版本 | 日期 | 目的 | 操作人 | 工具/模型 |
|
||||
|------|------|------|--------|-----------|
|
||||
| V1.0 | 2026-03-16 | 創建文件 | Warren | OpenCode / MiniMax M2.5 |
|
||||
|
||||
---
|
||||
|
||||
## 概述
|
||||
|
||||
本文檔說明 Momentry 專案中 Node.js 環境的配置、管理與監控。
|
||||
|
||||
---
|
||||
|
||||
## 當前狀態
|
||||
|
||||
| 項目 | 狀態 |
|
||||
|------|------|
|
||||
| 系統預設 Node.js | v25.8.1 |
|
||||
| 鎖定 Node.js (n8n) | v22.22.1 |
|
||||
| n8n 版本 | v2.3.5 |
|
||||
| npm 路徑 (預設) | /opt/homebrew/bin/npm |
|
||||
| node 路徑 (預設) | /opt/homebrew/bin/node |
|
||||
| node@22 路徑 | /opt/homebrew/opt/node@22/bin/node |
|
||||
|
||||
---
|
||||
|
||||
## 版本策略
|
||||
|
||||
### 為什麼需要版本鎖定?
|
||||
|
||||
n8n 要求 Node.js 版本為 22.x LTS。系統預設的 Node.js 25.x 會導致:
|
||||
- n8n 啟動警告
|
||||
- Task-runner 執行錯誤
|
||||
- 相容性問題
|
||||
|
||||
### 版本對照表
|
||||
|
||||
| 用途 | Node.js 版本 | 路徑 |
|
||||
|------|-------------|------|
|
||||
| 系統預設 | 25.8.1 | /opt/homebrew/bin/node |
|
||||
| n8n 專用 | 22.22.1 | /opt/homebrew/opt/node@22/bin/node |
|
||||
| 開發測試 | 22.x | /opt/homebrew/opt/node@22/bin/node |
|
||||
|
||||
---
|
||||
|
||||
## 安裝步驟
|
||||
|
||||
### 安裝特定版本 Node.js
|
||||
|
||||
```bash
|
||||
# 安裝 Node.js 22.x (LTS)
|
||||
brew install node@22
|
||||
|
||||
# 驗證安裝
|
||||
/opt/homebrew/opt/node@22/bin/node --version
|
||||
# v22.22.1
|
||||
```
|
||||
|
||||
### 安裝全局工具
|
||||
|
||||
```bash
|
||||
# 使用 node@22 安裝全局工具
|
||||
/opt/homebrew/opt/node@22/bin/npm install -g <package-name>
|
||||
|
||||
# 例如安裝 n8n
|
||||
/opt/homebrew/opt/node@22/bin/npm install -g n8n
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 使用方式
|
||||
|
||||
### 方式 1:直接使用完整路徑
|
||||
|
||||
```bash
|
||||
# 使用 node@22
|
||||
/opt/homebrew/opt/node@22/bin/node script.js
|
||||
|
||||
# 使用 npm@22
|
||||
/opt/homebrew/opt/node@22/bin/npm install
|
||||
```
|
||||
|
||||
### 方式 2:修改 PATH 環境變數
|
||||
|
||||
在 shell 配置檔案 (~/.zshrc) 中加入:
|
||||
|
||||
```bash
|
||||
# 優先使用 node@22
|
||||
export PATH="/opt/homebrew/opt/node@22/bin:$PATH"
|
||||
|
||||
# 重新載入
|
||||
source ~/.zshrc
|
||||
|
||||
# 驗證
|
||||
node --version
|
||||
# v22.22.1
|
||||
```
|
||||
|
||||
### 方式 3:使用 nvm (推薦)
|
||||
|
||||
```bash
|
||||
# 安裝 nvm
|
||||
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.0/install.sh | bash
|
||||
|
||||
# 安裝 Node.js 22.x
|
||||
nvm install 22
|
||||
nvm use 22
|
||||
|
||||
# 設定預設版本
|
||||
nvm alias default 22
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## n8n 配置
|
||||
|
||||
### 環境變數設定
|
||||
|
||||
在 plist 或啟動腳本中設定:
|
||||
|
||||
```xml
|
||||
<key>ProgramArguments</key>
|
||||
<array>
|
||||
<string>/opt/homebrew/opt/node@22/bin/node</string>
|
||||
<string>/opt/homebrew/lib/node_modules/n8n/bin/n8n</string>
|
||||
<string>start</string>
|
||||
</array>
|
||||
|
||||
<key>EnvironmentVariables</key>
|
||||
<dict>
|
||||
<key>PATH</key>
|
||||
<string>/opt/homebrew/opt/node@22/bin:/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin</string>
|
||||
</dict>
|
||||
```
|
||||
|
||||
### Task Runner 配置
|
||||
|
||||
n8n Task Runner 用於執行 Code node 中的 JavaScript 代碼。
|
||||
|
||||
確保 PATH 中 node@22 在前面,這樣 task-runner 子進程會使用正確版本。
|
||||
|
||||
---
|
||||
|
||||
## 監控配置
|
||||
|
||||
### 健康檢查
|
||||
|
||||
`monitor/service/health_check.sh` 已包含 Node.js 版本檢查:
|
||||
|
||||
```bash
|
||||
# 檢查 n8n 進程是否使用正確的 Node.js 版本
|
||||
check_node() {
|
||||
local LOCKED_NODE_VERSION="22"
|
||||
# ...
|
||||
}
|
||||
```
|
||||
|
||||
### 執行監控
|
||||
|
||||
```bash
|
||||
# 單獨檢查 Node.js
|
||||
bash /Users/accusys/momentry_core_0.1/monitor/control/monitor_control.sh check node
|
||||
|
||||
# 檢查 Python
|
||||
bash /Users/accusys/momentry_core_0.1/monitor/control/monitor_control.sh check python
|
||||
```
|
||||
|
||||
### 查看版本狀態
|
||||
|
||||
```bash
|
||||
# 查詢資料庫中的版本記錄
|
||||
psql -U accusys -h localhost -d momentry -c "SELECT * FROM node_version_baseline;"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 應用Registry
|
||||
|
||||
記錄系統中所有使用 Node.js 的應用程式。
|
||||
|
||||
| 應用 | Node.js 版本 | 執行路徑 | Port | 狀態 | 說明 |
|
||||
|------|-------------|----------|------|------|------|
|
||||
| n8n | 22.22.1 | /opt/homebrew/opt/node@22/bin/node | 5678/5679 | ✅ 執行中 | 工作流自動化平台 |
|
||||
| markdownlint-cli | 25.x | /opt/homebrew/bin/npm | - | ✅ 已安裝 | Markdown lint 工具 |
|
||||
| - | - | - | - | - | 新增應用請填入此表 |
|
||||
|
||||
---
|
||||
|
||||
## 新增 Node.js 應用決策
|
||||
|
||||
### 決策樹
|
||||
|
||||
```
|
||||
新應用需要 Node.js?
|
||||
│
|
||||
├─ 支援 Node.js 22.x ────→ 使用現有 node@22
|
||||
│ (路徑: /opt/homebrew/opt/node@22/bin/node)
|
||||
│
|
||||
├─ 需要特定版本 (如 18.x, 20.x) ────→ 安裝新版本 node@XX
|
||||
│ │
|
||||
│ ├─ 建立獨立目錄: /opt/homebrew/opt/node@XX/
|
||||
│ ├─ 更新本文檔 Registry
|
||||
│ └─ 建立獨立 plist 使用完整路徑
|
||||
│
|
||||
└─ 需要最新版本 ────→ 使用系統預設 node@25
|
||||
(路徑: /opt/homebrew/bin/node)
|
||||
```
|
||||
|
||||
### 步驟清單
|
||||
|
||||
1. **確認版本需求**
|
||||
```bash
|
||||
# 查看應用支援的 Node.js 版本
|
||||
# 通常在 package.json 或官方文件中說明
|
||||
```
|
||||
|
||||
2. **選擇現有版本或安裝新版本**
|
||||
- 若支援 22.x → 使用 node@22
|
||||
- 若需要特定版本 → 使用完整路徑隔離
|
||||
|
||||
3. **安裝新版本 (如需要)**
|
||||
```bash
|
||||
# 安裝特定版本
|
||||
brew install node@20
|
||||
# 或
|
||||
brew install node@18
|
||||
|
||||
# 驗證安裝
|
||||
/opt/homebrew/opt/node@20/bin/node --version
|
||||
```
|
||||
|
||||
4. **建立隔離的服務配置**
|
||||
- 使用完整路徑,不要依賴 PATH
|
||||
- plist 中明确指定 node 路徑
|
||||
- 設定獨立的環境變數
|
||||
|
||||
5. **更新文件**
|
||||
- 更新本文檔 Registry 表格
|
||||
- 新增應用的安裝文檔
|
||||
- 更新監控配置
|
||||
|
||||
### 隔離原則
|
||||
|
||||
| 原則 | 說明 |
|
||||
|------|------|
|
||||
| **完整路徑** | 使用 `/opt/homebrew/opt/node@XX/bin/node` 而非 `node` |
|
||||
| **獨立 PATH** | plist 中設定隔離的 PATH 環境變數 |
|
||||
| **獨立目錄** | 不同版本的 node 安裝在各自目錄 |
|
||||
| **獨立全局套件** | 每個版本有自己的 node_modules |
|
||||
|
||||
---
|
||||
|
||||
## 版本衝突處理
|
||||
|
||||
### 常見衝突場景
|
||||
|
||||
| 場景 | 原因 | 解決方案 |
|
||||
|------|------|----------|
|
||||
| n8n 顯示版本警告 | 使用 Node.js 25.x 啟動 | 確認 plist 使用 node@22 路徑 |
|
||||
| task-runner 執行錯誤 | 子進程繼承錯誤版本 | 在 plist 中設定正確的 PATH |
|
||||
| 全域套件找不到 | 跨版本安裝 | 使用正確版本的 npm |
|
||||
| Port 被佔用 | 多個應用使用相同 Port | 分配獨立 Port |
|
||||
|
||||
### 診斷命令
|
||||
|
||||
```bash
|
||||
# 1. 查看程序使用的 node 版本
|
||||
ps aux | grep node
|
||||
|
||||
# 2. 查看程序 PID 使用的執行檔
|
||||
lsof -p <PID> | grep bin/node
|
||||
|
||||
# 3. 確認路徑優先順序
|
||||
echo $PATH
|
||||
|
||||
# 4. 查看 launchctl 服務的環境變數
|
||||
sudo launchctl list | grep <service-name>
|
||||
|
||||
# 5. 檢查 Port 佔用
|
||||
lsof -i :<port-number>
|
||||
```
|
||||
|
||||
### 預防措施
|
||||
|
||||
1. **每次新增服務都更新 Registry**
|
||||
2. **使用完整路徑而非 PATH**
|
||||
3. **建立服務時指定版本**
|
||||
4. **定期檢查執行中的程序**
|
||||
|
||||
---
|
||||
|
||||
## 故障排除
|
||||
|
||||
### 問題:n8n 顯示 Node.js 版本警告
|
||||
|
||||
**原因**:n8n 檢測到 Node.js 版本不是 22.x
|
||||
|
||||
**解決方案**:
|
||||
1. 確認 plist 中 ProgramArguments 使用正確路徑
|
||||
2. 確認 PATH 環境變數中 node@22 在前面
|
||||
3. 重載服務:
|
||||
```bash
|
||||
sudo launchctl unload /Library/LaunchDaemons/com.momentry.n8n.main.plist
|
||||
sudo launchctl unload /Library/LaunchDaemons/com.momentry.n8n.worker.plist
|
||||
sudo launchctl load /Library/LaunchDaemons/com.momentry.n8n.main.plist
|
||||
sudo launchctl load /Library/LaunchDaemons/com.momentry.n8n.worker.plist
|
||||
```
|
||||
|
||||
### 問題:Task Runner 使用錯誤版本
|
||||
|
||||
**原因**:PATH 環境變數設定不正確
|
||||
|
||||
**診斷**:
|
||||
```bash
|
||||
# 查看 task-runner 進程使用的 node
|
||||
ps aux | grep "task-runner"
|
||||
lsof -p <PID> | grep "txt" | grep node
|
||||
```
|
||||
|
||||
**解決方案**:
|
||||
更新 plist 中的 PATH,確保 `/opt/homebrew/opt/node@22/bin` 在最前面。
|
||||
|
||||
### 問題:npm 全域套件找不到
|
||||
|
||||
**原因**:使用錯誤的 node 版本安裝
|
||||
|
||||
**解決方案**:
|
||||
```bash
|
||||
# 確認使用的是哪個 node
|
||||
which node
|
||||
node --version
|
||||
|
||||
# 使用正確版本重新安裝
|
||||
/opt/homebrew/opt/node@22/bin/npm install -g <package>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 版本切換腳本
|
||||
|
||||
建立快速切換腳本 `~/bin/switch-node.sh`:
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
|
||||
VERSION=$1
|
||||
|
||||
case $VERSION in
|
||||
22)
|
||||
export PATH="/opt/homebrew/opt/node@22/bin:$PATH"
|
||||
echo "切換到 Node.js 22.x"
|
||||
;;
|
||||
system|25)
|
||||
export PATH="/opt/homebrew/bin:$PATH"
|
||||
echo "切換到系統 Node.js 25.x"
|
||||
;;
|
||||
*)
|
||||
echo "用法: $0 {22|system}"
|
||||
echo " 22 - Node.js 22.x (n8n)"
|
||||
echo " system - Node.js 25.x (系統預設)"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
node --version
|
||||
```
|
||||
|
||||
賦予執行權限:
|
||||
```bash
|
||||
chmod +x ~/bin/switch-node.sh
|
||||
```
|
||||
|
||||
使用方式:
|
||||
```bash
|
||||
~/bin/switch-node.sh 22 # 切換到 22.x
|
||||
~/bin/switch-node.sh system # 切換到系統預設
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 常用指令
|
||||
|
||||
```bash
|
||||
# 查看 node 版本
|
||||
node --version
|
||||
|
||||
# 查看 npm 版本
|
||||
npm --version
|
||||
|
||||
# 查看 node 安裝位置
|
||||
which node
|
||||
|
||||
# 查看 node@22 版本
|
||||
/opt/homebrew/opt/node@22/bin/node --version
|
||||
|
||||
# 安裝全域套件 (使用 node@22)
|
||||
/opt/homebrew/opt/node@22/bin/npm install -g n8n
|
||||
|
||||
# 檢查 n8n 進程
|
||||
ps aux | grep n8n | grep -v grep
|
||||
|
||||
# 檢查 task-runner
|
||||
ps aux | grep task-runner | grep -v grep
|
||||
|
||||
# 查看 task-runner 使用的 node 版本
|
||||
lsof -p <PID> | grep "txt" | grep node
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 檔案位置
|
||||
|
||||
| 類型 | 路徑 | 說明 |
|
||||
|------|------|------|
|
||||
| node (系統) | /opt/homebrew/bin/node | 預設 node |
|
||||
| node@22 | /opt/homebrew/opt/node@22/bin/node | n8n 專用 |
|
||||
| npm (系統) | /opt/homebrew/bin/npm | 預設 npm |
|
||||
| npm@22 | /opt/homebrew/opt/node@22/bin/npm | n8n 專用 |
|
||||
| n8n 安裝 | /opt/homebrew/lib/node_modules/n8n/ | n8n 目錄 |
|
||||
| n8n main plist | /Library/LaunchDaemons/com.momentry.n8n.main.plist | 開機啟動 |
|
||||
| n8n worker plist | /Library/LaunchDaemons/com.momentry.n8n.worker.plist | 開機啟動 |
|
||||
|
||||
---
|
||||
|
||||
## 版本速查
|
||||
|
||||
| 版本 | 用途 | 路徑 |
|
||||
|------|------|------|
|
||||
| 22.22.1 | n8n 專用 | /opt/homebrew/opt/node@22/bin/node |
|
||||
| 25.x | 系統預設 | /opt/homebrew/bin/node |
|
||||
|
||||
---
|
||||
|
||||
## 相關文件
|
||||
|
||||
- [INSTALL_N8N.md](./INSTALL_N8N.md) - n8n 安裝指南 (包含 Node.js 配置範例)
|
||||
- [RUST_DEVELOPMENT.md](./RUST_DEVELOPMENT.md) - Rust 開發規範
|
||||
- [monitor_config.yaml](../monitor/config/monitor_config.yaml) - 監控配置
|
||||
- [node_monitor.sh](../monitor/service/node_monitor.sh) - Node.js 監控腳本
|
||||
@@ -1,430 +0,0 @@
|
||||
# OpenCode 使用規範
|
||||
|
||||
| 項目 | 內容 |
|
||||
|------|------|
|
||||
| 建立者 | Warren |
|
||||
| 建立時間 | 2026-03-21 |
|
||||
| 文件版本 | V1.1 |
|
||||
|
||||
---
|
||||
|
||||
## 版本歷史
|
||||
|
||||
| 版本 | 日期 | 目的 | 操作人 |
|
||||
|------|------|------|--------|
|
||||
| V1.0 | 2026-03-21 | 創建文件 | Warren |
|
||||
| V1.1 | 2026-03-21 | 新增 MCP 設定章節 | OpenCode |
|
||||
|
||||
---
|
||||
|
||||
## 概述
|
||||
|
||||
本文檔定義使用 OpenCode 進行專案開發的最佳實踐,確保開發效率和程式碼品質。
|
||||
|
||||
---
|
||||
|
||||
## 任務管理
|
||||
|
||||
### 任務批次策略
|
||||
|
||||
**原則**: 一次處理 1-2 個功能,完成後驗證,再繼續下一個。
|
||||
|
||||
| 批次大小 | 適用場景 | 說明 |
|
||||
|----------|----------|------|
|
||||
| 1 個 | 緊急修復、簡單任務 | 快速完成 |
|
||||
| 2-3 個 | 一般功能開發 | 平衡效率與品質 |
|
||||
| 3+ 個 | 大型重構 | 需要更詳細的追蹤 |
|
||||
|
||||
### 驗證流程
|
||||
|
||||
每個任務完成後必須執行:
|
||||
|
||||
```bash
|
||||
# 1. 編譯檢查
|
||||
cargo check
|
||||
|
||||
# 2. Lint 檢查
|
||||
cargo clippy --lib
|
||||
|
||||
# 3. 單元測試
|
||||
cargo test --lib
|
||||
|
||||
# 4. 格式化檢查
|
||||
cargo fmt -- --check
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 溝通模式
|
||||
|
||||
### 有效的任務描述
|
||||
|
||||
**建議格式**:
|
||||
```
|
||||
執行 [功能名稱]
|
||||
- 優先級: 高/中/低
|
||||
- 驗收標準: [明確的標準]
|
||||
- 約束: [限制條件]
|
||||
```
|
||||
|
||||
**範例**:
|
||||
```
|
||||
實作 monitor_jobs 表
|
||||
- 優先級: 高
|
||||
- 驗收標準: CRUD 操作可用,單元測試通過
|
||||
- 約束: 使用現有架構
|
||||
```
|
||||
|
||||
### 明確的暫停點
|
||||
|
||||
在每個階段完成後主動詢問:
|
||||
|
||||
```
|
||||
[任務] 完成。要繼續:
|
||||
1. 實作功能 A
|
||||
2. 添加測試
|
||||
3. 更新文檔
|
||||
```
|
||||
|
||||
### 不建議的溝通
|
||||
|
||||
| 模式 | 問題 | 建議 |
|
||||
|------|------|------|
|
||||
| 一次指定太多項目 | 增加複雜度,難以追蹤 | 分批處理 |
|
||||
| 模糊的任務描述 | 難以評估進度 | 使用明確的驗收標準 |
|
||||
| 從不驗證 | 累積問題 | 每步驟完成後驗證 |
|
||||
|
||||
---
|
||||
|
||||
## 決策點
|
||||
|
||||
### 常見決策點
|
||||
|
||||
| 階段 | 問題 | 選項 |
|
||||
|------|------|------|
|
||||
| 開始 | 如何開始? | 先了解現況 / 直接實作 |
|
||||
| 實作 | 實作方式? | 保持現有架構 / 重構 |
|
||||
| 驗證 | 通過了嗎? | 繼續 / 修復問題 |
|
||||
| 完成 | 還有什麼? | 下一個任務 / 結束 |
|
||||
|
||||
### 決策準則
|
||||
|
||||
1. **安全優先**: 破壞性變更需要明確確認
|
||||
2. **驗證後繼續**: 每步驟完成後驗證
|
||||
3. **文檔同步**: 變更後更新文檔
|
||||
|
||||
---
|
||||
|
||||
## 文檔使用
|
||||
|
||||
### 必讀文檔
|
||||
|
||||
| 文檔 | 用途 | 查閱時機 |
|
||||
|------|------|----------|
|
||||
| `AGENTS.md` | 專案規範 | 每次對話開始 |
|
||||
| `docs/*.md` | 技術規格 | 功能實作前 |
|
||||
|
||||
### 文檔更新時機
|
||||
|
||||
| 變更類型 | 需要更新 |
|
||||
|----------|----------|
|
||||
| 新功能 | `AGENTS.md` + 相關技術文檔 |
|
||||
| 架構變更 | `ARCHITECTURE_EVALUATION.md` |
|
||||
| 問題修復 | `PENDING_ISSUES.md` |
|
||||
| 環境變更 | `INSTALL_*.md` |
|
||||
|
||||
---
|
||||
|
||||
## 審查清單
|
||||
|
||||
### 實作完成後檢查
|
||||
|
||||
- [ ] `cargo clippy --lib` 通過
|
||||
- [ ] `cargo test --lib` 通過
|
||||
- [ ] `cargo fmt -- --check` 通過
|
||||
- [ ] 文檔已更新
|
||||
- [ ] 新功能有單元測試
|
||||
- [ ] Pre-commit hook 通過
|
||||
|
||||
### 對話結束前
|
||||
|
||||
- [ ] 所有變更已驗證
|
||||
- [ ] 文檔已同步
|
||||
- [ ] 下一步計劃明確
|
||||
|
||||
---
|
||||
|
||||
## 範例流程
|
||||
|
||||
### 範例 1: 實作新功能
|
||||
|
||||
```
|
||||
用戶: 實作使用者認證功能
|
||||
|
||||
OpenCode:
|
||||
1. 分析現有架構
|
||||
2. 創建任務清單
|
||||
3. 實作核心功能
|
||||
4. 添加單元測試
|
||||
5. 更新文檔
|
||||
6. 驗證通過
|
||||
|
||||
用戶: 完成,繼續下一個
|
||||
```
|
||||
|
||||
### 範例 2: 修復問題
|
||||
|
||||
```
|
||||
用戶: 修復登入超時問題
|
||||
|
||||
OpenCode:
|
||||
1. 重現問題
|
||||
2. 分析根因
|
||||
3. 實作修復
|
||||
4. 驗證修復
|
||||
5. 添加測試防止回歸
|
||||
|
||||
用戶: 確認修復完成
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 常見問題
|
||||
|
||||
### Q: 如何避免一次處理太多?
|
||||
|
||||
**A**: 使用 `/todo` 追蹤任務,分批處理。
|
||||
|
||||
### Q: 如何確保文檔同步?
|
||||
|
||||
**A**: 每個任務完成後檢查是否需要更新文檔。
|
||||
|
||||
### Q: 何時應該結束對話?
|
||||
|
||||
**A**: 當主要任務完成,且沒有緊急的後續步驟時。
|
||||
|
||||
---
|
||||
|
||||
## MCP 設定
|
||||
|
||||
### 設定檔案
|
||||
|
||||
OpenCode MCP 設定位於 `~/.config/opencode/opencode.json`
|
||||
|
||||
### 已啟用的 MCP Servers
|
||||
|
||||
| Server | 用途 | 命令 |
|
||||
|--------|------|------|
|
||||
| gitea | Gitea API 操作 | `/opt/homebrew/bin/gitea-mcp-server` |
|
||||
| n8n | n8n Workflow 操作 | `/opt/homebrew/bin/mcp-n8n` |
|
||||
| postgres | PostgreSQL 資料庫查詢 | `/opt/homebrew/bin/mcp-server-postgres` |
|
||||
| redis | Redis 快取操作 | `/opt/homebrew/bin/mcp-server-redis` |
|
||||
| qdrant | Qdrant 向量搜尋 | `/opt/homebrew/bin/mcp-server-qdrant` |
|
||||
| filesystem | 檔案系統操作 | `/opt/homebrew/bin/mcp-server-filesystem` |
|
||||
|
||||
### MCP 設定格式
|
||||
|
||||
**Schema 參考**: `https://opencode.ai/config.json`
|
||||
|
||||
**必要欄位**:
|
||||
| 欄位 | 類型 | 說明 |
|
||||
|------|------|------|
|
||||
| `type` | string | `"local"` 或 `"remote"` |
|
||||
| `command` | array | 命令和參數(local 必要) |
|
||||
| `url` | string | 遠端 URL(remote 必要) |
|
||||
|
||||
**可選欄位**:
|
||||
| 欄位 | 類型 | 說明 |
|
||||
|------|------|------|
|
||||
| `environment` | object | 環境變數(local only) |
|
||||
| `enabled` | boolean | 是否啟用 |
|
||||
| `timeout` | number | 請求超時(毫秒) |
|
||||
| `headers` | object | 請求頭(remote only) |
|
||||
|
||||
**Local MCP 範例**:
|
||||
```json
|
||||
{
|
||||
"mcp": {
|
||||
"gitea": {
|
||||
"type": "local",
|
||||
"enabled": true,
|
||||
"command": [
|
||||
"/opt/homebrew/bin/gitea-mcp-server",
|
||||
"-token", "<GITEA_TOKEN>",
|
||||
"-host", "http://localhost:3000"
|
||||
]
|
||||
},
|
||||
"n8n": {
|
||||
"type": "local",
|
||||
"enabled": true,
|
||||
"command": ["/opt/homebrew/bin/mcp-n8n"],
|
||||
"environment": {
|
||||
"N8N_BASE_URL": "http://localhost:5678",
|
||||
"N8N_API_KEY": "<N8N_API_KEY>"
|
||||
}
|
||||
},
|
||||
"postgres": {
|
||||
"type": "local",
|
||||
"enabled": true,
|
||||
"command": ["/opt/homebrew/bin/mcp-server-postgres"],
|
||||
"environment": {
|
||||
"DATABASE_URL": "postgresql://accusys:accusys@localhost:5432/momentry"
|
||||
}
|
||||
},
|
||||
"redis": {
|
||||
"type": "local",
|
||||
"enabled": true,
|
||||
"command": ["/opt/homebrew/bin/mcp-server-redis"],
|
||||
"environment": {
|
||||
"REDIS_URL": "redis://:accusys@localhost:6379"
|
||||
}
|
||||
},
|
||||
"qdrant": {
|
||||
"type": "local",
|
||||
"enabled": true,
|
||||
"command": ["/opt/homebrew/bin/mcp-server-qdrant"],
|
||||
"environment": {
|
||||
"QDRANT_URL": "http://localhost:6333",
|
||||
"QDRANT_API_KEY": ""
|
||||
}
|
||||
},
|
||||
"filesystem": {
|
||||
"type": "local",
|
||||
"enabled": true,
|
||||
"command": ["/opt/homebrew/bin/mcp-server-filesystem"],
|
||||
"args": ["/Users/accusys/momentry"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Remote MCP 範例**:
|
||||
```json
|
||||
{
|
||||
"mcp": {
|
||||
"jira": {
|
||||
"type": "remote",
|
||||
"url": "https://jira.example.com/mcp",
|
||||
"enabled": true,
|
||||
"headers": {
|
||||
"Authorization": "Bearer your-token"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### n8n MCP 工具 (43 個)
|
||||
|
||||
#### Workflows (10)
|
||||
| 工具 | 說明 |
|
||||
|------|------|
|
||||
| `n8n_list_workflows` | 列出所有 workflows |
|
||||
| `n8n_get_workflow` | 取得 workflow 詳情 |
|
||||
| `n8n_create_workflow` | 建立新 workflow |
|
||||
| `n8n_update_workflow` | 更新 workflow |
|
||||
| `n8n_delete_workflow` | 刪除 workflow |
|
||||
| `n8n_activate_workflow` | 啟用 workflow |
|
||||
| `n8n_deactivate_workflow` | 停用 workflow |
|
||||
| `n8n_execute_workflow` | 執行 workflow |
|
||||
| `n8n_get_workflow_tags` | 取得 workflow 標籤 |
|
||||
| `n8n_update_workflow_tags` | 更新 workflow 標籤 |
|
||||
|
||||
#### Executions (3)
|
||||
| 工具 | 說明 |
|
||||
|------|------|
|
||||
| `n8n_list_executions` | 列出執行記錄 |
|
||||
| `n8n_get_execution` | 取得執行詳情 |
|
||||
| `n8n_delete_execution` | 刪除執行記錄 |
|
||||
|
||||
#### Data Tables (8)
|
||||
| 工具 | 說明 |
|
||||
|------|------|
|
||||
| `n8n_list_datatables` | 列出資料表 |
|
||||
| `n8n_create_datatable` | 建立資料表 |
|
||||
| `n8n_get_datatable` | 取得資料表結構 |
|
||||
| `n8n_get_datatable_rows` | 取得資料表列 |
|
||||
| `n8n_insert_datatable_rows` | 插入資料列 |
|
||||
| `n8n_update_datatable_rows` | 更新資料列 |
|
||||
| `n8n_upsert_datatable_row` | 插入或更新資料列 |
|
||||
| `n8n_delete_datatable_rows` | 刪除資料列 |
|
||||
|
||||
#### Tags (5)
|
||||
| 工具 | 說明 |
|
||||
|------|------|
|
||||
| `n8n_list_tags` | 列出所有標籤 |
|
||||
| `n8n_get_tag` | 取得標籤 |
|
||||
| `n8n_create_tag` | 建立標籤 |
|
||||
| `n8n_update_tag` | 更新標籤 |
|
||||
| `n8n_delete_tag` | 刪除標籤 |
|
||||
|
||||
#### Credentials (4)
|
||||
| 工具 | 說明 |
|
||||
|------|------|
|
||||
| `n8n_list_credentials` | 列出憑證 |
|
||||
| `n8n_create_credential` | 建立憑證 |
|
||||
| `n8n_delete_credential` | 刪除憑證 |
|
||||
| `n8n_get_credential_schema` | 取得憑證 schema |
|
||||
|
||||
#### Users (3)
|
||||
| 工具 | 說明 |
|
||||
|------|------|
|
||||
| `n8n_list_users` | 列出使用者 |
|
||||
| `n8n_get_user` | 取得使用者 |
|
||||
| `n8n_delete_user` | 刪除使用者 |
|
||||
|
||||
#### Variables (3)
|
||||
| 工具 | 說明 |
|
||||
|------|------|
|
||||
| `n8n_list_variables` | 列出變數 |
|
||||
| `n8n_create_variable` | 建立變數 |
|
||||
| `n8n_delete_variable` | 刪除變數 |
|
||||
|
||||
#### 其他 (7)
|
||||
| 工具 | 說明 |
|
||||
|------|------|
|
||||
| `n8n_list_projects` | 列出專案 |
|
||||
| `n8n_create_project` | 建立專案 |
|
||||
| `n8n_update_project` | 更新專案 |
|
||||
| `n8n_delete_project` | 刪除專案 |
|
||||
| `n8n_generate_audit` | 產生安全審計報告 |
|
||||
| `n8n_health_check` | 健康檢查 |
|
||||
| `n8n_trigger_webhook` | 觸發 webhook |
|
||||
|
||||
### 安裝 n8n MCP
|
||||
|
||||
```bash
|
||||
npm install -g @nextoolsolutions/mcp-n8n
|
||||
```
|
||||
|
||||
### n8n API Key 設定
|
||||
|
||||
1. 開啟 n8n UI (http://localhost:5678)
|
||||
2. 前往 Settings → API
|
||||
3. 建立 API Key
|
||||
4. 將 API Key 加入 `opencode.json` 的 `N8N_API_KEY`
|
||||
|
||||
### 驗證 MCP 運作
|
||||
|
||||
```bash
|
||||
# 測試 MCP server
|
||||
N8N_BASE_URL=https://n8n.momentry.ddns.net \
|
||||
N8N_API_KEY="your-key" \
|
||||
mcp-n8n
|
||||
|
||||
# 測試工具列表
|
||||
echo '{"jsonrpc": "2.0", "id": 1, "method": "initialize", "params": {"protocolVersion": "2024-11-05", "capabilities": {}, "clientInfo": {"name": "test", "version": "1.0"}}}
|
||||
{"jsonrpc": "2.0", "id": 2, "method": "tools/list", "params": {}}' | \
|
||||
N8N_BASE_URL=https://n8n.momentry.ddns.net \
|
||||
N8N_API_KEY="your-key" \
|
||||
mcp-n8n
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 相關文件
|
||||
|
||||
- [AGENTS.md](../AGENTS.md) - 專案開發規範
|
||||
- [ARCHITECTURE_EVALUATION.md](./ARCHITECTURE_EVALUATION.md) - 架構優化評估
|
||||
- [PENDING_ISSUES.md](./PENDING_ISSUES.md) - 待解決問題追蹤
|
||||
- [INSTALL_N8N.md](./INSTALL_N8N.md) - n8n 安裝指南
|
||||
@@ -1,535 +0,0 @@
|
||||
# OpenCode MCP Servers 安裝指南
|
||||
|
||||
| 項目 | 內容 |
|
||||
|------|------|
|
||||
| 建立者 | OpenCode |
|
||||
| 建立時間 | 2026-03-24 |
|
||||
| 文件版本 | V1.1 |
|
||||
|
||||
---
|
||||
|
||||
## 版本歷史
|
||||
|
||||
| 版本 | 日期 | 目的 | 操作人 | 工具/模型 |
|
||||
|------|------|------|--------|-----------|
|
||||
| V1.0 | 2026-03-24 | 創建文件 | OpenCode | OpenCode / big-pickle |
|
||||
| V1.1 | 2026-03-24 | 新增 sentry, context7, playwright MCP | OpenCode | OpenCode / big-pickle |
|
||||
|
||||
---
|
||||
|
||||
## 概述
|
||||
|
||||
本文檔說明如何在 macOS 上為 OpenCode 安裝和配置 MCP (Model Context Protocol) Servers,透過標準化的協定讓 AI 助手能夠存取各種外部服務和工具。
|
||||
|
||||
MCP Servers 提供:
|
||||
- 標準化的工具調用介面
|
||||
- 安全的資源存取控制
|
||||
- 統一的配置和管理方式
|
||||
|
||||
---
|
||||
|
||||
## 已安裝的 MCP Servers
|
||||
|
||||
| Server | 安裝方式 | 用途 | 狀態 |
|
||||
|--------|----------|------|------|
|
||||
| gitea | Homebrew | Gitea API 操作 | ✅ |
|
||||
| n8n | NPM (@nextoolsolutions/mcp-n8n) | n8n Workflow 管理 | ✅ |
|
||||
| postgres | NPM (@modelcontextprotocol/server-postgres) | PostgreSQL 資料庫查詢 | ✅ |
|
||||
| redis | NPM (@modelcontextprotocol/server-redis) | Redis 快取操作 | ✅ |
|
||||
| mongodb | NPM (mongodb-mcp-server) | MongoDB 文件資料庫 | ✅ |
|
||||
| qdrant | Python (qdrant/mcp-server-qdrant) | Qdrant 向量搜尋 | ✅ |
|
||||
| filesystem | NPM (@modelcontextprotocol/server-filesystem) | 檔案系統操作 | ✅ |
|
||||
| sentry | NPM (@sentry/mcp-server) | 錯誤追蹤和監控 | ⏳ |
|
||||
| context7 | NPM (@upstash/context7-mcp) | 技術文檔搜尋 | ✅ |
|
||||
| playwright | NPM (@playwright/mcp) | 瀏覽器自動化 | ✅ |
|
||||
|
||||
---
|
||||
|
||||
## 前置條件
|
||||
|
||||
- OpenCode 已安裝
|
||||
- Node.js (用於 NPM 套件)
|
||||
- Python 3.11+ (用於 qdrant-mcp)
|
||||
- 相關服務已運行 (PostgreSQL, Redis, Qdrant, Gitea, n8n, MongoDB)
|
||||
|
||||
---
|
||||
|
||||
## 安裝步驟
|
||||
|
||||
### Step 1: 安裝 NPM MCP Servers
|
||||
|
||||
```bash
|
||||
npm install -g @modelcontextprotocol/server-postgres
|
||||
npm install -g @modelcontextprotocol/server-redis
|
||||
npm install -g @modelcontextprotocol/server-filesystem
|
||||
npm install -g @modelcontextprotocol/server-everything
|
||||
npm install -g @nextoolsolutions/mcp-n8n
|
||||
npm install -g mongodb-mcp-server
|
||||
npm install -g @sentry/mcp-server
|
||||
npm install -g @upstash/context7-mcp
|
||||
npm install -g @playwright/mcp
|
||||
```
|
||||
|
||||
**驗證**:
|
||||
|
||||
```bash
|
||||
which mcp-server-postgres
|
||||
which mcp-server-redis
|
||||
which mcp-server-filesystem
|
||||
which mcp-n8n
|
||||
which mongodb-mcp-server
|
||||
which sentry-mcp
|
||||
which context7-mcp
|
||||
which playwright-mcp
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Step 2: 安裝 gitea-mcp-server
|
||||
|
||||
```bash
|
||||
brew install gitea-mcp-server
|
||||
```
|
||||
|
||||
**驗證**:
|
||||
|
||||
```bash
|
||||
which gitea-mcp-server
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Step 3: 安裝 MongoDB MCP Server (已包含在 Step 1)
|
||||
|
||||
MongoDB MCP 已透過 NPM 安裝:
|
||||
|
||||
```bash
|
||||
npm install -g mongodb-mcp-server
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Step 4: 安裝 Qdrant MCP Server (Python)
|
||||
|
||||
```bash
|
||||
cd /tmp
|
||||
git clone https://github.com/qdrant/mcp-server-qdrant.git
|
||||
cd mcp-server-qdrant
|
||||
/opt/homebrew/bin/python3.11 -m pip install -e .
|
||||
```
|
||||
|
||||
**驗證**:
|
||||
|
||||
```bash
|
||||
which mcp-server-qdrant
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Step 5: 配置 OpenCode MCP 設定
|
||||
|
||||
編輯 `~/.config/opencode/opencode.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"$schema": "https://opencode.ai/config.json",
|
||||
"mcp": {
|
||||
"gitea": {
|
||||
"type": "local",
|
||||
"enabled": true,
|
||||
"command": [
|
||||
"/opt/homebrew/bin/gitea-mcp-server",
|
||||
"-token",
|
||||
"<GITEA_TOKEN>",
|
||||
"-host",
|
||||
"http://localhost:3000"
|
||||
]
|
||||
},
|
||||
"n8n": {
|
||||
"type": "local",
|
||||
"enabled": true,
|
||||
"command": ["/opt/homebrew/bin/mcp-n8n"],
|
||||
"environment": {
|
||||
"N8N_BASE_URL": "http://localhost:5678",
|
||||
"N8N_API_KEY": "<N8N_API_KEY>"
|
||||
}
|
||||
},
|
||||
"postgres": {
|
||||
"type": "local",
|
||||
"enabled": true,
|
||||
"command": [
|
||||
"/opt/homebrew/bin/mcp-server-postgres",
|
||||
"postgresql://accusys:accusys@localhost:5432/momentry"
|
||||
]
|
||||
},
|
||||
"redis": {
|
||||
"type": "local",
|
||||
"enabled": true,
|
||||
"command": [
|
||||
"/opt/homebrew/bin/mcp-server-redis",
|
||||
"redis://:accusys@localhost:6379"
|
||||
]
|
||||
},
|
||||
"mongodb": {
|
||||
"type": "local",
|
||||
"enabled": true,
|
||||
"command": ["/opt/homebrew/bin/mongodb-mcp-server"],
|
||||
"environment": {
|
||||
"MONGODB_URI": "mongodb://localhost:27017"
|
||||
}
|
||||
},
|
||||
"qdrant": {
|
||||
"type": "local",
|
||||
"enabled": true,
|
||||
"command": ["/opt/homebrew/bin/mcp-server-qdrant"],
|
||||
"environment": {
|
||||
"QDRANT_URL": "http://localhost:6333",
|
||||
"QDRANT_API_KEY": ""
|
||||
}
|
||||
},
|
||||
"filesystem": {
|
||||
"type": "local",
|
||||
"enabled": true,
|
||||
"command": [
|
||||
"/opt/homebrew/bin/mcp-server-filesystem",
|
||||
"/Users/accusys/momentry"
|
||||
]
|
||||
},
|
||||
"sentry": {
|
||||
"type": "local",
|
||||
"enabled": true,
|
||||
"command": [
|
||||
"/opt/homebrew/bin/sentry-mcp",
|
||||
"--access-token",
|
||||
"<SENTRY_ACCESS_TOKEN>"
|
||||
]
|
||||
},
|
||||
"context7": {
|
||||
"type": "local",
|
||||
"enabled": true,
|
||||
"command": ["/opt/homebrew/bin/context7-mcp"]
|
||||
},
|
||||
"playwright": {
|
||||
"type": "local",
|
||||
"enabled": true,
|
||||
"command": ["/opt/homebrew/bin/playwright-mcp"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Step 6: 驗證 MCP Servers
|
||||
|
||||
```bash
|
||||
# 列出所有 MCP servers
|
||||
opencode mcp ls
|
||||
|
||||
# 或在 OpenCode 中執行
|
||||
/mcps
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## MCP Server 工具說明
|
||||
|
||||
### gitea
|
||||
|
||||
| 工具 | 說明 |
|
||||
|------|------|
|
||||
| list_repos | 列出倉庫 |
|
||||
| get_repo | 取得倉庫詳情 |
|
||||
| list_issues | 列出 Issues |
|
||||
| create_issue | 建立 Issue |
|
||||
| list_pulls | 列出 Pull Requests |
|
||||
|
||||
---
|
||||
|
||||
### n8n (43 個工具)
|
||||
|
||||
#### Workflows (10)
|
||||
| 工具 | 說明 |
|
||||
|------|------|
|
||||
| n8n_list_workflows | 列出所有 workflows |
|
||||
| n8n_get_workflow | 取得 workflow 詳情 |
|
||||
| n8n_create_workflow | 建立新 workflow |
|
||||
| n8n_update_workflow | 更新 workflow |
|
||||
| n8n_delete_workflow | 刪除 workflow |
|
||||
| n8n_activate_workflow | 啟用 workflow |
|
||||
| n8n_deactivate_workflow | 停用 workflow |
|
||||
| n8n_execute_workflow | 執行 workflow |
|
||||
| n8n_get_workflow_tags | 取得 workflow 標籤 |
|
||||
| n8n_update_workflow_tags | 更新 workflow 標籤 |
|
||||
|
||||
#### Executions (3)
|
||||
| 工具 | 說明 |
|
||||
|------|------|
|
||||
| n8n_list_executions | 列出執行記錄 |
|
||||
| n8n_get_execution | 取得執行詳情 |
|
||||
| n8n_delete_execution | 刪除執行記錄 |
|
||||
|
||||
#### Data Tables (8)
|
||||
| 工具 | 說明 |
|
||||
|------|------|
|
||||
| n8n_list_datatables | 列出資料表 |
|
||||
| n8n_create_datatable | 建立資料表 |
|
||||
| n8n_get_datatable | 取得資料表結構 |
|
||||
| n8n_get_datatable_rows | 取得資料表列 |
|
||||
| n8n_insert_datatable_rows | 插入資料列 |
|
||||
| n8n_update_datatable_rows | 更新資料列 |
|
||||
| n8n_upsert_datatable_row | 插入或更新資料列 |
|
||||
| n8n_delete_datatable_rows | 刪除資料列 |
|
||||
|
||||
#### Tags (5)
|
||||
| 工具 | 說明 |
|
||||
|------|------|
|
||||
| n8n_list_tags | 列出所有標籤 |
|
||||
| n8n_get_tag | 取得標籤 |
|
||||
| n8n_create_tag | 建立標籤 |
|
||||
| n8n_update_tag | 更新標籤 |
|
||||
| n8n_delete_tag | 刪除標籤 |
|
||||
|
||||
#### 其他 (17)
|
||||
| 工具 | 說明 |
|
||||
|------|------|
|
||||
| n8n_list_credentials | 列出憑證 |
|
||||
| n8n_create_credential | 建立憑證 |
|
||||
| n8n_delete_credential | 刪除憑證 |
|
||||
| n8n_get_credential_schema | 取得憑證 schema |
|
||||
| n8n_list_users | 列出使用者 |
|
||||
| n8n_get_user | 取得使用者 |
|
||||
| n8n_delete_user | 刪除使用者 |
|
||||
| n8n_list_variables | 列出變數 |
|
||||
| n8n_create_variable | 建立變數 |
|
||||
| n8n_delete_variable | 刪除變數 |
|
||||
| n8n_list_projects | 列出專案 |
|
||||
| n8n_create_project | 建立專案 |
|
||||
| n8n_update_project | 更新專案 |
|
||||
| n8n_delete_project | 刪除專案 |
|
||||
| n8n_generate_audit | 產生安全審計報告 |
|
||||
| n8n_health_check | 健康檢查 |
|
||||
| n8n_trigger_webhook | 觸發 webhook |
|
||||
|
||||
---
|
||||
|
||||
### postgres
|
||||
|
||||
| 工具 | 說明 |
|
||||
|------|------|
|
||||
| query | 執行唯讀 SQL 查詢 |
|
||||
|
||||
**範例**:
|
||||
|
||||
```json
|
||||
{
|
||||
"sql": "SELECT * FROM users LIMIT 10;"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### redis
|
||||
|
||||
| 工具 | 說明 |
|
||||
|------|------|
|
||||
| set | 設定 key-value 配對 |
|
||||
| get | 取得 key 的值 |
|
||||
| delete | 刪除 key(s) |
|
||||
| list | 列出符合模式的 keys |
|
||||
|
||||
---
|
||||
|
||||
### mongodb
|
||||
|
||||
| 工具 | 說明 |
|
||||
|------|------|
|
||||
| mongodb_list_databases | 列出所有資料庫 |
|
||||
| mongodb_list_collections | 列出資料庫中的集合 |
|
||||
| mongodb_find | 查詢文件 |
|
||||
| mongodb_insert_one | 插入單個文件 |
|
||||
| mongodb_insert_many | 插入多個文件 |
|
||||
| mongodb_update_one | 更新單個文件 |
|
||||
| mongodb_update_many | 更新多個文件 |
|
||||
| mongodb_delete_one | 刪除單個文件 |
|
||||
| mongodb_delete_many | 刪除多個文件 |
|
||||
| mongodb_aggregate | 聚合查詢 |
|
||||
|
||||
**範例**:
|
||||
|
||||
```json
|
||||
{
|
||||
"database": "momentry",
|
||||
"collection": "videos",
|
||||
"filter": {"uuid": "abc123"}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### qdrant
|
||||
|
||||
| 工具 | 說明 |
|
||||
|------|------|
|
||||
| 查詢工具 | 向量搜尋和集合管理 |
|
||||
|
||||
---
|
||||
|
||||
### filesystem
|
||||
|
||||
| 工具 | 說明 |
|
||||
|------|------|
|
||||
| read_file | 讀取檔案 |
|
||||
| write_file | 寫入檔案 |
|
||||
| list_directory | 列出目錄 |
|
||||
| create_directory | 建立目錄 |
|
||||
|
||||
---
|
||||
|
||||
### sentry
|
||||
|
||||
| 工具 | 說明 |
|
||||
|------|------|
|
||||
| sentry_list_issues | 列出 issues |
|
||||
| sentry_get_issue | 取得 issue 詳情 |
|
||||
| sentry_create_issue | 建立 issue |
|
||||
| sentry_update_issue | 更新 issue |
|
||||
| sentry_list_projects | 列出專案 |
|
||||
| sentry_search_docs | 搜尋 Sentry 文件 |
|
||||
|
||||
**注意**: 需要設定 `SENTRY_ACCESS_TOKEN`
|
||||
|
||||
**取得 Token**:
|
||||
1. 登入 https://sentry.io
|
||||
2. Settings → Developer Tools → API Keys
|
||||
3. 建立新 Token (需要 `event:read`, `project:read` 權限)
|
||||
|
||||
---
|
||||
|
||||
### context7
|
||||
|
||||
| 工具 | 說明 |
|
||||
|------|------|
|
||||
| context7_resolve-library-id | 解析函式庫 ID |
|
||||
| context7_query-docs | 查詢函式庫文件 |
|
||||
|
||||
**用途**: 即時技術文件和函式庫查詢,獲取最新的 API 語法和範例。
|
||||
|
||||
---
|
||||
|
||||
### playwright
|
||||
|
||||
| 工具 | 說明 |
|
||||
|------|------|
|
||||
| playwright_navigate | 導航到 URL |
|
||||
| playwright_screenshot | 截圖 |
|
||||
| playwright_click | 點擊元素 |
|
||||
| playwright_type | 輸入文字 |
|
||||
| playwright_evaluate | 執行 JavaScript |
|
||||
|
||||
**用途**: 瀏覽器自動化測試、網頁截圖、網頁內容抓取。
|
||||
|
||||
---
|
||||
|
||||
## 管理指令
|
||||
|
||||
### 新增 MCP Server
|
||||
|
||||
```bash
|
||||
opencode mcp add
|
||||
```
|
||||
|
||||
### 列出 MCP Servers
|
||||
|
||||
```bash
|
||||
opencode mcp ls
|
||||
```
|
||||
|
||||
### 認證 OAuth Server
|
||||
|
||||
```bash
|
||||
opencode mcp auth <server-name>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 故障排除
|
||||
|
||||
### MCP Server 連線失敗
|
||||
|
||||
1. 檢查服務是否運行:
|
||||
```bash
|
||||
# PostgreSQL
|
||||
pg_isready -h localhost -p 5432
|
||||
|
||||
# Redis
|
||||
redis-cli -a accusys ping
|
||||
|
||||
# Qdrant
|
||||
curl -s http://localhost:6333/
|
||||
```
|
||||
|
||||
2. 檢查 MCP Server 是否安裝:
|
||||
```bash
|
||||
which mcp-server-postgres
|
||||
which mcp-server-redis
|
||||
```
|
||||
|
||||
3. 驗證配置格式正確
|
||||
|
||||
---
|
||||
|
||||
### Token 或 API Key 無效
|
||||
|
||||
1. PostgreSQL: 確認使用者名稱和密碼
|
||||
2. Redis: 確認密碼
|
||||
3. n8n: 確認 API Key 未過期
|
||||
4. Gitea: 確認 Token 有效
|
||||
|
||||
---
|
||||
|
||||
## 檔案位置
|
||||
|
||||
| 類型 | 路徑 | 說明 |
|
||||
|------|------|------|
|
||||
| OpenCode 配置 | ~/.config/opencode/opencode.json | MCP 設定檔 |
|
||||
| Gitea MCP | /opt/homebrew/bin/gitea-mcp-server | 安裝路徑 |
|
||||
| n8n MCP | /opt/homebrew/bin/mcp-n8n | 安裝路徑 |
|
||||
| Postgres MCP | /opt/homebrew/bin/mcp-server-postgres | 安裝路徑 |
|
||||
| Redis MCP | /opt/homebrew/bin/mcp-server-redis | 安裝路徑 |
|
||||
| Qdrant MCP | /opt/homebrew/bin/mcp-server-qdrant | 安裝路徑 |
|
||||
| Filesystem MCP | /opt/homebrew/bin/mcp-server-filesystem | 安裝路徑 |
|
||||
|
||||
---
|
||||
|
||||
## 常用指令
|
||||
|
||||
```bash
|
||||
# 列出所有 MCP servers
|
||||
opencode mcp ls
|
||||
|
||||
# 測試 PostgreSQL MCP
|
||||
echo '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"query","arguments":{"sql":"SELECT 1;"}}}' | \
|
||||
/opt/homebrew/bin/mcp-server-postgres postgresql://accusys:accusys@localhost:5432/momentry
|
||||
|
||||
# 測試 Redis MCP
|
||||
echo '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"ping","arguments":{}}}' | \
|
||||
/opt/homebrew/bin/mcp-server-redis redis://:accusys@localhost:6379
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 版本資訊
|
||||
|
||||
- 版本: 1.0
|
||||
- 安裝日期: 2026-03-24
|
||||
- 文件更新: 2026-03-24
|
||||
|
||||
---
|
||||
|
||||
## 相關文件
|
||||
|
||||
- [OpenCode Guide](./OPENCODE_GUIDE.md) - OpenCode 使用指南
|
||||
- [Gitea MCP 安裝](./INSTALL_GITEA_MCP.md) - Gitea MCP 詳細設定
|
||||
- [n8n MCP 設定](./N8N_MCP_SETUP.md) - n8n MCP 詳細設定
|
||||
- [服務總覽](./SERVICES.md) - 所有服務狀態總覽
|
||||
@@ -1,813 +0,0 @@
|
||||
# 待解決問題追蹤
|
||||
|
||||
| 項目 | 內容 |
|
||||
|------|------|
|
||||
| 建立者 | Warren |
|
||||
| 建立時間 | 2026-03-17 |
|
||||
| 文件版本 | V1.0 |
|
||||
|
||||
---
|
||||
|
||||
## 版本歷史
|
||||
|
||||
| 版本 | 日期 | 目的 | 操作人 | 工具/模型 |
|
||||
|------|------|------|--------|-----------|
|
||||
| V1.0 | 2026-03-17 | 創建文件 | Warren | OpenCode / MiniMax M2.5 |
|
||||
| V1.1 | 2026-03-21 | 更新問題狀態 | OpenCode | - |
|
||||
| V1.2 | 2026-03-21 | 添加備份機制優化待辦 | OpenCode | - |
|
||||
| V1.3 | 2026-03-21 | 完成清理硬編碼密碼 | OpenCode | - |
|
||||
| V1.4 | 2026-03-21 | 完成 OpenCode n8n MCP 整合 | OpenCode | - |
|
||||
| V1.5 | 2026-03-21 | 完成 API Key Management 核心模組 | OpenCode | - |
|
||||
| V1.6 | 2026-03-23 | 添加 Momentry Core API launchd 待辦 | OpenCode | - |
|
||||
| V1.7 | 2026-03-23 | 完成 Momentry Core API launchd 設定 | OpenCode | - |
|
||||
| V1.8 | 2026-03-24 | 完成服務統一遷移,所有服務使用自定義 plist | OpenCode | big-pickle |
|
||||
| V1.9 | 2026-03-24 | 建立統一會員系統實作計畫 | OpenCode | big-pickle |
|
||||
| V2.0 | 2026-03-24 | 建立 Job Worker 實作計畫 | OpenCode | big-pickle |
|
||||
|
||||
---
|
||||
|
||||
## 問題 #1: sqlx async INSERT 不會實際寫入數據庫
|
||||
|
||||
### 問題描述
|
||||
使用 sqlx async 執行 INSERT 時,報告成功(rows_affected=1),但數據沒有實際寫入數據庫。
|
||||
|
||||
### 嘗試過的解決方案
|
||||
| # | 嘗試方法 | 結果 |
|
||||
|---|---------|------|
|
||||
| 1 | 使用 `execute()` | 報告成功但未寫入 |
|
||||
| 2 | 使用 `fetch()` | 同樣問題 |
|
||||
| 3 | 使用交易 | 同樣問題 |
|
||||
| 4 | 使用連接池 `acquire()` | 同樣問題 |
|
||||
| 5 | 每個操作創建新連接池 | 同樣問題 |
|
||||
| 6 | 使用 `std::process::Command` 同步調用 psql | 同樣問題 |
|
||||
| 7 | 使用 `tokio::task::spawn_blocking` | 同樣問題 |
|
||||
|
||||
### 觀察到的現象
|
||||
- 直接用 psql 命令行可以成功寫入
|
||||
- 用另一個 PostgreSQL client 可以成功寫入
|
||||
- sqlx 查詢 COUNT(*) 可以正確讀取數據
|
||||
- 但 sqlx INSERT 報告成功卻不寫入
|
||||
|
||||
### 懷疑方向
|
||||
- sqlx 連接池與 PostgreSQL 的某種交互問題
|
||||
- Rust async runtime 與 PostgreSQL client 的問題
|
||||
- postgresql.conf 配置問題
|
||||
|
||||
### 臨時解決方案
|
||||
- Qdrant 向量存儲正常工作(不受影響)
|
||||
- 存儲狀態追蹤功能正常運作
|
||||
|
||||
### 負責人
|
||||
-
|
||||
|
||||
### 建立日期
|
||||
2026-03-17
|
||||
|
||||
### 備註
|
||||
影響 `store_vector` 函數,PVector 存儲無法正常工作,但 QVector 正常運作
|
||||
|
||||
### 2026-03-21 調查結果
|
||||
|
||||
#### 測試結果
|
||||
- 直接 psql INSERT: ✅ 成功
|
||||
- 資料寫入驗證: ✅ 成功
|
||||
|
||||
#### 發現的問題
|
||||
`store_vector` 函數 (`postgres_db.rs:819-860`) 存在以下問題:
|
||||
|
||||
```rust
|
||||
// 問題 1: 錯誤被靜默忽略
|
||||
match join_result {
|
||||
Ok((cid, Ok(output))) => {
|
||||
if !output.status.success() {
|
||||
let err = String::from_utf8_lossy(&output.stderr);
|
||||
tracing::error!("psql error for {}: {}", cid, err);
|
||||
// 沒有返回錯誤!只是記錄日誌
|
||||
}
|
||||
}
|
||||
// ...
|
||||
}
|
||||
Ok(()) // 即使失敗也返回 Ok
|
||||
```
|
||||
|
||||
#### 建議修復
|
||||
1. 讓 `store_vector` 返回實際結果
|
||||
2. 在失敗時返回 `Err`
|
||||
3. 添加單元測試驗證
|
||||
|
||||
#### 下一步
|
||||
- [x] 修復 `store_vector` 錯誤處理
|
||||
- [x] 添加單元測試
|
||||
- [ ] 重現並確認問題根因
|
||||
|
||||
### 2026-03-21 修復完成
|
||||
|
||||
已修復 `store_vector` 函數的錯誤處理:
|
||||
|
||||
```rust
|
||||
// 修復前:錯誤被靜默忽略
|
||||
match join_result {
|
||||
Ok((cid, Ok(output))) => {
|
||||
if !output.status.success() {
|
||||
tracing::error!("..."); // 只記錄,不返回錯誤
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(()) // 即使失敗也返回 Ok
|
||||
|
||||
// 修復後:正確傳播錯誤
|
||||
let (cid, output) = result;
|
||||
let output = output.map_err(|e| anyhow::anyhow!(...))?;
|
||||
if !output.status.success() {
|
||||
anyhow::bail!("psql INSERT failed...");
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 問題 #2: TUI 與 stdout 輸出混合
|
||||
|
||||
### 問題描述
|
||||
Python processors 的 progress 輸出蓋過 TUI,導致使用者無法清楚看到處理進度。
|
||||
|
||||
### 解決方案
|
||||
~~使用 TUI 渲染到 stderr~~ → 使用 Redis Pub/Sub 作為消息總線
|
||||
|
||||
### 當前狀態
|
||||
| 子項目 | 狀態 |
|
||||
|--------|------|
|
||||
| RedisPublisher Python 端 | ✅ 已實作 |
|
||||
| Rust Redis 客戶端 | ✅ 已實作 (`redis_client.rs`) |
|
||||
| Rust 訂閱更新 TUI | ✅ 已實作 (main.rs) |
|
||||
| 中斷後繼續/重做 | 🔜 待實作 |
|
||||
|
||||
### 負責人
|
||||
-
|
||||
|
||||
### 建立日期
|
||||
2026-03-17
|
||||
|
||||
### 更新日期
|
||||
2026-03-21
|
||||
|
||||
### 參考文檔
|
||||
- `docs/MOMENTRY_CORE_REDIS_KEYS.md`
|
||||
- `scripts/redis_publisher.py`
|
||||
- `src/core/db/redis_client.rs`
|
||||
|
||||
---
|
||||
|
||||
## 問題 #3: Redis Message Bus 尚未實作
|
||||
|
||||
### 問題描述
|
||||
根據設計規範,需要使用 Redis 作為監控和狀態管理系統。
|
||||
|
||||
### 當前狀態
|
||||
|
||||
| 實作項目 | 狀態 | 說明 |
|
||||
|---------|------|------|
|
||||
| Redis 客戶端 (Hash) | ✅ | `redis_client.rs` |
|
||||
| Redis 客戶端 (Pub/Sub) | ✅ | `redis_client.rs::subscribe_progress()` |
|
||||
| Python RedisPublisher | ✅ | `scripts/redis_publisher.py` |
|
||||
| Rust 訂閱頻道 | ✅ | `main.rs` 中的 redis 訂閱邏輯 |
|
||||
| 監控腳本 | ✅ | `monitor/service/health_check.sh` |
|
||||
|
||||
### 負責人
|
||||
-
|
||||
|
||||
### 建立日期
|
||||
2026-03-17
|
||||
|
||||
### 更新日期
|
||||
2026-03-21
|
||||
|
||||
### 優先級
|
||||
~~高~~ → 中 - 核心功能已完成
|
||||
|
||||
### 參考文檔
|
||||
- `docs/MOMENTRY_CORE_REDIS_KEYS.md`
|
||||
- `docs/MOMENTRY_CORE_MONITORING.md`
|
||||
|
||||
---
|
||||
|
||||
## 架構優化待評估
|
||||
|
||||
詳細內容請參考 [ARCHITECTURE_EVALUATION.md](./ARCHITECTURE_EVALUATION.md)
|
||||
|
||||
| 項目 | 複雜度 | 優先級 |
|
||||
|------|--------|--------|
|
||||
| PostgreSQL → Redis 故障轉移 | 中 | 待評估 |
|
||||
| 連接池監控 | 低 | 待評估 |
|
||||
| Processor 重試機制 | 中 | 待評估 |
|
||||
| PyO3 整合 | 高 | 低 |
|
||||
| HTTP 健康端點 | 低 | ✅ 已完成 |
|
||||
| Commit Message Lint | 低 | ✅ 已完成 |
|
||||
|
||||
---
|
||||
|
||||
## 備份機制優化 (2026-03-21)
|
||||
|
||||
### 問題分析
|
||||
|
||||
| 問題 | 嚴重性 | 說明 |
|
||||
|------|--------|------|
|
||||
| 溫冷分層未啟用 | 中 | weekly/monthly/archive 目錄為空 |
|
||||
| 清理策略未執行 | 高 | 每日備份保留過多,空間浪費 |
|
||||
| 無異地備份 | 高 | 無遠程備份(雲端/另一設備) |
|
||||
| 備份驗證未自動化 | 中 | 只檢查文件存在,沒驗證可恢復性 |
|
||||
| Gitea 大文件 | 中 | 每次備份 1GB,頻率過高 |
|
||||
| 密碼硬編碼 | 高 | 腳本中有多處明文密碼 |
|
||||
|
||||
### 待辦事項
|
||||
|
||||
| # | 任務 | 優先級 | 狀態 |
|
||||
|---|------|--------|------|
|
||||
| 1 | 啟用溫冷分層 (`backup_monitor.sh tier`) | 高 | 待辦 |
|
||||
| 2 | 啟用清理策略 (`backup_monitor.sh cleanup`) | 高 | 待辦 |
|
||||
| 3 | 添加備份驗證腳本 | 高 | 待辦 |
|
||||
| 4 | 優化 Gitea 備份頻率 (改為週備份) | 中 | 待辦 |
|
||||
| 5 | 創建外部備份腳本 (rsync/雲端) | 高 | 待辦 |
|
||||
| 6 | 清理腳本中的硬編碼密碼 | 高 | ✅ 已完成 |
|
||||
|
||||
### 推薦備份策略
|
||||
|
||||
| 層級 | 保留時間 | 頻率 | 目標 |
|
||||
|------|----------|------|------|
|
||||
| Hot | 7 天 | 每日 | 本地 SSD |
|
||||
| Warm | 30 天 | 每週 | 本地 HDD |
|
||||
| Cold | 90 天 | 每月 | 外部存儲 |
|
||||
| Archive | 1 年 | 每季 | 離線/雲端 |
|
||||
|
||||
### 建議的 Crontab
|
||||
|
||||
```bash
|
||||
# 每日備份 (排除 Gitea)
|
||||
0 3 * * 1-6 /Users/accusys/momentry/scripts/backup_all.sh all_except_gitea
|
||||
|
||||
# 每週完整備份 (含 Gitea)
|
||||
0 3 * * 0 /Users/accusys/momentry/scripts/backup_all.sh all
|
||||
|
||||
# 每週溫冷分層
|
||||
0 4 * * 0 /Users/accusys/momentry_core_0.1/monitor/storage/backup_monitor.sh tier
|
||||
|
||||
# 每週清理
|
||||
0 5 * * 0 /Users/accusys/momentry_core_0.1/monitor/storage/backup_monitor.sh cleanup
|
||||
|
||||
# 每月驗證
|
||||
0 6 1 * * /Users/accusys/momentry/scripts/verify_backup.sh
|
||||
```
|
||||
|
||||
### 負責人
|
||||
-
|
||||
|
||||
### 參考文件
|
||||
- `/Users/accusys/momentry/scripts/backup_all.sh`
|
||||
- `/Users/accusys/momentry_core_0.1/monitor/storage/backup_monitor.sh`
|
||||
- `/Users/accusys/momentry/backup/`
|
||||
|
||||
---
|
||||
|
||||
## OpenCode n8n MCP 整合 (2026-03-21)
|
||||
|
||||
### 完成狀態
|
||||
|
||||
| 項目 | 狀態 | 說明 |
|
||||
|------|------|------|
|
||||
| n8n REST API 啟用 | ✅ | `N8N_PUBLIC_API_ENABLED=true` |
|
||||
| API Key 生成 | ✅ | Settings → API |
|
||||
| MCP Server 安裝 | ✅ | `@nextoolsolutions/mcp-n8n` |
|
||||
| OpenCode 設定 | ✅ | `~/.config/opencode/opencode.json` |
|
||||
| 43 工具可用 | ✅ | workflows, executions, datatables, tags 等 |
|
||||
|
||||
### 設定檔案
|
||||
|
||||
`~/.config/opencode/opencode.json`:
|
||||
```json
|
||||
{
|
||||
"mcp": {
|
||||
"gitea": {
|
||||
"type": "local",
|
||||
"enabled": true,
|
||||
"command": [
|
||||
"/opt/homebrew/bin/gitea-mcp-server",
|
||||
"-token", "<GITEA_TOKEN>",
|
||||
"-host", "http://localhost:3000"
|
||||
]
|
||||
},
|
||||
"n8n": {
|
||||
"type": "local",
|
||||
"enabled": true,
|
||||
"command": ["/opt/homebrew/bin/mcp-n8n"],
|
||||
"environment": {
|
||||
"N8N_BASE_URL": "http://localhost:5678",
|
||||
"N8N_API_KEY": "<N8N_API_KEY>"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 重要提醒
|
||||
|
||||
1. **API Key 安全**: 避免提交到 Git
|
||||
2. **n8n 需通過反向代理**: localhost:5678 無法直接訪問 API,需通過 Caddy
|
||||
3. **重啟生效**: 修改 `opencode.json` 後需重啟 OpenCode
|
||||
|
||||
### 參考文件
|
||||
- `docs/OPENCODE_GUIDE.md` - MCP 設定章節
|
||||
- `docs/INSTALL_N8N.md` - n8n 安裝指南
|
||||
|
||||
---
|
||||
|
||||
## n8n API 備份腳本 (2026-03-21)
|
||||
|
||||
### 腳本位置
|
||||
|
||||
| 腳本 | 說明 | 依賴 |
|
||||
|------|------|------|
|
||||
| `monitor/workflow/backup_n8n_api.py` | REST API 備份 | requests |
|
||||
| `monitor/workflow/backup_n8n_mcp.py` | MCP 備份(開發中) | mcp SDK |
|
||||
|
||||
### 使用方式
|
||||
|
||||
```bash
|
||||
# 備份所有 workflows
|
||||
N8N_API_KEY="..." python3.11 backup_n8n_api.py
|
||||
|
||||
# 只顯示變更(不備份)
|
||||
N8N_API_KEY="..." python3.11 backup_n8n_api.py --diff
|
||||
|
||||
# 差異備份(只備份變更的 workflows)
|
||||
N8N_API_KEY="..." python3.11 backup_n8n_api.py --incremental
|
||||
|
||||
# 列出可用備份
|
||||
python3.11 backup_n8n_api.py --list
|
||||
|
||||
# 驗證最新備份
|
||||
python3.11 backup_n8n_api.py --verify
|
||||
|
||||
# 顯示備份統計
|
||||
python3.11 backup_n8n_api.py --stats
|
||||
|
||||
# 只備份啟用的 workflows
|
||||
N8N_API_KEY="..." python3.11 backup_n8n_api.py --active-only
|
||||
|
||||
# 備份失敗的執行記錄
|
||||
N8N_API_KEY="..." python3.11 backup_n8n_api.py --failed-only
|
||||
```
|
||||
|
||||
### 功能
|
||||
|
||||
- [x] REST API 備份
|
||||
- [x] 變更偵測
|
||||
- [x] SHA256 校驗
|
||||
- [x] 備份版本化
|
||||
- [x] 差異備份(`--incremental`)
|
||||
- [x] 備份驗證(`--verify`)
|
||||
- [x] 備份統計(`--stats`)
|
||||
- [x] 失敗執行記錄備份(`--failed-only`)
|
||||
- [ ] 選擇性備份(按 Tags)
|
||||
|
||||
### 備份位置
|
||||
|
||||
```
|
||||
/Users/accusys/momentry/backup/n8n_workflows/api/
|
||||
├── 20260321_122059/
|
||||
│ ├── workflows.json # 21 workflows
|
||||
│ ├── workflows.json.sha256 # SHA256 校驗
|
||||
│ ├── tags.json # Tags(若有)
|
||||
│ └── manifest.json # 元數據
|
||||
└── ...
|
||||
```
|
||||
|
||||
### Crontab 建議
|
||||
|
||||
```bash
|
||||
# 每日備份(下午 3 點)
|
||||
0 15 * * * N8N_API_KEY="..." /opt/homebrew/bin/python3.11 /Users/accusys/momentry_core_0.1/monitor/workflow/backup_n8n_api.py >> /Users/accusys/momentry/log/monitor/workflow_backup_api.log 2>&1
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## API Key Management System (2026-03-21)
|
||||
|
||||
### 設計目標
|
||||
|
||||
為 Momentry Core 實現完整的 API Key 管理系統,包括:
|
||||
- API Key 生成(安全隨機)
|
||||
- Key 哈希(SHA256)
|
||||
- 異常檢測
|
||||
- 強制輪換機制
|
||||
- 審計日誌
|
||||
|
||||
### 模組架構
|
||||
|
||||
```
|
||||
src/core/api_key/
|
||||
├── mod.rs # 模組導出
|
||||
├── models.rs # 數據模型和類型
|
||||
├── service.rs # 核心服務邏輯
|
||||
├── anomaly.rs # 異常檢測
|
||||
└── rotation.rs # 輪換管理
|
||||
```
|
||||
|
||||
### Key 類型
|
||||
|
||||
| 類型 | 前綴 | 默認 TTL | 寬限期 |
|
||||
|------|------|----------|--------|
|
||||
| System | `msys_` | 365 天 | 72 小時 |
|
||||
| User | `muser_` | 90 天 | 24 小時 |
|
||||
| Service | `msvc_` | 180 天 | 48 小時 |
|
||||
| Integration | `mint_` | 30 天 | 24 小時 |
|
||||
| Emergency | `memg_` | 1 天 | 0 小時 |
|
||||
|
||||
### Key 格式
|
||||
|
||||
```
|
||||
{prefix}{uuid}_{timestamp}_{random}
|
||||
例如: muser_a1b2c3d4e5f6_1710998400_abc12345
|
||||
```
|
||||
|
||||
### 異常檢測閾值
|
||||
|
||||
| 指標 | 閾值 |
|
||||
|------|------|
|
||||
| 每分鐘請求數 | 1000 |
|
||||
| 每小時請求數 | 10000 |
|
||||
| 錯誤率 | 50% |
|
||||
| 每小時唯一 IP 數 | 5 |
|
||||
| 鎖定閾值 | 3 次觸發 |
|
||||
|
||||
### 實現狀態
|
||||
|
||||
| 組件 | 狀態 | 說明 |
|
||||
|------|------|------|
|
||||
| 數據模型 | ✅ 完成 | `models.rs` |
|
||||
| Key 生成/哈希 | ✅ 完成 | `service.rs` |
|
||||
| 異常檢測 | ✅ 完成 | `anomaly.rs` |
|
||||
| 輪換機制 | ✅ 完成 | `rotation.rs` |
|
||||
| CLI 命令 | ✅ 完成 | `main.rs` |
|
||||
| 數據庫集成 | ✅ 完成 | `postgres_db.rs` |
|
||||
| Redis 告警 | ✅ 完成 | `redis_client.rs` |
|
||||
| 數據庫遷移 | ✅ 完成 | `migrations/001_api_key_management.sql` |
|
||||
| 單元測試 | ✅ 完成 | 55 個測試通過 |
|
||||
|
||||
### CLI 命令
|
||||
|
||||
```bash
|
||||
# 創建 API Key
|
||||
momentry api-key create <name> --key-type service --ttl 90
|
||||
|
||||
# 列出所有 Keys
|
||||
momentry api-key list
|
||||
|
||||
# 驗證 Key
|
||||
momentry api-key validate --key <key>
|
||||
|
||||
# 撤銷 Key
|
||||
momentry api-key revoke --key <key>
|
||||
|
||||
# 請求輪換
|
||||
momentry api-key rotate --key <key>
|
||||
|
||||
# 顯示統計
|
||||
momentry api-key stats
|
||||
```
|
||||
|
||||
### 參考文件
|
||||
|
||||
- `src/core/api_key/` - API Key 模組
|
||||
- `docs/API_KEY_MANAGEMENT.md` - 設計文檔
|
||||
- `migrations/001_api_key_management.sql` - 數據庫遷移
|
||||
|
||||
---
|
||||
|
||||
## 問題 #5: Redis 用戶名問題 (2026-03-21)
|
||||
|
||||
### 問題描述
|
||||
|
||||
Redis 僅有 `default` 用戶,無 `accusys` 用戶。`.env` 檔案使用 `redis://accusys:accusys@localhost:6379` 格式會導致認證失敗。
|
||||
|
||||
### 測試結果
|
||||
|
||||
| URL 格式 | 結果 |
|
||||
|----------|------|
|
||||
| `redis://accusys:accusys@localhost:6379` | ❌ AUTH failed |
|
||||
| `redis://:accusys@localhost:6379` | ✅ PONG |
|
||||
|
||||
### Redis ACL 狀態
|
||||
|
||||
```
|
||||
user default on sanitize-payload #1bd51c... ~* &* +@all
|
||||
requirepass: accusys
|
||||
```
|
||||
|
||||
### 根本原因
|
||||
|
||||
1. Redis 啟動時僅設定 `--requirepass accusys`
|
||||
2. 未建立自訂用戶 `accusys`
|
||||
3. ACL 變更不會持久化(無 config file)
|
||||
|
||||
### 已執行修復
|
||||
|
||||
| 項目 | 修改 |
|
||||
|------|------|
|
||||
| `.env` | `redis://accusys:accusys@localhost:6379` → `redis://:accusys@localhost:6379` |
|
||||
|
||||
### 待解決問題
|
||||
|
||||
1. **ACL 持久化**:Redis 啟動後手動建立的用戶不會保留(重啟後消失)
|
||||
2. **需配置 ACL 文件**:建議建立 `users.acl` 並在 plist 中指定
|
||||
|
||||
### 建議解決方案
|
||||
|
||||
#### 方案 A:使用默認用戶(現行)
|
||||
|
||||
```bash
|
||||
# .env
|
||||
REDIS_URL=redis://:accusys@localhost:6379
|
||||
```
|
||||
|
||||
**優點**:簡單,無需修改 Redis 配置
|
||||
**缺點**:所有應用共享默認用戶
|
||||
|
||||
#### 方案 B:建立 ACL 配置文件
|
||||
|
||||
```bash
|
||||
# 1. 創建 ACL 文件
|
||||
cat > /Users/accusys/momentry/etc/redis/users.acl << 'EOF'
|
||||
user default on sanitize-payload ~* &* +@all >accusys
|
||||
user accusys on sanitize-payload ~* &* +@all >accusys
|
||||
EOF
|
||||
|
||||
# 2. 修改 plist 添加 --aclfile 參數
|
||||
--aclfile /Users/accusys/momentry/etc/redis/users.acl
|
||||
|
||||
# 3. 重啟 Redis
|
||||
sudo launchctl unload /Library/LaunchDaemons/com.momentry.redis.plist
|
||||
sudo launchctl load /Library/LaunchDaemons/com.momentry.redis.plist
|
||||
```
|
||||
|
||||
**優點**:支持多用戶,ACL 持久化
|
||||
**缺點**:需修改 plist 並重啟
|
||||
|
||||
### 影響範圍
|
||||
|
||||
- `src/core/config.rs` - REDIS_URL 讀取
|
||||
- `src/core/db/redis_client.rs` - Redis 連線
|
||||
- `momentry api-key` 命令 - 異常告警
|
||||
|
||||
### 狀態
|
||||
|
||||
- [x] 已確認問題存在
|
||||
- [x] 已修改 `.env` 使用默認用戶
|
||||
- [ ] 待決定是否實施 ACL 方案
|
||||
|
||||
---
|
||||
|
||||
## 問題 #6: Momentry Core API 未開機自動啟動 (2026-03-23)
|
||||
|
||||
### 問題描述
|
||||
|
||||
Momentry Core API 服務 (`momentry server --port 3002`) 未設定 launchd,導致:
|
||||
1. 系統重啟後 API 服務不會自動啟動
|
||||
2. `api.momentry.ddns.net` 返回 502 Bad Gateway
|
||||
3. n8n workflow 呼叫 API 時失敗
|
||||
|
||||
### 發現過程
|
||||
|
||||
1. n8n workflow 呼叫 `https://api.momentry.ddns.net/api/v1/n8n/search` 返回 502
|
||||
2. 檢查發現 port 3002 無服務運行
|
||||
3. Caddy 配置正向確,但後端服務未啟動
|
||||
4. 手動啟動服務後 API 正常運作
|
||||
|
||||
### 配置需求
|
||||
|
||||
| 項目 | 值 |
|
||||
|------|-----|
|
||||
| 服務名稱 | `com.momentry.api` |
|
||||
| 二進位檔 | `/Users/accusys/momentry_core_0.1/target/release/momentry` |
|
||||
| 命令 | `server --port 3002` |
|
||||
| Port | 3002 |
|
||||
| 環境變數 | `DATABASE_URL`, `REDIS_URL` 等 |
|
||||
|
||||
### 待辦事項
|
||||
|
||||
- [x] 建立 `docs/INSTALL_MOMENTRY_API.md` 安裝文件
|
||||
- [x] 建立 `/Library/LaunchDaemons/com.momentry.api.plist`
|
||||
- [x] 設定環境變數 (`DATABASE_URL`, `REDIS_URL` 等)
|
||||
- [x] 測試 launchctl load/unload
|
||||
- [x] 驗證開機自動啟動 (launchd 載入成功)
|
||||
|
||||
### 完成日期
|
||||
2026-03-23
|
||||
|
||||
### 參考文件
|
||||
|
||||
- `/Library/LaunchDaemons/com.momentry.n8n.main.plist` - n8n plist 範例
|
||||
- `docs/INSTALL_N8N.md` - plist 配置說明
|
||||
|
||||
---
|
||||
|
||||
## 服務統一遷移 (2026-03-24)
|
||||
|
||||
### 問題描述
|
||||
|
||||
Reboot 後發現 n8n workflow 數量從 42 變成 41,確認是 PostgreSQL 資料庫問題。經過調查發現:
|
||||
|
||||
1. **兩組不同的 PostgreSQL 資料目錄**:
|
||||
- Homebrew plist: `/opt/homebrew/var/postgresql@18` (有最新資料)
|
||||
- Custom plist: `/Users/accusys/momentry/var/postgresql` (可能是舊資料)
|
||||
|
||||
2. **Reboot 時 custom plist 搶先啟動**,使用了錯誤的資料目錄
|
||||
|
||||
### 解決方案
|
||||
|
||||
1. **統一使用 custom plist**:
|
||||
- 刪除 homebrew plist (`~/Library/LaunchAgents/homebrew.mxcl.postgresql@18.plist`)
|
||||
- Custom plist 使用 `/Users/accusys/momentry/var/postgresql` 作為資料目錄
|
||||
- 將所有 14 個服務的 plist 註冊到 launchd
|
||||
|
||||
2. **所有已遷移的服務**:
|
||||
|
||||
| 服務 | Plist | 資料目錄 |
|
||||
|------|-------|----------|
|
||||
| PostgreSQL | ✅ | `/Users/accusys/momentry/var/postgresql` |
|
||||
| MariaDB | ✅ | `/Users/accusys/momentry/var/mariadb` |
|
||||
| MongoDB | ✅ | `/opt/homebrew/var/mongodb` |
|
||||
| Redis | ✅ | - |
|
||||
| Ollama | ✅ | - |
|
||||
| Qdrant | ✅ | - |
|
||||
| n8n Main | ✅ | - |
|
||||
| n8n Worker | ✅ | - |
|
||||
| Caddy | ✅ | - |
|
||||
| SFTPGo | ✅ | - |
|
||||
| Gitea | ✅ | - |
|
||||
| Gitea MCP | ✅ | - |
|
||||
| PHP | ✅ | - |
|
||||
| Momentry API | ✅ | - |
|
||||
| RustDesk HBBR | ✅ | - |
|
||||
| RustDesk HBBS | ✅ | - |
|
||||
|
||||
### 還發現的 Homebrew 服務 (未遷移)
|
||||
|
||||
| 服務 | 建議 |
|
||||
|------|------|
|
||||
| homebrew.mxcl.grafana | ⚠️ 考慮遷移 |
|
||||
| homebrew.mxcl.prometheus | ⚠️ 考慮遷移 |
|
||||
| homebrew.mxcl.openwebui | ⚠️ 考慮遷移 |
|
||||
| homebrew.mxcl.kafka | ⚠️ 考慮遷移 |
|
||||
| homebrew.mxcl.seaweedfs | ⚠️ 考慮遷移 |
|
||||
| homebrew.mxcl.netdata | ⚠️ 考慮遷移 |
|
||||
| homebrew.mxcl.ddclient | ⚠️ 動態 DNS |
|
||||
| homebrew.mxcl.shadowsocks-rust | ⚠️ VPN |
|
||||
|
||||
### 預防措施
|
||||
|
||||
1. **確保統一資料目錄**:所有服務只使用一個資料目錄
|
||||
2. **Reboot 測試**:遷移完成後需進行 Reboot 測試
|
||||
3. **文件同步**:plist 檔案同步到 repo
|
||||
|
||||
### 完成日期
|
||||
2026-03-24
|
||||
|
||||
### 參考文件
|
||||
|
||||
- `docs/SERVICES.md` - 服務管理文檔
|
||||
- `docs/SERVICE_ADDITION_GUIDE.md` - 服務添加規範
|
||||
- `momentry_runtime/plist/` - plist 檔案存放位置
|
||||
|
||||
---
|
||||
|
||||
## Job Worker 實作 (2026-03-24)
|
||||
|
||||
### 目標
|
||||
|
||||
實作輪詢式 Job Worker,實現檔案註冊後自動觸發處理:
|
||||
|
||||
1. **輪詢機制**:Worker 定期輪詢 jobs 佇列
|
||||
2. **並行處理**:最多 2 個 processor 同時執行
|
||||
3. **失敗容忍**:任一模組獨立,失敗可接續
|
||||
|
||||
### 設計決策
|
||||
|
||||
| 項目 | 決策 | 理由 |
|
||||
|------|------|------|
|
||||
| 觸發方式 | 輪詢(Job Worker) | 暫無可靠 API 觸發 |
|
||||
| 並行處理 | 最多 2 個 | 可根據 CPU/GPU 調整 |
|
||||
| 失敗處理 | 獨立模組,部分完成可接續 | 任何模組失敗都產出狀態 |
|
||||
| Worker 啟動 | 獨立進程 | 隔離、易管理 |
|
||||
| 並行上限 | 環境變數 + 預設值 | 靈活調整 |
|
||||
| 狀態同步 | PostgreSQL + Redis | 可靠 + 即時 |
|
||||
|
||||
### 環境變數
|
||||
|
||||
| 變數 | 預設值 | 說明 |
|
||||
|------|--------|------|
|
||||
| `MOMENTRY_MAX_CONCURRENT` | 2 | 最大並行 processor 數 |
|
||||
| `MOMENTRY_POLL_INTERVAL` | 5 | 輪詢間隔(秒) |
|
||||
| `MOMENTRY_WORKER_ENABLED` | true | 是否啟用 worker |
|
||||
|
||||
### 實作計畫
|
||||
|
||||
詳細內容請參考 [JOB_WORKER_IMPLEMENTATION_PLAN.md](./JOB_WORKER_IMPLEMENTATION_PLAN.md)
|
||||
|
||||
### Phase 規劃
|
||||
|
||||
| Phase | 任務 | 預估工時 |
|
||||
|-------|------|----------|
|
||||
| 1 | 資料庫遷移 | 2h |
|
||||
| 2 | Worker 框架 | 4h |
|
||||
| 3 | Register API 整合 | 2h |
|
||||
| 4 | Processor 執行 | 4h |
|
||||
| 5 | 進度追蹤 | 2h |
|
||||
| 6 | API 端點 | 3h |
|
||||
| 7 | CLI 命令 | 2h |
|
||||
| 8 | 測試 | 4h |
|
||||
|
||||
**總預估**: ~23h
|
||||
|
||||
### 實作結構
|
||||
|
||||
```
|
||||
src/
|
||||
├── worker/
|
||||
│ ├── mod.rs # Worker 模組導出
|
||||
│ ├── config.rs # Worker 配置
|
||||
│ ├── worker.rs # Worker 主邏輯
|
||||
│ ├── processor.rs # Processor 執行器
|
||||
│ ├── queue.rs # Job 佇列管理
|
||||
│ └── progress.rs # 進度追蹤
|
||||
├── api/
|
||||
│ └── server.rs # 更新 Register API
|
||||
└── main.rs # 新增 worker 命令
|
||||
```
|
||||
|
||||
### 狀態
|
||||
|
||||
- [x] 系統分析完成
|
||||
- [x] 實作計畫文件建立
|
||||
- [ ] Phase 1: 資料庫遷移
|
||||
- [ ] Phase 2: Worker 框架
|
||||
- [ ] Phase 3: Register API 整合
|
||||
- [ ] Phase 4: Processor 執行
|
||||
- [ ] Phase 5-8: 依序實作
|
||||
|
||||
### 參考文件
|
||||
|
||||
- `docs/JOB_WORKER_IMPLEMENTATION_PLAN.md` - 完整實作計畫
|
||||
- `docs/PROCESSING_PIPELINE.md` - 處理流程
|
||||
- `docs/MOMENTRY_CORE_REDIS_KEYS.md` - Redis Key 設計
|
||||
|
||||
---
|
||||
|
||||
## 統一會員系統 + 影片歸屬追蹤 (2026-03-24)
|
||||
|
||||
### 目標
|
||||
|
||||
建立統一的會員系統:
|
||||
1. WordPress 作為唯一登入入口
|
||||
2. 每個影片關聯到 user_id(追蹤歸屬)
|
||||
3. Per-user 配額管理
|
||||
4. API 端點啟用認證
|
||||
|
||||
### 實作計畫
|
||||
|
||||
詳細內容請參考 [USER_MANAGEMENT_PLAN.md](./USER_MANAGEMENT_PLAN.md)
|
||||
|
||||
### Phase 規劃
|
||||
|
||||
| Phase | 任務 | 複雜度 | 優先級 | 預估工時 |
|
||||
|-------|------|--------|--------|----------|
|
||||
| 1 | WordPress Application Passwords 測試 | 低 | P0 | 1.5h |
|
||||
| 2 | 資料庫遷移 (users 表) | 中 | P0 | 3h |
|
||||
| 3 | API auth middleware | 中 | P0 | 4h |
|
||||
| 4 | Register API 更新 | 低 | P0 | 2h |
|
||||
| 5 | Admin users API | 中 | P1 | 4h |
|
||||
| 6 | n8n workflow | 中 | P1 | 6h |
|
||||
| 7 | 配額管理 | 中 | P2 | 4h |
|
||||
| 8 | 測試驗證 | 中 | P2 | 4h |
|
||||
|
||||
**總預估**: ~28.5h
|
||||
|
||||
### 待確認事項
|
||||
|
||||
- [ ] WordPress 用戶建立方式(手動/Elementor表單)
|
||||
- [ ] API Key 格式確認
|
||||
- [ ] SFTPGo 整合方式
|
||||
- [ ] 配額管理策略
|
||||
- [ ] 用戶刪除同步流程
|
||||
|
||||
### 狀態
|
||||
|
||||
- [x] 系統分析完成
|
||||
- [x] 實作計畫文件建立
|
||||
- [ ] Phase 1: WordPress 認證測試
|
||||
- [ ] Phase 2: 資料庫遷移
|
||||
- [ ] Phase 3-8: 依序實作
|
||||
|
||||
### 參考文件
|
||||
|
||||
- `docs/USER_MANAGEMENT_PLAN.md` - 完整實作計畫
|
||||
- `docs/API_KEY_MANAGEMENT.md` - API Key 管理
|
||||
- `docs/SFTPGO_DEMO_USER.md` - SFTPGo 用戶設定
|
||||
@@ -1,392 +0,0 @@
|
||||
# Playground Binary Implementation Plan
|
||||
|
||||
| Item | Content |
|
||||
|------|---------|
|
||||
| Author | Warren |
|
||||
| Created | 2026-03-23 |
|
||||
| Document Version | V1.0 |
|
||||
|
||||
---
|
||||
|
||||
## Version History
|
||||
|
||||
| Version | Date | Purpose | Operator | Tool/Model |
|
||||
|---------|------|---------|----------|------------|
|
||||
| V1.0 | 2026-03-23 | Create implementation plan | Warren | OpenCode |
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
Create separate `momentry_playground` binary with distinct configuration from `momentry` (production).
|
||||
|
||||
| Aspect | Production (`momentry`) | Development (`momentry_playground`) |
|
||||
|--------|------------------------|-------------------------------------|
|
||||
| **Port** | 3002 | 3003 |
|
||||
| **Redis Prefix** | `momentry:` | `momentry_dev:` |
|
||||
| **Worker** | Enabled | Disabled |
|
||||
| **Purpose** | Production deployment | Testing/Development |
|
||||
|
||||
---
|
||||
|
||||
## Files to Modify
|
||||
|
||||
```
|
||||
Files Changed: 6 files (+1 new)
|
||||
├── src/core/config.rs ← Add server_port(), redis_key_prefix()
|
||||
├── src/core/db/redis_client.rs ← Replace hardcoded prefixes
|
||||
├── src/core/cache/redis_cache.rs ← Use configurable prefix
|
||||
├── src/main.rs ← Update CLI defaults
|
||||
├── src/playground.rs ← NEW: Development binary
|
||||
├── Cargo.toml ← Add new binary
|
||||
└── .env.development ← NEW: Dev environment config
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Implementation Steps
|
||||
|
||||
### Step 1: Update `src/core/config.rs`
|
||||
|
||||
Add after line 51 (after `MEDIA_BASE_URL`):
|
||||
|
||||
```rust
|
||||
pub static SERVER_PORT: Lazy<u16> = Lazy::new(|| {
|
||||
env::var("MOMENTRY_SERVER_PORT")
|
||||
.unwrap_or_else(|_| "3002".to_string())
|
||||
.parse()
|
||||
.unwrap_or(3002)
|
||||
});
|
||||
|
||||
pub static REDIS_KEY_PREFIX: Lazy<String> = Lazy::new(|| {
|
||||
env::var("MOMENTRY_REDIS_PREFIX")
|
||||
.unwrap_or_else(|_| "momentry:".to_string())
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Step 2: Update `src/core/db/redis_client.rs`
|
||||
|
||||
Replace all hardcoded `momentry:` prefixes with configurable prefix.
|
||||
|
||||
**Import at top:**
|
||||
```rust
|
||||
use crate::core::config::REDIS_KEY_PREFIX;
|
||||
```
|
||||
|
||||
**Pattern for each method:**
|
||||
```rust
|
||||
let prefix = REDIS_KEY_PREFIX.as_str();
|
||||
let key = format!("{}job:{}", prefix, uuid);
|
||||
```
|
||||
|
||||
**Affected lines:**
|
||||
|
||||
| Line | Key Pattern |
|
||||
|------|-------------|
|
||||
| 47 | `job:{uuid}` |
|
||||
| 81, 109 | `job:{uuid}:processor:{processor}` |
|
||||
| 136, 146 | `progress:{uuid}` |
|
||||
| 172 | `jobs:active` |
|
||||
| 179 | `jobs:active` → `jobs:completed` |
|
||||
| 187 | `jobs:active` → `jobs:failed` |
|
||||
| 194 | `jobs:active` |
|
||||
| 201, 208 | `health:momentry_core` |
|
||||
| 214 | `monitor:job:{uuid}` |
|
||||
| 242, 300 | `errors:{uuid}` |
|
||||
| 258, 281 | `anomaly:alerts`, `anomaly:key:{key_id}` |
|
||||
| 317, 346, 364, 392, 397 | `worker:job:{uuid}...` |
|
||||
| 406, 410 | `worker:job:*` |
|
||||
|
||||
---
|
||||
|
||||
### Step 3: Update `src/core/cache/redis_cache.rs`
|
||||
|
||||
**Import:**
|
||||
```rust
|
||||
use crate::core::config::REDIS_KEY_PREFIX;
|
||||
```
|
||||
|
||||
**Replace line 10:**
|
||||
```rust
|
||||
// Remove: const KEY_PREFIX: &str = "momentry:cache:";
|
||||
```
|
||||
|
||||
**Update `prefixed_key` method (line 24):**
|
||||
```rust
|
||||
fn prefixed_key(&self, key: &str) -> String {
|
||||
format!("{}cache:{}", REDIS_KEY_PREFIX.as_str(), key)
|
||||
}
|
||||
```
|
||||
|
||||
**Update tests (lines 161-162):**
|
||||
```rust
|
||||
#[test]
|
||||
fn test_prefixed_key() {
|
||||
// Note: This test will use the configured prefix
|
||||
let cache = RedisCache::new().unwrap();
|
||||
// With default prefix "momentry:"
|
||||
assert_eq!(cache.prefixed_key("test"), "momentry:cache:test");
|
||||
assert_eq!(cache.prefixed_key("video:abc"), "momentry:cache:video:abc");
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Step 4: Update `src/main.rs`
|
||||
|
||||
**Change CLI defaults (Lines 691-695):**
|
||||
|
||||
```rust
|
||||
// Before:
|
||||
#[arg(long, default_value = "3000")]
|
||||
port: u16,
|
||||
|
||||
// After:
|
||||
#[arg(long)]
|
||||
port: Option<u16>,
|
||||
```
|
||||
|
||||
**Update Server match arm (around line 2398):**
|
||||
|
||||
```rust
|
||||
Commands::Server { host, port } => {
|
||||
let port = port.unwrap_or_else(|| *crate::core::config::SERVER_PORT);
|
||||
momentry_core::api::start_server(&host, port).await?;
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
**Update Redis key usage (Line 1098):**
|
||||
|
||||
```rust
|
||||
// Before:
|
||||
let key = format!("momentry:job:{}:processor:{}", uuid, processor);
|
||||
|
||||
// After:
|
||||
let key = format!(
|
||||
"{}job:{}:processor:{}",
|
||||
crate::core::config::REDIS_KEY_PREFIX.as_str(),
|
||||
uuid,
|
||||
processor
|
||||
);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Step 5: Create `src/playground.rs`
|
||||
|
||||
```rust
|
||||
use anyhow::{Context, Result};
|
||||
use clap::{Parser, Subcommand};
|
||||
// ... same imports as main.rs ...
|
||||
|
||||
fn main() -> Result<()> {
|
||||
// Load development environment first
|
||||
dotenv::from_filename(".env.development").ok();
|
||||
|
||||
tracing_subscriber::fmt::init();
|
||||
tracing::info!("Starting momentry_playground (development binary)");
|
||||
tracing::info!("Port: {}", *momentry_core::core::config::SERVER_PORT);
|
||||
tracing::info!("Redis prefix: {}", *momentry_core::core::config::REDIS_KEY_PREFIX);
|
||||
|
||||
let cli = Cli::parse();
|
||||
// ... rest identical to main.rs ...
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Step 6: Update `Cargo.toml`
|
||||
|
||||
**Add after line 90:**
|
||||
|
||||
```toml
|
||||
[[bin]]
|
||||
name = "momentry_playground"
|
||||
path = "src/playground.rs"
|
||||
```
|
||||
|
||||
**Add dependency (if not present):**
|
||||
|
||||
```toml
|
||||
dotenv = "0.15"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Step 7: Create `.env.development`
|
||||
|
||||
```bash
|
||||
# Development Environment Configuration
|
||||
# Used by: momentry_playground binary
|
||||
|
||||
# Server Configuration
|
||||
MOMENTRY_SERVER_PORT=3003
|
||||
MOMENTRY_REDIS_PREFIX=momentry_dev:
|
||||
|
||||
# Worker Configuration (disabled for development)
|
||||
MOMENTRY_WORKER_ENABLED=false
|
||||
MOMENTRY_MAX_CONCURRENT=1
|
||||
MOMENTRY_POLL_INTERVAL=10
|
||||
|
||||
# Database (can use separate dev database)
|
||||
DATABASE_URL=postgres://accusys@localhost:5432/momentry
|
||||
MONGODB_URL=mongodb://accusys:Test3200Test3200@localhost:27017/admin
|
||||
|
||||
# Redis
|
||||
REDIS_URL=redis://:accusys@localhost:6379
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Step 8: Update `.env` (Production)
|
||||
|
||||
Add these lines:
|
||||
|
||||
```bash
|
||||
# Production Environment Configuration
|
||||
# Used by: momentry binary
|
||||
|
||||
# Server Configuration
|
||||
MOMENTRY_SERVER_PORT=3002
|
||||
MOMENTRY_REDIS_PREFIX=momentry:
|
||||
|
||||
# Worker Configuration
|
||||
MOMENTRY_WORKER_ENABLED=true
|
||||
MOMENTRY_MAX_CONCURRENT=2
|
||||
MOMENTRY_POLL_INTERVAL=5
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Testing Checklist
|
||||
|
||||
### 1. Build and Run Production Binary
|
||||
|
||||
```bash
|
||||
cargo build --release --bin momentry
|
||||
cargo run --bin momentry -- server
|
||||
# Expected: Listening on http://127.0.0.1:3002
|
||||
|
||||
cargo run --bin momentry -- worker
|
||||
# Expected: Worker started with momentry: prefix
|
||||
```
|
||||
|
||||
### 2. Build and Run Development Binary
|
||||
|
||||
```bash
|
||||
cargo build --bin momentry_playground
|
||||
cargo run --bin momentry_playground -- server
|
||||
# Expected: Listening on http://127.0.0.1:3003
|
||||
```
|
||||
|
||||
### 3. Verify Redis Key Isolation
|
||||
|
||||
```bash
|
||||
# Production data
|
||||
redis-cli KEYS "momentry:*"
|
||||
# Development data
|
||||
redis-cli KEYS "momentry_dev:*"
|
||||
# Should be separate
|
||||
```
|
||||
|
||||
### 4. Run Both Simultaneously
|
||||
|
||||
```bash
|
||||
# Terminal 1: Production
|
||||
cargo run --bin momentry -- server
|
||||
|
||||
# Terminal 2: Development
|
||||
cargo run --bin momentry_playground -- server
|
||||
|
||||
# Both should run without port conflicts
|
||||
```
|
||||
|
||||
### 5. Unit Tests
|
||||
|
||||
```bash
|
||||
cargo test --lib
|
||||
# All tests should pass
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Redis Key Structure
|
||||
|
||||
### Production (`momentry:`)
|
||||
|
||||
```
|
||||
momentry:job:{uuid} # Job status
|
||||
momentry:job:{uuid}:processor:{name} # Processor progress
|
||||
momentry:progress:{uuid} # Progress pub/sub
|
||||
momentry:jobs:active # Active job set
|
||||
momentry:jobs:completed # Completed job set
|
||||
momentry:jobs:failed # Failed job set
|
||||
momentry:health:momentry_core # Health status
|
||||
momentry:cache:{key} # Cache entries
|
||||
momentry:worker:job:{uuid} # Worker job
|
||||
momentry:worker:job:{uuid}:processor:{name}
|
||||
```
|
||||
|
||||
### Development (`momentry_dev:`)
|
||||
|
||||
```
|
||||
momentry_dev:job:{uuid}
|
||||
momentry_dev:job:{uuid}:processor:{name}
|
||||
momentry_dev:progress:{uuid}
|
||||
momentry_dev:jobs:active
|
||||
momentry_dev:jobs:completed
|
||||
momentry_dev:jobs:failed
|
||||
momentry_dev:health:momentry_core
|
||||
momentry_dev:cache:{key}
|
||||
momentry_dev:worker:job:{uuid}
|
||||
momentry_dev:worker:job:{uuid}:processor:{name}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Potential Issues & Solutions
|
||||
|
||||
| Issue | Solution |
|
||||
|-------|----------|
|
||||
| `dotenv` crate not in dependencies | Add to Cargo.toml |
|
||||
| Tests use hardcoded prefix | Update tests to use config, or use `#[cfg(test)]` defaults |
|
||||
| Worker starts in playground | Check `MOMENTRY_WORKER_ENABLED=false` in `.env.development` |
|
||||
| Port already in use | Graceful error message with suggestion to use `--port` flag |
|
||||
| Mixed data in Redis | Ensure prefix is loaded before any Redis operations |
|
||||
|
||||
---
|
||||
|
||||
## Files Summary
|
||||
|
||||
| File | Lines Changed | Purpose |
|
||||
|------|---------------|---------|
|
||||
| `src/core/config.rs` | +15 | Add SERVER_PORT and REDIS_KEY_PREFIX |
|
||||
| `src/core/db/redis_client.rs` | ~50 | Replace hardcoded prefixes |
|
||||
| `src/core/cache/redis_cache.rs` | ~10 | Use configurable prefix |
|
||||
| `src/main.rs` | ~15 | Update CLI defaults, Redis key usage |
|
||||
| `src/playground.rs` | NEW (~2800) | Development binary |
|
||||
| `Cargo.toml` | +4 | Add binary definition |
|
||||
| `.env.development` | NEW (~20) | Development environment |
|
||||
|
||||
**Total**: ~60 lines modified + ~2800 lines new file
|
||||
|
||||
---
|
||||
|
||||
## Reference Documents
|
||||
|
||||
| Document | Purpose |
|
||||
|----------|---------|
|
||||
| `docs/SERVICES.md` | Port allocations |
|
||||
| `docs/MOMENTRY_CORE_REDIS_KEYS.md` | Redis key design |
|
||||
| `AGENTS.md` | Code style and conventions |
|
||||
|
||||
---
|
||||
|
||||
## Version History
|
||||
|
||||
| Version | Date | Author | Changes |
|
||||
|---------|------|--------|---------|
|
||||
| 1.0 | 2025-03-25 | OpenCode | Initial implementation plan |
|
||||
@@ -1,292 +0,0 @@
|
||||
# Video Processing Pipeline - 處理流程
|
||||
|
||||
| 項目 | 內容 |
|
||||
|------|------|
|
||||
| 建立者 | Warren |
|
||||
| 建立時間 | 2026-03-22 |
|
||||
| 文件版本 | V1.1 |
|
||||
|
||||
---
|
||||
|
||||
## 版本歷史
|
||||
|
||||
| 版本 | 日期 | 目的 | 操作人 | 工具/模型 |
|
||||
|------|------|------|--------|-----------|
|
||||
| V1.0 | 2026-03-22 | 創建文件 | Warren | OpenCode |
|
||||
| V1.1 | 2026-03-26 | 更新流程圖文字 (media_url→file_path) | OpenCode | deepseek-reasoner |
|
||||
|
||||
---
|
||||
|
||||
## 處理流程架構
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────────────┐
|
||||
│ Video Processing Pipeline │
|
||||
├─────────────────────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ ┌─────────────────────────────────────────────────────────────────────┐ │
|
||||
│ │ Stage 1: JSON 生成 (Process) │ │
|
||||
│ │ │ │
|
||||
│ │ video.mp4 ──→ [ASR] ──→ asr.json (語音辨識) │ │
|
||||
│ │ ──→ [CUT] ──→ cut.json (場景偵測) │ │
|
||||
│ │ ──→ [ASRX] ──→ asrx.json (說話者分離) │ │
|
||||
│ │ ──→ [YOLO] ──→ yolo.json (物體偵測) │ │
|
||||
│ │ ──→ [OCR] ──→ ocr.json (文字辨識) │ │
|
||||
│ │ ──→ [Face] ──→ face.json (人臉偵測) │ │
|
||||
│ │ ──→ [Pose] ──→ pose.json (姿態估計) │ │
|
||||
│ └─────────────────────────────────────────────────────────────────────┘ │
|
||||
│ ↓ │
|
||||
│ ┌─────────────────────────────────────────────────────────────────────┐ │
|
||||
│ │ Stage 2: 入庫 (Import) │ │
|
||||
│ │ │ │
|
||||
│ │ .json files ──→ PostgreSQL (fs_json = true) │ │
|
||||
│ │ ↓ │ │
|
||||
│ │ pre_chunks 表 (from ASR, CUT) │ │
|
||||
│ │ frames 表 (from YOLO, OCR, Face, Pose) │ │
|
||||
│ └─────────────────────────────────────────────────────────────────────┘ │
|
||||
│ ↓ │
|
||||
│ ┌─────────────────────────────────────────────────────────────────────┐ │
|
||||
│ │ Stage 3: Chunk 生成 (Chunk) │ │
|
||||
│ │ │ │
|
||||
│ │ pre_chunks ──→ [Chunk Rule] ──→ chunks 表 │ │
|
||||
│ │ ↓ │ │
|
||||
│ │ 清洗 → 純文字 │ │
|
||||
│ └─────────────────────────────────────────────────────────────────────┘ │
|
||||
│ ↓ │
|
||||
│ ┌─────────────────────────────────────────────────────────────────────┐ │
|
||||
│ │ Stage 4: 向量化 (Vectorize) │ │
|
||||
│ │ │ │
|
||||
│ │ chunks ──→ [Embedding Model] ──→ vectors │ │
|
||||
│ │ ↓ │ │
|
||||
│ │ Qdrant (主要向量庫) │ │
|
||||
│ │ PGVector (備份向量庫) │ │
|
||||
│ └─────────────────────────────────────────────────────────────────────┘ │
|
||||
│ ↓ │
|
||||
│ ┌─────────────────────────────────────────────────────────────────────┐ │
|
||||
│ │ Stage 5: 搜尋 (Search) │ │
|
||||
│ │ │ │
|
||||
│ │ Natural Language Query ──→ [Embedding] ──→ [Qdrant Search] │ │
|
||||
│ │ ↓ │ │
|
||||
│ │ 返回結果含 file_path │ │
|
||||
│ └─────────────────────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## CLI 命令
|
||||
|
||||
### Stage 1: JSON 生成 (Process)
|
||||
|
||||
```bash
|
||||
# 基本用法
|
||||
cargo run --bin momentry -- process <uuid_or_path>
|
||||
|
||||
# 只處理特定模組
|
||||
cargo run --bin momentry -- process <uuid> --modules asr,cut
|
||||
|
||||
# 強制重新處理(忽略完整性檢查)
|
||||
cargo run --bin momentry -- process <uuid> --force
|
||||
|
||||
# 從中斷點續傳
|
||||
cargo run --bin momentry -- process <uuid> --resume
|
||||
|
||||
# 模組使用雲端處理
|
||||
cargo run --bin momentry -- process <uuid> --modules yolo,face --cloud yolo
|
||||
|
||||
# 完整範例
|
||||
cargo run --bin momentry -- process /path/to/video.mp4 \
|
||||
--modules asr,cut,yolo,ocr \
|
||||
--cloud yolo
|
||||
```
|
||||
|
||||
### Stage 2: 入庫 (Import)
|
||||
|
||||
```bash
|
||||
# 目前入庫在 process 完成後自動執行
|
||||
# 計劃新增獨立的 import 命令
|
||||
# cargo run --bin momentry -- import <uuid>
|
||||
```
|
||||
|
||||
### Stage 3: Chunk 生成
|
||||
|
||||
```bash
|
||||
# 生成 chunks
|
||||
cargo run --bin momentry -- chunk <uuid>
|
||||
```
|
||||
|
||||
### Stage 4: 向量化
|
||||
|
||||
```bash
|
||||
# 向量化 chunks(使用預設模型 nomic-embed-text-v2-moe:latest)
|
||||
cargo run --bin momentry -- vectorize <uuid>
|
||||
|
||||
# 明確指定模型
|
||||
cargo run --bin momentry -- vectorize <uuid> --model nomic-embed-text-v2-moe:latest
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 處理模式選項
|
||||
|
||||
### --force (強制重新處理)
|
||||
|
||||
- 刪除現有的 JSON 檔案
|
||||
- 從頭開始處理
|
||||
- 適用於:處理失敗、模型更新、需要重新處理
|
||||
|
||||
```bash
|
||||
# 強制重新處理 YOLO
|
||||
cargo run --bin momentry -- process <uuid> --modules yolo --force
|
||||
```
|
||||
|
||||
### --resume (續傳)
|
||||
|
||||
- 檢查現有 JSON 的進度
|
||||
- 從中斷點繼續處理
|
||||
- 適用於:處理中斷、系統崩潰後恢復
|
||||
|
||||
```bash
|
||||
# 從上次中斷點繼續
|
||||
cargo run --bin momentry -- process <uuid> --resume
|
||||
```
|
||||
|
||||
### 預設行為 (Smart Mode)
|
||||
|
||||
- 如果 JSON 完全:跳過
|
||||
- 如果 JSON 不完整:警告 + 跳過(需要 --resume 或 --force)
|
||||
- 如果 JSON 不存在:處理
|
||||
|
||||
```
|
||||
Output:
|
||||
ASR: ✓ Already complete, skipping
|
||||
|
||||
⚠️ Found incomplete JSON file: /path/to/yolo.json
|
||||
Progress: 73800/412343 (17.9%)
|
||||
Use --resume to continue from checkpoint
|
||||
Use --force to reprocess from scratch
|
||||
YOLO: ✓ Already complete, skipping
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 可用模組
|
||||
|
||||
| 模組 | 功能 | 輸出 | 用途 |
|
||||
|------|------|------|------|
|
||||
| asr | 自動語音辨識 | asr.json | 語音轉文字 |
|
||||
| cut | 場景偵測 | cut.json | 影片分段 |
|
||||
| asrx | 說話者分離 | asrx.json | 多人對話分析 |
|
||||
| yolo | 物體偵測 | yolo.json | 物體辨識 |
|
||||
| ocr | 文字辨識 | ocr.json | 畫面文字 |
|
||||
| face | 人臉偵測 | face.json | 人臉辨識 |
|
||||
| pose | 姿態估計 | pose.json | 人體姿態 |
|
||||
|
||||
---
|
||||
|
||||
## 向量化模型選擇
|
||||
|
||||
### 統一嵌入模型
|
||||
Momentry Core 統一使用 **`nomic-embed-text-v2-moe:latest`** 作為所有規則的嵌入模型:
|
||||
|
||||
```bash
|
||||
# 統一模型(所有 Rule 1/2/3 使用)
|
||||
--model nomic-embed-text-v2-moe:latest
|
||||
```
|
||||
|
||||
### 模型特性
|
||||
| 特性 | 說明 |
|
||||
|------|------|
|
||||
| **模型名稱** | `nomic-embed-text-v2-moe:latest` |
|
||||
| **向量維度** | 768 維 |
|
||||
| **多語言支持** | ✅ 完整支持(英語、中文、日語、韓語等) |
|
||||
| **模型架構** | Mixture of Experts (MoE) |
|
||||
| **推理速度** | 快速,適合實時應用 |
|
||||
|
||||
### 使用方式
|
||||
```bash
|
||||
# 向量化命令
|
||||
cargo run --bin momentry -- vectorize <uuid> --model nomic-embed-text-v2-moe:latest
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 資料庫儲存
|
||||
|
||||
### PostgreSQL (主要關聯式資料庫)
|
||||
|
||||
- 影片資訊
|
||||
- Chunks 資料
|
||||
- Pre-chunks 資料
|
||||
- Frames 資料
|
||||
- 使用者資料
|
||||
|
||||
### Qdrant (主要向量資料庫)
|
||||
|
||||
- Chunk 向量
|
||||
- 相似度搜尋
|
||||
|
||||
### PGVector (備份向量資料庫)
|
||||
|
||||
- Chunk 向量副本
|
||||
- 備援機制
|
||||
|
||||
---
|
||||
|
||||
## Pipeline 狀態追蹤
|
||||
|
||||
### PostgreSQL 狀態欄位
|
||||
|
||||
```sql
|
||||
-- 影片處理狀態
|
||||
videos.status: 'pending' | 'processing' | 'completed' | 'failed'
|
||||
|
||||
-- 檔案處理狀態
|
||||
videos.fs_json: true/false
|
||||
videos.fs_chunks: true/false
|
||||
videos.fs_vectors: true/false
|
||||
|
||||
-- pre_chunks 狀態
|
||||
pre_chunks.imported: true/false
|
||||
|
||||
-- frames 狀態
|
||||
frames.imported: true/false
|
||||
|
||||
-- chunks 狀態
|
||||
chunks.cleaned: true/false
|
||||
chunks.vectorized: true/false
|
||||
```
|
||||
|
||||
### 進度查詢 API
|
||||
|
||||
```bash
|
||||
# 查詢處理進度
|
||||
curl http://localhost:3002/api/v1/progress/{uuid}
|
||||
|
||||
# 回應範例
|
||||
{
|
||||
"uuid": "a1b10138a6bbb0cd",
|
||||
"file_name": "video.mp4",
|
||||
"overall_progress": 65,
|
||||
"cpu_percent": 45.2,
|
||||
"gpu_percent": 98.5,
|
||||
"memory_mb": 8500,
|
||||
"processors": [
|
||||
{"name": "asr", "status": "complete", "progress": 100},
|
||||
{"name": "cut", "status": "complete", "progress": 100},
|
||||
{"name": "yolo", "status": "progress", "progress": 45},
|
||||
{"name": "ocr", "status": "pending", "progress": 0}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 下一步
|
||||
|
||||
1. **API 端點** - 支援 --modules 和 --cloud 參數
|
||||
2. **獨立 Import 命令** - 分離入庫流程
|
||||
3. **獨立 Chunk 命令** - 分離 chunk 生成
|
||||
4. **獨立 Vectorize 命令** - 分離向量化流程
|
||||
5. **模型管理** - 新增、選擇、預覽模型
|
||||
568
docs/PYTHON.md
568
docs/PYTHON.md
@@ -1,568 +0,0 @@
|
||||
# Python 開發規範
|
||||
|
||||
| 項目 | 內容 |
|
||||
|------|------|
|
||||
| 建立者 | Warren |
|
||||
| 建立時間 | 2026-03-16 |
|
||||
| 文件版本 | V1.0 |
|
||||
|
||||
---
|
||||
|
||||
## 版本歷史
|
||||
|
||||
| 版本 | 日期 | 目的 | 操作人 | 工具/模型 |
|
||||
|------|------|------|--------|-----------|
|
||||
| V1.0 | 2026-03-16 | 創建文件 | Warren | OpenCode / MiniMax M2.5 |
|
||||
| V1.1 | 2026-03-21 | 新增 RedisPublisher API 文檔 | OpenCode | - |
|
||||
|
||||
---
|
||||
|
||||
## 概述
|
||||
|
||||
本文檔定義 Momentry 專案中 Python 程式碼的開發標準與最佳實踐。
|
||||
|
||||
---
|
||||
|
||||
## 版本管理
|
||||
|
||||
### 鎖定版本
|
||||
|
||||
| 版本 | 用途 | 路徑 |
|
||||
|------|------|------|
|
||||
| Python 3.11.14 | Momentry venv | /Users/accusys/momentry_core_0.1/venv/bin/python |
|
||||
| Python 3.11.14 | 系統安裝 | /opt/homebrew/bin/python3.11 |
|
||||
| Python 3.14.3 | 系統預設 | /opt/homebrew/bin/python3 |
|
||||
| Python 3.9.6 | 系統預設 (備用) | /usr/bin/python3 |
|
||||
|
||||
### 版本選擇原則
|
||||
|
||||
- **Momentry 專案**:使用 venv 中的 Python 3.11.14
|
||||
- **新專案**:建議使用 venv 管理
|
||||
- **系統工具**:可使用系統預設版本
|
||||
|
||||
---
|
||||
|
||||
## 腳本規範
|
||||
|
||||
### Shebang 宣告
|
||||
|
||||
所有 Momentry Python 腳本必須在第一行宣告明確的 Python 路徑:
|
||||
|
||||
```python
|
||||
#!/opt/homebrew/bin/python3.11
|
||||
```
|
||||
|
||||
**錯誤範例**:
|
||||
```python
|
||||
#!/usr/bin/env python3 # 會解析到系統預設 (3.14.3)
|
||||
#!/usr/bin/python3 # 會使用系統 Python (3.9.6)
|
||||
```
|
||||
|
||||
**正確範例**:
|
||||
```python
|
||||
#!/opt/homebrew/bin/python3.11
|
||||
import sys
|
||||
...
|
||||
```
|
||||
|
||||
### 檔案結構
|
||||
|
||||
```
|
||||
scripts/
|
||||
├── asr_processor.py # ASR 處理腳本
|
||||
├── thumbnail_extractor.py # 縮圖提取腳本
|
||||
└── new_script.py # 新腳本模板
|
||||
```
|
||||
|
||||
### 腳本模板
|
||||
|
||||
```python
|
||||
#!/opt/homebrew/bin/python3.11
|
||||
"""
|
||||
腳本名稱
|
||||
簡短描述腳本功能
|
||||
|
||||
用法:
|
||||
python3.11 script.py <args>
|
||||
|
||||
作者: Momentry Team
|
||||
版本: 1.0.0
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import logging
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format="%(asctime)s - %(levelname)s - %(message)s",
|
||||
stream=sys.stderr,
|
||||
)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="腳本功能描述")
|
||||
parser.add_argument("input", help="輸入檔案或參數")
|
||||
parser.add_argument("-o", "--output", default="output.json", help="輸出檔案")
|
||||
parser.add_argument("-v", "--verbose", action="store_true", help="詳細輸出")
|
||||
parser.add_argument("-c", "--count", type=int, default=10, help="數量")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.verbose:
|
||||
logger.setLevel(logging.DEBUG)
|
||||
|
||||
# 業務邏輯
|
||||
result = process_data(args.input, args.count)
|
||||
|
||||
# 輸出 JSON結果
|
||||
print(json.dumps(result))
|
||||
|
||||
|
||||
def process_data(input_path: str, count: int) -> dict:
|
||||
"""處理資料並返回結果"""
|
||||
logger.info(f"Processing: {input_path}")
|
||||
|
||||
# TODO: 實作業務邏輯
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"input": input_path,
|
||||
"count": count,
|
||||
}
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 與 Rust 整合
|
||||
|
||||
### 使用 venv (目前採用)
|
||||
|
||||
Momentry 使用 venv 管理 Python 環境,避免與系統其他程式衝突。
|
||||
|
||||
#### 建立 venv
|
||||
|
||||
```bash
|
||||
# 建立虛擬環境
|
||||
cd /Users/accusys/momentry_core_0.1
|
||||
/opt/homebrew/bin/python3.11 -m venv venv
|
||||
|
||||
# 啟用虛擬環境
|
||||
source venv/bin/activate
|
||||
|
||||
# 安裝依賴
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
#### 從 Rust 呼叫 venv Python
|
||||
|
||||
```rust
|
||||
use std::path::Path;
|
||||
|
||||
let script_path = Path::new(env!("CARGO_MANIFEST_DIR"))
|
||||
.join("scripts")
|
||||
.join("asr_processor.py");
|
||||
|
||||
// 使用 venv 中的 Python
|
||||
let venv_python = Path::new(env!("CARGO_MANIFEST_DIR"))
|
||||
.join("venv")
|
||||
.join("bin")
|
||||
.join("python");
|
||||
|
||||
let output = Command::new(venv_python)
|
||||
.arg(script_path)
|
||||
.arg(video_path)
|
||||
.output()
|
||||
.context("Failed to run processor")?;
|
||||
```
|
||||
|
||||
**優點**:
|
||||
- 專案依賴隔離
|
||||
- 不同專案可使用不同 Python 版本
|
||||
- 易於重現環境
|
||||
- 不影響系統其他程式
|
||||
|
||||
---
|
||||
|
||||
## 依賴管理
|
||||
|
||||
### venv 目錄結構
|
||||
|
||||
```
|
||||
momentry_core_0.1/
|
||||
├── venv/ # 虛擬環境
|
||||
│ ├── bin/
|
||||
│ │ ├── python # Python 3.11.14
|
||||
│ │ ├── pip
|
||||
│ │ └── ...
|
||||
│ └── lib/python3.11/ # 安裝的套件
|
||||
├── requirements.txt # 依賴列表
|
||||
├── scripts/ # Python 腳本
|
||||
│ ├── asr_processor.py
|
||||
│ └── thumbnail_extractor.py
|
||||
└── src/ # Rust 程式碼
|
||||
```
|
||||
|
||||
### 使用虛擬環境
|
||||
|
||||
```bash
|
||||
# 啟用虛擬環境
|
||||
source venv/bin/activate
|
||||
|
||||
# 安裝依賴
|
||||
pip install faster-whisper
|
||||
|
||||
# 退出虛擬環境
|
||||
deactivate
|
||||
```
|
||||
|
||||
# 退出虛擬環境
|
||||
deactivate
|
||||
```
|
||||
|
||||
### 依賴列表格式
|
||||
|
||||
建立 `requirements.txt`:
|
||||
|
||||
```
|
||||
faster-whisper>=1.0.0
|
||||
ffmpeg-python>=0.2.0
|
||||
Pillow>=10.0.0
|
||||
```
|
||||
|
||||
### 安裝專案依賴
|
||||
|
||||
```bash
|
||||
# 使用 python3.11 安裝
|
||||
/opt/homebrew/bin/python3.11 -m pip install -r requirements.txt
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## RedisPublisher 進度發布
|
||||
|
||||
### 概述
|
||||
|
||||
`redis_publisher.py` 提供統一的進度發布介面,用於 Python 處理器向 Rust 端的 TUI 即時回報進度。
|
||||
|
||||
### 基本用法
|
||||
|
||||
```python
|
||||
#!/opt/homebrew/bin/python3.11
|
||||
import sys
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
from redis_publisher import RedisPublisher
|
||||
|
||||
def process_video(video_path: str, uuid: str):
|
||||
pub = RedisPublisher(uuid)
|
||||
|
||||
pub.info("asr", "Starting ASR processing")
|
||||
pub.progress("asr", current=50, total=100, message="Processing segment")
|
||||
pub.complete("asr", "Transcription complete")
|
||||
```
|
||||
|
||||
### API 參考
|
||||
|
||||
| 方法 | 說明 | 範例 |
|
||||
|------|------|------|
|
||||
| `info(proc, msg)` | 發布資訊訊息 | `pub.info("asr", "Model loaded")` |
|
||||
| `progress(proc, cur, tot, msg)` | 發布進度 | `pub.progress("asr", 50, 100, "...")` |
|
||||
| `complete(proc, msg)` | 發布完成 | `pub.complete("asr", "Done")` |
|
||||
| `error(proc, msg)` | 發布錯誤 | `pub.error("asr", "Failed")` |
|
||||
| `warning(proc, msg)` | 發布警告 | `pub.warning("asr", "Retry...")` |
|
||||
| `percentage(proc, pct, msg)` | 發布百分比 | `pub.percentage("asr", 50.5, "50%")` |
|
||||
|
||||
### 結構化訊息格式
|
||||
|
||||
```python
|
||||
from redis_publisher import MessageType, ProgressContext
|
||||
|
||||
# 使用 Context Manager
|
||||
with ProgressContext(pub, "asr"):
|
||||
# 自動發布開始/完成/錯誤
|
||||
run_asr()
|
||||
|
||||
# 帶 extra 資料
|
||||
pub.progress("asr", current=50, total=100, message="...",
|
||||
extra={"fps": 30.5, "model": "tiny"})
|
||||
```
|
||||
|
||||
### 環境變數
|
||||
|
||||
| 變數 | 預設值 | 說明 |
|
||||
|------|--------|------|
|
||||
| `REDIS_URL` | `redis://:accusys@localhost:6379` | Redis 連線 URL |
|
||||
| `REDIS_PASSWORD` | `accusys` | Redis 密碼 |
|
||||
|
||||
---
|
||||
|
||||
## 程式碼規範
|
||||
|
||||
### Import 排序
|
||||
|
||||
```python
|
||||
# 1. 標準庫
|
||||
import sys
|
||||
import os
|
||||
import json
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
# 2. 第三方庫
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
from faster_whisper import WhisperModel
|
||||
|
||||
# 3. 本地模組
|
||||
from . import local_module
|
||||
from ..package import module
|
||||
```
|
||||
|
||||
### 命名規範
|
||||
|
||||
| 類型 | 規範 | 範例 |
|
||||
|------|------|------|
|
||||
| 模組/檔案 | snake_case | `asr_processor.py` |
|
||||
| 類別 | PascalCase | `class VideoProcessor` |
|
||||
| 函數/變數 | snake_case | `def process_video()` |
|
||||
| 常量 | UPPER_SNAKE_CASE | `MAX_WORKERS = 4` |
|
||||
| 私有成員 | _leading_underscore | `_private_method()` |
|
||||
|
||||
### 類型提示
|
||||
|
||||
```python
|
||||
from typing import Optional, List, Dict
|
||||
|
||||
def process_video(
|
||||
video_path: str,
|
||||
options: Optional[Dict[str, int]] = None,
|
||||
) -> List[Dict[str, float]]:
|
||||
"""處理影片並返回結果"""
|
||||
...
|
||||
```
|
||||
|
||||
### 錯誤處理
|
||||
|
||||
```python
|
||||
import logging
|
||||
from pathlib import Path
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def process_video(video_path: str) -> dict:
|
||||
path = Path(video_path)
|
||||
|
||||
if not path.exists():
|
||||
logger.error(f"Video file not found: {video_path}")
|
||||
raise FileNotFoundError(f"Video not found: {video_path}")
|
||||
|
||||
try:
|
||||
result = _do_process(path)
|
||||
logger.info(f"Processed successfully: {path}")
|
||||
return result
|
||||
except Exception as e:
|
||||
logger.exception(f"Processing failed: {e}")
|
||||
raise
|
||||
```
|
||||
|
||||
### 日誌規範
|
||||
|
||||
```python
|
||||
import logging
|
||||
import sys
|
||||
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format="%(asctime)s - %(levelname)s - %(message)s",
|
||||
stream=sys.stderr,
|
||||
)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# 使用說明
|
||||
logger.info("Starting process...")
|
||||
logger.debug(f"Input: {input_path}")
|
||||
logger.warning(f"Using fallback: {reason}")
|
||||
logger.error(f"Failed: {error}")
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 測試規範
|
||||
|
||||
### 測試結構
|
||||
|
||||
```
|
||||
tests/
|
||||
├── __init__.py
|
||||
├── test_asr_processor.py
|
||||
└── test_thumbnail_extractor.py
|
||||
```
|
||||
|
||||
### 測試範例
|
||||
|
||||
```python
|
||||
import pytest
|
||||
from pathlib import Path
|
||||
import sys
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent / "scripts"))
|
||||
|
||||
from asr_processor import run_asr
|
||||
|
||||
|
||||
def test_asr_processor_with_valid_video(tmp_path):
|
||||
video_path = tmp_path / "test.mp4"
|
||||
output_path = tmp_path / "output.json"
|
||||
|
||||
# 建立測試影片
|
||||
video_path.write_text("dummy")
|
||||
|
||||
# 執行
|
||||
result = run_asr(str(video_path), str(output_path))
|
||||
|
||||
# 斷言
|
||||
assert output_path.exists()
|
||||
assert result["segments"]
|
||||
|
||||
|
||||
def test_asr_processor_with_invalid_video():
|
||||
with pytest.raises(FileNotFoundError):
|
||||
run_asr("/nonexistent/video.mp4", "/tmp/output.json")
|
||||
```
|
||||
|
||||
### 執行測試
|
||||
|
||||
```bash
|
||||
# 使用 python3.11 執行測試
|
||||
/opt/homebrew/bin/python3.11 -m pytest tests/ -v
|
||||
|
||||
# 包含覆蓋率
|
||||
/opt/homebrew/bin/python3.11 -m pytest tests/ --cov=scripts
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 監控配置
|
||||
|
||||
### 監控腳本
|
||||
|
||||
在 `monitor/config/monitor_config.yaml` 中配置:
|
||||
|
||||
```yaml
|
||||
service:
|
||||
- name: "python"
|
||||
type: "process"
|
||||
process_name: "python3"
|
||||
enabled: true
|
||||
check_interval: 60
|
||||
version_lock: "3.11.14"
|
||||
scripts:
|
||||
- "/Users/accusys/momentry_core_0.1/scripts/asr_processor.py"
|
||||
- "/Users/accusys/momentry_core_0.1/scripts/thumbnail_extractor.py"
|
||||
```
|
||||
|
||||
### 檢查版本
|
||||
|
||||
```bash
|
||||
# 執行 Python 監控
|
||||
bash /Users/accusys/momentry_core_0.1/monitor/control/monitor_control.sh check python
|
||||
|
||||
# 查看資料庫記錄
|
||||
psql -U accusys -h localhost -d momentry -c "SELECT * FROM python_version_baseline;"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## CI/CD 考量
|
||||
|
||||
### GitHub Actions 範例
|
||||
|
||||
```yaml
|
||||
name: Python Tests
|
||||
|
||||
on: [push, pull_request]
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: macos-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Python 3.11
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.11"
|
||||
|
||||
- name: Run tests
|
||||
run: |
|
||||
python -m pytest tests/ -v
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 常見問題
|
||||
|
||||
### Q: 為什麼腳本要用 `#!/opt/homebrew/bin/python3.11` 而不是 `#!/usr/bin/env python3`?
|
||||
|
||||
A: `#!/usr/bin/env python3` 會解析 PATH 中的第一個 `python3`,在 macOS 上可能是:
|
||||
- `/opt/homebrew/bin/python3` (3.14.3)
|
||||
- `/usr/bin/python3` (3.9.6)
|
||||
|
||||
明確指定路徑可確保使用正確版本。
|
||||
|
||||
### Q: Rust 呼叫 Python 腳本時如何確保版本正確?
|
||||
|
||||
A: 有三種方式:
|
||||
1. Rust 程式碼中使用明確路徑:`Command::new("/opt/homebrew/bin/python3.11")`
|
||||
2. 設定環境變數 PATH
|
||||
3. 建立系統別名(不推薦,影響其他程式)
|
||||
|
||||
### Q: 如何管理多個 Python 版本?
|
||||
|
||||
A: 建議使用:
|
||||
- **pyenv**:管理多個 Python 版本
|
||||
- **venv**:隔離專案依賴
|
||||
- **Docker**:容器化環境
|
||||
|
||||
---
|
||||
|
||||
## 檢查清單
|
||||
|
||||
新增 Python 腳本時確認:
|
||||
|
||||
- [ ] 使用 `#!/opt/homebrew/bin/python3.11` shebang
|
||||
- [ ] 包含 docstring 說明功能
|
||||
- [ ] 使用 argparse 處理命令行參數
|
||||
- [ ] 使用 logging 進行日誌輸出
|
||||
- [ ] 錯誤處理適當
|
||||
- [ ] 類型提示完整
|
||||
- [ ] 更新監控配置
|
||||
- [ ] 建立測試案例
|
||||
|
||||
---
|
||||
|
||||
## 版本速查
|
||||
|
||||
| 版本 | 用途 | 路徑 |
|
||||
|------|------|------|
|
||||
| 3.11.14 | Momentry venv | /Users/accusys/momentry_core_0.1/venv/bin/python |
|
||||
| 3.11.14 | 系統安裝 | /opt/homebrew/bin/python3.11 |
|
||||
| 3.14.3 | 系統預設 | /opt/homebrew/bin/python3 |
|
||||
|
||||
---
|
||||
|
||||
## 相關文件
|
||||
|
||||
- [NODEJS.md](./NODEJS.md) - Node.js 開發指南
|
||||
- [RUST_DEVELOPMENT.md](./RUST_DEVELOPMENT.md) - Rust 開發規範
|
||||
- [monitor_config.yaml](../monitor/config/monitor_config.yaml) - 監控配置
|
||||
- [python_monitor.sh](../monitor/service/python_monitor.sh) - Python 監控腳本
|
||||
@@ -1,990 +0,0 @@
|
||||
# Rust 開發規範 - Momentry Core
|
||||
|
||||
| 項目 | 內容 |
|
||||
|------|------|
|
||||
| 建立者 | Warren |
|
||||
| 建立時間 | 2026-03-16 |
|
||||
| 文件版本 | V1.0 |
|
||||
|
||||
---
|
||||
|
||||
## 版本歷史
|
||||
|
||||
| 版本 | 日期 | 目的 | 操作人 | 工具/模型 |
|
||||
|------|------|------|--------|-----------|
|
||||
| V1.0 | 2026-03-16 | 創建文件 | Warren | OpenCode / MiniMax M2.5 |
|
||||
| V1.1 | 2026-03-21 | 新增 PythonExecutor 模組說明 | OpenCode | - |
|
||||
|
||||
---
|
||||
|
||||
本規範定義 Momentry Core 專案的 Rust 開發標準,確保程式碼品質與一致性。
|
||||
|
||||
## 1. 專案結構
|
||||
|
||||
### 1.1 目錄架構
|
||||
|
||||
```
|
||||
src/
|
||||
├── main.rs # CLI 入口點
|
||||
├── lib.rs # 函式庫導出
|
||||
├── cli/
|
||||
│ ├── mod.rs
|
||||
│ └── commands/ # CLI 命令模組
|
||||
├── core/
|
||||
│ ├── mod.rs
|
||||
│ ├── chunk/ # 影片分段邏輯
|
||||
│ │ ├── mod.rs
|
||||
│ │ ├── splitter.rs
|
||||
│ │ └── types.rs
|
||||
│ ├── db/ # 資料庫抽象層
|
||||
│ │ ├── mod.rs
|
||||
│ │ ├── postgres_db.rs
|
||||
│ │ ├── mongodb_db.rs
|
||||
│ │ ├── redis_db.rs
|
||||
│ │ └── qdrant_db.rs
|
||||
│ ├── processor/ # 影片處理器
|
||||
│ │ ├── mod.rs
|
||||
│ │ ├── executor.rs # Python 腳本統一執行器 (含超時控制)
|
||||
│ │ ├── asr.rs # 語音識別
|
||||
│ │ ├── asrx.rs # 說話者分離
|
||||
│ │ ├── ocr.rs # 文字辨識
|
||||
│ │ ├── yolo.rs # 物件偵測
|
||||
│ │ ├── face.rs # 人臉偵測
|
||||
│ │ └── pose.rs # 姿態估計
|
||||
│ ├── embedding/ # 向量嵌入
|
||||
│ ├── probe/ # ffprobe 整合
|
||||
│ ├── storage/ # 檔案管理
|
||||
│ └── thumbnail/ # 縮圖生成
|
||||
├── api/ # HTTP API
|
||||
│ ├── mod.rs
|
||||
│ └── routes/
|
||||
├── player/ # 影片播放
|
||||
└── watcher/ # 檔案監控
|
||||
```
|
||||
|
||||
### 1.2 模組設計原則
|
||||
|
||||
- **單一職責**: 每個模組專注於一項功能
|
||||
- **介面抽象**: 使用 trait 定義資料庫、操作器等介面
|
||||
- **依賴注入**: 透過建構函式注入依賴
|
||||
|
||||
```rust
|
||||
pub trait VideoProcessor: Send + Sync {
|
||||
async fn process(&self, video_path: &str) -> Result<ProcessResult>;
|
||||
}
|
||||
```
|
||||
|
||||
## 2. 程式碼風格
|
||||
|
||||
### 2.1 命名規範
|
||||
|
||||
| 類型 | 規範 | 範例 |
|
||||
|------|------|------|
|
||||
| 結構體/列舉 | PascalCase | `VideoRecord`, `ChunkType` |
|
||||
| 函式/變數 | snake_case | `get_video_by_uuid` |
|
||||
| Trait | PascalCase + er 尾碼 | `Database`, `ChunkStore` |
|
||||
| 檔案 | snake_case | `postgres_db.rs` |
|
||||
| 常量 | SCREAMING_SNAKE_CASE | `MAX_CHUNK_SIZE` |
|
||||
| 模組 | snake_case | `chunk`, `processor` |
|
||||
|
||||
### 2.2 匯入順序
|
||||
|
||||
```rust
|
||||
// 1. 標準庫
|
||||
use std::path::Path;
|
||||
use std::process::Command;
|
||||
|
||||
// 2. 外部庫
|
||||
use anyhow::{Context, Result};
|
||||
use async_trait::async_trait;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tokio::fs;
|
||||
|
||||
// 3. 內部模組
|
||||
use crate::core::chunk::Chunk;
|
||||
use crate::core::db::PostgresDb;
|
||||
```
|
||||
|
||||
### 2.3 行寬與格式
|
||||
|
||||
- 最大行寬: 100 字元
|
||||
- 使用 4 空格縮排
|
||||
- 啟用 clippy 與 fmt
|
||||
|
||||
```bash
|
||||
# 格式化
|
||||
cargo fmt
|
||||
|
||||
# 檢查格式
|
||||
cargo fmt -- --check
|
||||
|
||||
# Lint
|
||||
cargo clippy --all-features
|
||||
```
|
||||
|
||||
## 3. 錯誤處理
|
||||
|
||||
### 3.1 錯誤類型選擇
|
||||
|
||||
| 情境 | 錯誤類型 | 原因 |
|
||||
|------|----------|------|
|
||||
| 應用程式 | `anyhow::Result<T>` | 提供靈活的錯誤傳播 |
|
||||
| 函式庫 | `thiserror` | 定義明確的錯誤類型 |
|
||||
| API 錯誤 | 自定義 Error enum | 提供客戶端錯誤碼 |
|
||||
|
||||
### 3.2 錯誤處理範例
|
||||
|
||||
```rust
|
||||
use anyhow::{Context, Result, bail};
|
||||
|
||||
fn process_video(video_path: &str) -> Result<VideoMetadata> {
|
||||
// 使用 context 提供錯誤上下文
|
||||
let output = Command::new("ffprobe")
|
||||
.args(["-v", "quiet", "-print_format", "json", "-show_format", video_path])
|
||||
.output()
|
||||
.context("Failed to run ffprobe")?;
|
||||
|
||||
// 使用 bail 進行早期返回
|
||||
if !output.status.success() {
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
bail!("ffprobe failed: {}", stderr);
|
||||
}
|
||||
|
||||
// 解析輸出
|
||||
let metadata: Metadata = serde_json::from_slice(&output.stdout)
|
||||
.context("Failed to parse ffprobe output")?;
|
||||
|
||||
Ok(metadata)
|
||||
}
|
||||
```
|
||||
|
||||
### 3.3 自定義錯誤 (適用於函式庫)
|
||||
|
||||
```rust
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum VideoError {
|
||||
#[error("Video not found: {0}")]
|
||||
NotFound(String),
|
||||
|
||||
#[error("Invalid codec: {0}")]
|
||||
InvalidCodec(String),
|
||||
|
||||
#[error("Processing failed: {0}")]
|
||||
ProcessingError(#[from] std::io::Error),
|
||||
}
|
||||
```
|
||||
|
||||
## 4. 异步編程
|
||||
|
||||
### 4.1 Tokio 配置
|
||||
|
||||
```rust
|
||||
// Cargo.toml
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
```
|
||||
|
||||
### 4.2 Async Trait
|
||||
|
||||
```rust
|
||||
use async_trait::async_trait;
|
||||
|
||||
#[async_trait]
|
||||
pub trait Database: Send + Sync {
|
||||
async fn init() -> Result<Self>
|
||||
where Self: Sized;
|
||||
|
||||
async fn get_video(&self, uuid: &str) -> Result<Option<VideoRecord>>;
|
||||
|
||||
async fn store_chunk(&self, chunk: &Chunk) -> Result<()>;
|
||||
}
|
||||
```
|
||||
|
||||
### 4.3 避免常見陷阱
|
||||
|
||||
```rust
|
||||
// ❌ 錯誤: 在同步上下文中調用 async 函式
|
||||
fn bad_example() {
|
||||
let result = db.get_video("xxx"); // 編譯錯誤
|
||||
}
|
||||
|
||||
// ✅ 正確: 使用 #[tokio::main]
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
let result = db.get_video("xxx").await;
|
||||
}
|
||||
|
||||
// ❌ 錯誤: 阻塞執行緒池
|
||||
async fn bad_practice() {
|
||||
let data = std::fs::read_to_string("file.txt").unwrap(); // 阻塞
|
||||
}
|
||||
|
||||
// ✅ 正確: 使用 tokio::fs
|
||||
async fn good_practice() {
|
||||
let data = tokio::fs::read_to_string("file.txt").await.unwrap();
|
||||
}
|
||||
```
|
||||
|
||||
## 5. 外部程序整合
|
||||
|
||||
當需要使用 Python 生態系工具 (如 faster-whisper, YOLO) 時:
|
||||
|
||||
```rust
|
||||
pub async fn process_asr(video_path: &str, output_path: &str) -> Result<AsrResult> {
|
||||
let script_path = Path::new(env!("CARGO_MANIFEST_DIR"))
|
||||
.join("scripts")
|
||||
.join("asr_processor.py");
|
||||
|
||||
// 使用 venv 中的 Python,確保版本隔離
|
||||
let venv_python = Path::new(env!("CARGO_MANIFEST_DIR"))
|
||||
.join("venv")
|
||||
.join("bin")
|
||||
.join("python");
|
||||
|
||||
// 執行腳本
|
||||
let output = Command::new(venv_python)
|
||||
.arg(script_path)
|
||||
.arg(video_path)
|
||||
.arg(output_path)
|
||||
.output()
|
||||
.context("Failed to run ASR processor")?;
|
||||
|
||||
if !output.status.success() {
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
bail!("ASR failed: {}", stderr);
|
||||
}
|
||||
|
||||
// 讀取輸出
|
||||
let json_str = std::fs::read_to_string(output_path)
|
||||
.context("Failed to read ASR output")?;
|
||||
|
||||
let result: AsrResult = serde_json::from_str(&json_str)
|
||||
.context("Failed to parse ASR output")?;
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
```
|
||||
|
||||
### 5.2 進度回報
|
||||
|
||||
透過 stderr 回報進度,供 Rust 端解析:
|
||||
|
||||
```python
|
||||
# Python 腳本
|
||||
import sys
|
||||
|
||||
print(f"ASR_START", file=sys.stderr)
|
||||
print(f"ASR_LANGUAGE:{detected_lang}", file=sys.stderr)
|
||||
print(f"ASR_PROGRESS:{count}", file=sys.stderr)
|
||||
print(f"ASR_COMPLETE:{total}", file=sys.stderr)
|
||||
```
|
||||
|
||||
```rust
|
||||
// Rust 端解析
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
for line in stderr.lines() {
|
||||
if line.starts_with("ASR_PROGRESS:") {
|
||||
let count = line.trim_start_matches("ASR_PROGRESS:");
|
||||
println!("[ASR] Processed {} segments...", count);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 5.3 PythonExecutor 統一執行器
|
||||
|
||||
使用 `PythonExecutor` 封裝 Python 腳本執行邏輯:
|
||||
|
||||
```rust
|
||||
use momentry_core::core::processor::{PythonExecutor, validate_python_env};
|
||||
|
||||
// 驗證 Python 環境
|
||||
fn init() -> Result<()> {
|
||||
validate_python_env()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// 使用 Executor 執行腳本
|
||||
async fn run_script() -> Result<()> {
|
||||
let executor = PythonExecutor::new()?;
|
||||
|
||||
executor.run(
|
||||
"asr_processor.py",
|
||||
&["/path/to/video.mp4", "/path/to/output.json"],
|
||||
Some("job-uuid"),
|
||||
"ASR",
|
||||
Some(Duration::from_secs(3600)), // 1小時超時
|
||||
).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
#### Processor 超時設定
|
||||
|
||||
| Processor | 超時 | 說明 |
|
||||
|----------|------|------|
|
||||
| ASR | 1 小時 | 語音識別 |
|
||||
| ASRx | 2 小時 | 說話者分離 |
|
||||
| YOLO | 2 小時 | 物件偵測 |
|
||||
| OCR | 2 小時 | 文字辨識 |
|
||||
| Face | 2 小時 | 人臉偵測 |
|
||||
| Pose | 2 小時 | 姿態估計 |
|
||||
| Cut | 1 小時 | 場景偵測 |
|
||||
|
||||
---
|
||||
|
||||
## 6. Python 與 Node.js 混用規範
|
||||
|
||||
本專案同時使用 Python 和 Node.js (n8n),需建立明確的版本隔離與管理規範。
|
||||
|
||||
### 6.1 架構概述
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ Momentry Core │
|
||||
│ │
|
||||
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
|
||||
│ │ Rust │ │ Python │ │ Node.js │ │
|
||||
│ │ (Core) │───▶ │ (Scripts) │ │ (n8n) │ │
|
||||
│ │ │ │ │ │ │ │
|
||||
│ │ - CLI │ │ - ASR │ │ - Workflow │ │
|
||||
│ │ - DB │ │ - Thumb │ │ - API │ │
|
||||
│ │ - Storage │ │ - OCR │ │ - Webhooks │ │
|
||||
│ └─────────────┘ └─────────────┘ └─────────────┘ │
|
||||
│ │ │ │ │
|
||||
│ ▼ ▼ ▼ │
|
||||
│ ┌─────────────────────────────────────────────────────┐ │
|
||||
│ │ 資料庫 / 檔案系統 / Qdrant │ │
|
||||
│ └─────────────────────────────────────────────────────┘ │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 6.2 Python 版本管理
|
||||
|
||||
#### 6.2.1 版本鎖定
|
||||
|
||||
| 版本 | 用途 | 路徑 |
|
||||
|------|------|------|
|
||||
| 3.11.14 | 影片處理腳本 | `/opt/homebrew/bin/python3.11` |
|
||||
|
||||
#### 6.2.2 虛擬環境
|
||||
|
||||
使用專案隔離的 venv:
|
||||
|
||||
```bash
|
||||
# 建立虛擬環境
|
||||
cd /Users/accusys/momentry_core_0.1
|
||||
python3.11 -m venv venv
|
||||
|
||||
# 啟用
|
||||
source venv/bin/activate
|
||||
|
||||
# 安裝依賴
|
||||
pip install faster-whisper opencv-python python-dotenv
|
||||
```
|
||||
|
||||
#### 6.2.3 Rust 呼叫 Python
|
||||
|
||||
```rust
|
||||
let venv_python = Path::new(env!("CARGO_MANIFEST_DIR"))
|
||||
.join("venv")
|
||||
.join("bin")
|
||||
.join("python");
|
||||
|
||||
let output = Command::new(venv_python)
|
||||
.arg(script_path)
|
||||
.arg(video_path)
|
||||
.output()
|
||||
.context("Failed to run Python script")?;
|
||||
```
|
||||
|
||||
### 6.3 Node.js 版本管理
|
||||
|
||||
#### 6.3.1 版本鎖定
|
||||
|
||||
參考 `docs/NODEJS.md`:
|
||||
|
||||
| 版本 | 用途 | 路徑 |
|
||||
|------|------|------|
|
||||
| 22.22.1 | n8n | `/opt/homebrew/opt/node@22/bin/node` |
|
||||
|
||||
#### 6.3.2 n8n 服務配置
|
||||
|
||||
使用 launchd plist 隔離:
|
||||
|
||||
```xml
|
||||
<!-- com.momentry.n8n.main.plist -->
|
||||
<key>ProgramArguments</key>
|
||||
<array>
|
||||
<string>/opt/homebrew/opt/node@22/bin/node</string>
|
||||
<string>/opt/homebrew/lib/node_modules/n8n/bin/n8n</string>
|
||||
<string>start</string>
|
||||
</array>
|
||||
|
||||
<key>EnvironmentVariables</key>
|
||||
<dict>
|
||||
<key>PATH</key>
|
||||
<string>/opt/homebrew/opt/node@22/bin:/opt/homebrew/bin:...</string>
|
||||
</dict>
|
||||
```
|
||||
|
||||
### 6.4 Python + Node.js 共存原則
|
||||
|
||||
#### 6.4.1 隔離原則
|
||||
|
||||
| 原則 | 說明 |
|
||||
|------|------|
|
||||
| **獨立路徑** | Python 用 venv 路徑,Node.js 用 node@22 路徑 |
|
||||
| **獨立環境** | n8n 服務使用 launchd plist,不與 Rust 共享環境 |
|
||||
| **明確版本** | 所有腳本明確指定直譯器路徑 |
|
||||
| **PORT 分配** | n8n: 5678/5679, API: 另行分配 |
|
||||
|
||||
#### 6.4.2 環境變數隔離
|
||||
|
||||
```bash
|
||||
# Rust 專案 .env
|
||||
DATABASE_URL=postgres://...
|
||||
|
||||
# n8n plist
|
||||
N8N_ENCRYPTION_KEY=xxx
|
||||
N8N_BASIC_AUTH_ACTIVE=true
|
||||
|
||||
# 勿混用,避免 Rust 讀到 n8n 環境變數
|
||||
```
|
||||
|
||||
### 6.5 工作流程整合
|
||||
|
||||
#### 6.5.1 Rust → Python
|
||||
|
||||
```
|
||||
Rust CLI ──▶ Python Script ──▶ JSON Output ──▶ Rust Parse
|
||||
│ │
|
||||
└── venv/bin/python └── faster-whisper
|
||||
```
|
||||
|
||||
#### 6.5.2 Rust → n8n Webhook
|
||||
|
||||
```rust
|
||||
// 觸發 n8n workflow
|
||||
use reqwest;
|
||||
|
||||
pub async fn trigger_n8n_webhook(webhook_url: &str, payload: &str) -> Result<()> {
|
||||
let client = reqwest::Client::new();
|
||||
client.post(webhook_url)
|
||||
.json(payload)
|
||||
.send()
|
||||
.await
|
||||
.context("Failed to trigger n8n webhook")?;
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
#### 6.5.3 n8n → Rust API
|
||||
|
||||
```
|
||||
n8n Workflow ──▶ HTTP Request Node ──▶ Rust API Server
|
||||
│
|
||||
┌───────┴───────┐
|
||||
│ axum server │
|
||||
│ /api/webhook │
|
||||
└───────────────┘
|
||||
```
|
||||
|
||||
### 6.6 監控配置
|
||||
|
||||
#### 6.6.1 獨立監控腳本
|
||||
|
||||
```bash
|
||||
# monitor/service/node_monitor.sh
|
||||
# 監控 n8n Node.js 版本
|
||||
|
||||
# monitor/service/python_monitor.sh
|
||||
# 監控 Python 腳本執行狀態
|
||||
```
|
||||
|
||||
#### 6.6.2 健康檢查
|
||||
|
||||
```yaml
|
||||
# monitor_config.yaml
|
||||
services:
|
||||
- name: "n8n"
|
||||
type: "http"
|
||||
port: 5678
|
||||
check_url: "http://localhost:5678/"
|
||||
|
||||
- name: "Python Scripts"
|
||||
type: "process"
|
||||
check: "pgrep -f asr_processor.py"
|
||||
```
|
||||
|
||||
### 6.7 排程管理
|
||||
|
||||
#### 6.7.1 備份排程 (Python 腳本)
|
||||
|
||||
```bash
|
||||
# crontab
|
||||
0 3 * * * /Users/accusys/momentry/scripts/backup_all.sh
|
||||
```
|
||||
|
||||
#### 6.7.2 n8n 工作流排程
|
||||
|
||||
- 由 n8n 內建排程節點管理
|
||||
- 不與 crontab 衝突
|
||||
|
||||
### 6.8 故障排除
|
||||
|
||||
#### 6.8.1 常見問題
|
||||
|
||||
| 問題 | 原因 | 解決方案 |
|
||||
|------|------|----------|
|
||||
| n8n 版本警告 | 使用 Node 25.x | 確認 plist 使用 node@22 |
|
||||
| Python 腳本找不到模組 | 未啟用 venv | 使用 venv/bin/python |
|
||||
| 執行權限錯誤 | shebang 錯誤 | 確認 #!/opt/homebrew/bin/python3.11 |
|
||||
| Port 被佔用 | 多個服務使用相同 port | 分配獨立 port |
|
||||
|
||||
#### 6.8.2 診斷命令
|
||||
|
||||
```bash
|
||||
# 檢查 Python 版本
|
||||
which python
|
||||
/opt/homebrew/bin/python3.11 --version
|
||||
|
||||
# 檢查 Node.js 版本
|
||||
/opt/homebrew/opt/node@22/bin/node --version
|
||||
|
||||
# 檢查 n8n 程序
|
||||
ps aux | grep n8n
|
||||
|
||||
# 檢查 Python 程序
|
||||
ps aux | grep python
|
||||
|
||||
# 檢查 Port 佔用
|
||||
lsof -i :5678 # n8n
|
||||
```
|
||||
|
||||
### 6.9 新增服務決策
|
||||
|
||||
```
|
||||
新服務需要哪種執行環境?
|
||||
│
|
||||
├─ Python 腳本 ──▶ 使用專案 venv
|
||||
│ (路徑: venv/bin/python)
|
||||
│
|
||||
├─ Node.js 工具 ──▶ 評估版本需求
|
||||
│ │
|
||||
│ ├─ 支援 Node 22 ──▶ 使用 node@22
|
||||
│ │
|
||||
│ └─ 需要其他版本 ──▶ 安裝新版本 (如 node@20)
|
||||
│
|
||||
└─ 現有服務依賴 ──▶ 根據現有服務配置
|
||||
```
|
||||
|
||||
### 6.10 文件維護
|
||||
|
||||
當新增 Python 或 Node.js 服務時:
|
||||
|
||||
1. 更新本文檔的版本表格
|
||||
2. 建立對應的監控腳本
|
||||
3. 如需 launchd plist,建立並加入 `momentry_runtime/plist/`
|
||||
4. 更新 `docs/NODEJS.md` 或 `docs/PYTHON.md`
|
||||
|
||||
### 5.2 進度回報
|
||||
|
||||
透過 stderr 回報進度,供 Rust 端解析:
|
||||
|
||||
```python
|
||||
# Python 腳本
|
||||
import sys
|
||||
|
||||
print(f"ASR_START", file=sys.stderr)
|
||||
print(f"ASR_LANGUAGE:{detected_lang}", file=sys.stderr)
|
||||
print(f"ASR_PROGRESS:{count}", file=sys.stderr)
|
||||
print(f"ASR_COMPLETE:{total}", file=sys.stderr)
|
||||
```
|
||||
|
||||
```rust
|
||||
// Rust 端解析
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
for line in stderr.lines() {
|
||||
if line.starts_with("ASR_PROGRESS:") {
|
||||
let count = line.trim_start_matches("ASR_PROGRESS:");
|
||||
println!("[ASR] Processed {} segments...", count);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 7. 測試策略
|
||||
|
||||
### 6.1 單元測試
|
||||
|
||||
```rust
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_chunk_creation() {
|
||||
let chunk = Chunk::new(
|
||||
"test-uuid".to_string(),
|
||||
0,
|
||||
ChunkType::Sentence,
|
||||
0.0,
|
||||
10.0,
|
||||
serde_json::json!({"text": "Hello"}),
|
||||
);
|
||||
|
||||
assert_eq!(chunk.uuid, "test-uuid");
|
||||
assert_eq!(chunk.chunk_type, ChunkType::Sentence);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 6.2 整合測試
|
||||
|
||||
```rust
|
||||
#[cfg(test)]
|
||||
mod integration {
|
||||
use super::*;
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_database_connection() {
|
||||
let db = PostgresDb::init().await.unwrap();
|
||||
let videos = db.list_videos().await.unwrap();
|
||||
assert!(videos.len() >= 0);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 6.3 測試資料
|
||||
|
||||
- 使用測試資料庫隔離測試環境
|
||||
- 避免在測試中使用真實敏感資料
|
||||
- 使用 mock 物件模擬外部依賴
|
||||
|
||||
```rust
|
||||
#[cfg(test)]
|
||||
mod mocks {
|
||||
pub struct MockVideoProcessor {
|
||||
pub result: AsrResult,
|
||||
}
|
||||
|
||||
impl VideoProcessor for MockVideoProcessor {
|
||||
async fn process(&self, _video_path: &str) -> Result<AsrResult> {
|
||||
Ok(self.result.clone())
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 8. 日誌與監控
|
||||
|
||||
### 7.1 日誌規範
|
||||
|
||||
- **使用 tracing**: 不要使用 `println!`
|
||||
- **結構化日誌**: 使用訊息 + 欄位
|
||||
|
||||
```rust
|
||||
use tracing::{info, warn, error};
|
||||
|
||||
fn process_video(uuid: &str) -> Result<()> {
|
||||
info!(uuid = uuid, "Starting video processing");
|
||||
|
||||
match do_processing(uuid) {
|
||||
Ok(_) => info!(uuid = uuid, "Processing completed"),
|
||||
Err(e) => {
|
||||
error!(uuid = uuid, error = %e, "Processing failed");
|
||||
return Err(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 7.2 初始化日誌
|
||||
|
||||
```rust
|
||||
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
|
||||
|
||||
fn init_logging() {
|
||||
tracing_subscriber::registry()
|
||||
.with(
|
||||
tracing_subscriber::EnvFilter::try_from_default_env()
|
||||
.unwrap_or_else(|_| "momentry_core=info,tokio=warn".into()),
|
||||
)
|
||||
.with(tracing_subscriber::fmt::layer())
|
||||
.init();
|
||||
}
|
||||
```
|
||||
|
||||
## 9. 性能優化
|
||||
|
||||
### 8.1 避免拷貝
|
||||
|
||||
```rust
|
||||
// ❌ 拷貝
|
||||
let data = record.clone();
|
||||
|
||||
// ✅ 引用
|
||||
fn process(data: &Data) { }
|
||||
|
||||
// ✅ 或使用 Arc 共用
|
||||
use std::sync::Arc;
|
||||
let shared = Arc::new(data);
|
||||
```
|
||||
|
||||
### 8.2 批量操作
|
||||
|
||||
```rust
|
||||
// ❌ 逐筆插入
|
||||
for item in items {
|
||||
db.insert(&item).await?;
|
||||
}
|
||||
|
||||
// ✅ 批量插入
|
||||
db.insert_batch(&items).await?;
|
||||
```
|
||||
|
||||
### 8.3 連線池
|
||||
|
||||
```rust
|
||||
// 使用 sqlx 連線池
|
||||
let pool = SqlxPool::connect(&DATABASE_URL).await?;
|
||||
let db = PostgresDb::new(pool);
|
||||
```
|
||||
|
||||
## 10. 安全考量
|
||||
|
||||
### 9.1 敏感資訊
|
||||
|
||||
- **不要**將密碼、API Key 寫入程式碼
|
||||
- 使用環境變數或設定檔
|
||||
- .env 檔案加入 .gitignore
|
||||
|
||||
```rust
|
||||
// ❌ 硬編碼密碼
|
||||
let password = "secret123";
|
||||
|
||||
// ✅ 使用環境變數
|
||||
let password = std::env::var("DATABASE_PASSWORD")
|
||||
.context("DATABASE_PASSWORD must be set")?;
|
||||
```
|
||||
|
||||
### 9.2 命令注入
|
||||
|
||||
```rust
|
||||
// ❌ 危險: 直接使用使用者輸入
|
||||
let cmd = format!("ffprobe {}", user_input);
|
||||
|
||||
// ✅ 安全: 使用參數化
|
||||
Command::new("ffprobe")
|
||||
.arg(user_input) // 自動轉義
|
||||
.output();
|
||||
```
|
||||
|
||||
## 11. 文件編寫
|
||||
|
||||
### 10.1 結構體/函式文件
|
||||
|
||||
```rust
|
||||
/// 代表一個影片記錄
|
||||
///
|
||||
/// # Fields
|
||||
/// * `id` - 資料庫 ID
|
||||
/// * `uuid` - 唯一識別碼
|
||||
/// * `duration` - 影片時長 (秒)
|
||||
///
|
||||
/// # Example
|
||||
/// ```
|
||||
/// let video = VideoRecord {
|
||||
/// id: 1,
|
||||
/// uuid: "abc123".to_string(),
|
||||
/// duration: 120.5,
|
||||
/// };
|
||||
/// ```
|
||||
pub struct VideoRecord {
|
||||
pub id: i64,
|
||||
pub uuid: String,
|
||||
pub duration: f64,
|
||||
}
|
||||
```
|
||||
|
||||
### 10.2 API 文件
|
||||
|
||||
```rust
|
||||
/// 取得影片記錄
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `uuid` - 影片的 UUID
|
||||
///
|
||||
/// # Returns
|
||||
/// * `Ok(Some(VideoRecord))` - 找到影片
|
||||
/// * `Ok(None)` - 影片不存在
|
||||
/// * `Err` - 資料庫錯誤
|
||||
///
|
||||
/// # Errors
|
||||
/// 如果資料庫連線失敗,返回資料庫錯誤
|
||||
pub async fn get_video(&self, uuid: &str) -> Result<Option<VideoRecord>>;
|
||||
```
|
||||
|
||||
## 12. CLI 命令設計
|
||||
|
||||
### 11.1 命令結構
|
||||
|
||||
使用 clap derive:
|
||||
|
||||
```rust
|
||||
use clap::{Parser, Subcommand};
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(name = "momentry")]
|
||||
#[command(about = "Digital asset management system")]
|
||||
struct Cli {
|
||||
#[command(subcommand)]
|
||||
command: Commands,
|
||||
}
|
||||
|
||||
#[derive(Subcommand)]
|
||||
enum Commands {
|
||||
/// Register a video file
|
||||
Register {
|
||||
/// Path to video file
|
||||
path: String,
|
||||
},
|
||||
/// Process video
|
||||
Process {
|
||||
/// UUID or path
|
||||
target: String,
|
||||
},
|
||||
/// Generate chunks
|
||||
Chunk {
|
||||
/// Video UUID
|
||||
uuid: String,
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
### 11.2 錯誤處理
|
||||
|
||||
```rust
|
||||
match &cli.command {
|
||||
Commands::Register { path } => {
|
||||
if !Path::new(path).exists() {
|
||||
eprintln!("Error: File not found: {}", path);
|
||||
std::process::exit(1);
|
||||
}
|
||||
// ...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 13. 依賴管理
|
||||
|
||||
### 12.1 版本約束
|
||||
|
||||
```toml
|
||||
# Cargo.toml
|
||||
[dependencies]
|
||||
anyhow = "1.0" # 精確版本
|
||||
tokio = { version = "1", features = ["full"] } # 範圍版本
|
||||
serde = "1.0" # 精確版本
|
||||
```
|
||||
|
||||
### 12.2 避免依賴地獄
|
||||
|
||||
- 審查依賴數量
|
||||
- 優先使用標準庫
|
||||
- 選擇維護良好的套件
|
||||
|
||||
## 14. 建構與部署
|
||||
|
||||
### 13.1 建構命令
|
||||
|
||||
```bash
|
||||
# 開發建構
|
||||
cargo build
|
||||
|
||||
# 發布建構
|
||||
cargo build --release
|
||||
|
||||
# 單一二進制
|
||||
cargo build --bin momentry
|
||||
```
|
||||
|
||||
### 13.2 檢查清單
|
||||
|
||||
```bash
|
||||
# 格式化
|
||||
cargo fmt -- --check
|
||||
|
||||
# Lint
|
||||
cargo clippy --all-features -- -D warnings
|
||||
|
||||
# 類型檢查
|
||||
cargo check --all-features
|
||||
|
||||
# 測試
|
||||
cargo test
|
||||
```
|
||||
|
||||
## 15. 版本控制
|
||||
|
||||
### 14.1 提交訊息規範
|
||||
|
||||
```
|
||||
<type>(<scope>): <subject>
|
||||
|
||||
<body>
|
||||
|
||||
<footer>
|
||||
```
|
||||
|
||||
類型:
|
||||
- `feat`: 新功能
|
||||
- `fix`: 錯誤修復
|
||||
- `docs`: 文件變更
|
||||
- `style`: 格式調整
|
||||
- `refactor`: 重構
|
||||
- `test`: 測試變更
|
||||
- `chore`: 建構/工具變更
|
||||
|
||||
範例:
|
||||
```
|
||||
feat(processor): Add ASR progress reporting
|
||||
|
||||
- Add stderr parsing for progress updates
|
||||
- Support ASR_START, ASR_PROGRESS, ASR_COMPLETE markers
|
||||
|
||||
Closes #123
|
||||
```
|
||||
|
||||
### 14.2 分支策略
|
||||
|
||||
- `main`: 穩定版本
|
||||
- `feature/*`: 新功能開發
|
||||
- `fix/*`: 錯誤修復
|
||||
- `refactor/*`: 重構
|
||||
|
||||
---
|
||||
|
||||
## 附錄: 快速參考
|
||||
|
||||
### 建構
|
||||
```bash
|
||||
cargo build --release
|
||||
cargo run -- --help
|
||||
```
|
||||
|
||||
### 品質檢查
|
||||
```bash
|
||||
cargo fmt -- --check
|
||||
cargo clippy --all-features
|
||||
cargo check --all-features
|
||||
cargo test
|
||||
```
|
||||
|
||||
### 依賴
|
||||
```bash
|
||||
cargo add <package>
|
||||
cargo tree
|
||||
cargo outdated
|
||||
```
|
||||
@@ -1,121 +0,0 @@
|
||||
# 搜尋範例 Prompt
|
||||
|
||||
## 基本搜尋測試
|
||||
|
||||
### 1. 簡單關鍵字搜尋
|
||||
```bash
|
||||
curl -X POST http://localhost:3002/api/v1/search \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"query": "charade", "limit": 5}'
|
||||
```
|
||||
|
||||
### 2. 電影相關詞
|
||||
```
|
||||
charade
|
||||
woody allen
|
||||
audrey hepburn
|
||||
classic movie
|
||||
old time movie
|
||||
romantic comedy
|
||||
```
|
||||
|
||||
### 3. 場景描述
|
||||
```
|
||||
widowed woman
|
||||
secret agent
|
||||
chase scene
|
||||
paris
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 進階搜尋測試
|
||||
|
||||
### 4. 短語搜尋
|
||||
```bash
|
||||
curl -X POST http://localhost:3002/api/v1/search \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"query": "fun plot twists", "limit": 3}'
|
||||
```
|
||||
|
||||
### 5. 情感/描述詞
|
||||
```
|
||||
charming performances
|
||||
hilarious
|
||||
suspenseful
|
||||
dramatic
|
||||
```
|
||||
|
||||
### 6. 動作場景
|
||||
```
|
||||
running
|
||||
chase
|
||||
fighting
|
||||
dancing
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 整合範例
|
||||
|
||||
### n8n Workflow
|
||||
```
|
||||
搜尋詞: "charade"
|
||||
→ 取得 chunk 的 start_time, end_time
|
||||
→ 組裝成影片 URL
|
||||
→ 回傳給用戶
|
||||
```
|
||||
|
||||
### PHP 範例
|
||||
```php
|
||||
$searchTerms = ['charade', 'woody', 'audrey', 'classic'];
|
||||
|
||||
// 搜尋每個詞
|
||||
foreach ($searchTerms as $term) {
|
||||
$ch = curl_init('http://localhost:3002/api/v1/search');
|
||||
curl_setopt($ch, CURLOPT_POST, true);
|
||||
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode([
|
||||
'query' => $term,
|
||||
'limit' => 5
|
||||
]));
|
||||
curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']);
|
||||
$response = curl_exec($ch);
|
||||
$data = json_decode($response, true);
|
||||
|
||||
// 處理結果
|
||||
foreach ($data['results'] as $result) {
|
||||
echo "{$result['text']} (score: {$result['score']})\n";
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 預期回傳格式
|
||||
|
||||
```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 整合格式
|
||||
- [ ] 影片時戳取得
|
||||
- [ ] 多筆結果排序
|
||||
- [ ] 不同 chunk_type 搜尋
|
||||
1074
docs/SERVICES.md
1074
docs/SERVICES.md
File diff suppressed because it is too large
Load Diff
@@ -1,698 +0,0 @@
|
||||
# Momentry 服務添加規範 v2.1
|
||||
|
||||
| 項目 | 內容 |
|
||||
|------|------|
|
||||
| 建立者 | Warren |
|
||||
| 建立時間 | 2026-03-16 |
|
||||
| 更新時間 | 2026-03-24 |
|
||||
| 文件版本 | V2.1 |
|
||||
|
||||
---
|
||||
|
||||
## 版本歷史
|
||||
|
||||
| 版本 | 日期 | 目的 | 操作人 | 工具/模型 |
|
||||
|------|------|------|--------|-----------|
|
||||
| V1.0 | 2026-03-16 | 創建文件 | Warren | OpenCode / MiniMax M2.5 |
|
||||
| V2.1 | 2026-03-24 | 更新 launchctl 命令,使用 bootstrap | OpenCode | OpenCode / big-pickle |
|
||||
|
||||
---
|
||||
|
||||
## 一、概述
|
||||
|
||||
本文檔定義在 Momentry 系統中添加新服務的標準流程和規範。
|
||||
|
||||
**重要原則**:
|
||||
- 使用 `launchctl` 管理服務,勿使用 `brew services`
|
||||
- 所有服務使用 `com.momentry.*` 作為 plist Label
|
||||
- 數據存放於 `/Users/accusys/momentry/` 目錄
|
||||
- 每個服務需提供完整的監控腳本
|
||||
- 所有服務 Plist 存放於 `/Library/LaunchDaemons/`
|
||||
- 所有服務以 `accusys` 用戶運行,確保 accusys 可以管理
|
||||
|
||||
---
|
||||
|
||||
## 二、服務運行方式
|
||||
|
||||
### 2.1 運行分類
|
||||
|
||||
| 類型 | 說明 | 示例 |
|
||||
|------|------|------|
|
||||
| **開機自動運行** | 電腦開機後立即自動啟動 | PostgreSQL, Redis, n8n, Caddy 等核心服務 |
|
||||
| **登入時運行** | 用戶登入後才啟動 | 開發工具、臨時服務 |
|
||||
|
||||
**當前所有服務**:均為開機自動運行
|
||||
|
||||
### 2.2 Plist 存放位置
|
||||
|
||||
所有 Momentry 服務統一存放於:
|
||||
```
|
||||
/Library/LaunchDaemons/com.momentry.{service_name}.plist
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 三、服務命名規範
|
||||
|
||||
### 3.1 Plist 文件命名
|
||||
|
||||
```
|
||||
com.momentry.{service_name}.plist
|
||||
```
|
||||
|
||||
示例:
|
||||
- `com.momentry.redis.plist`
|
||||
- `com.momentry.n8n.main.plist`
|
||||
- `com.momentry.rustdesk.hbbs.plist`
|
||||
|
||||
### 3.2 目錄命名
|
||||
|
||||
服務相關目錄統一放置於:
|
||||
```
|
||||
/Users/accusys/momentry/
|
||||
├── var/{service_name}/ # 服務數據
|
||||
├── etc/{service_name}/ # 服務配置
|
||||
├── log/{service_name}.log # 服務日誌 (stdout)
|
||||
└── log/{service_name}.error.log # 錯誤日誌 (stderr)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 四、Plist 文件模板
|
||||
|
||||
### 4.1 標準服務模板
|
||||
|
||||
```xml
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>Label</key>
|
||||
<string>com.momentry.{service_name}</string>
|
||||
|
||||
<key>UserName</key>
|
||||
<string>accusys</string>
|
||||
|
||||
<key>WorkingDirectory</key>
|
||||
<string>/Users/accusys/momentry/var/{service_name}</string>
|
||||
|
||||
<key>ProgramArguments</key>
|
||||
<array>
|
||||
<string>/path/to/executable</string>
|
||||
<string>--arg1</string>
|
||||
<string>value1</string>
|
||||
</array>
|
||||
|
||||
<key>EnvironmentVariables</key>
|
||||
<dict>
|
||||
<key>PATH</key>
|
||||
<string>/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin</string>
|
||||
<!-- 其他環境變數 -->
|
||||
</dict>
|
||||
|
||||
<key>RunAtLoad</key>
|
||||
<true/>
|
||||
|
||||
<key>KeepAlive</key>
|
||||
<true/>
|
||||
|
||||
<key>StandardOutPath</key>
|
||||
<string>/Users/accusys/momentry/log/{service_name}.log</string>
|
||||
|
||||
<key>StandardErrorPath</key>
|
||||
<string>/Users/accusys/momentry/log/{service_name}.error.log</string>
|
||||
</dict>
|
||||
</plist>
|
||||
```
|
||||
|
||||
### 4.2 日誌文件規範
|
||||
|
||||
每個服務必須創建兩個日誌文件:
|
||||
|
||||
| 文件 | 說明 | 路徑 |
|
||||
|------|------|------|
|
||||
| StandardOutPath | 標準輸出日誌 | `/Users/accusys/momentry/log/{service_name}.log` |
|
||||
| StandardErrorPath | 錯誤輸出日誌 | `/Users/accusys/momentry/log/{service_name}.error.log` |
|
||||
|
||||
**創建日誌文件**:
|
||||
```bash
|
||||
touch /Users/accusys/momentry/log/{service_name}.log
|
||||
touch /Users/accusys/momentry/log/{service_name}.error.log
|
||||
chmod 644 /Users/accusys/momentry/log/{service_name}.log
|
||||
chmod 644 /Users/accusys/momentry/log/{service_name}.error.log
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 五、添加服務步驟
|
||||
|
||||
### 步驟 1:創建目錄結構
|
||||
|
||||
```bash
|
||||
# 創建服務目錄
|
||||
mkdir -p /Users/accusys/momentry/var/{service_name}
|
||||
mkdir -p /Users/accusys/momentry/etc/{service_name}
|
||||
|
||||
# 創建日誌文件
|
||||
touch /Users/accusys/momentry/log/{service_name}.log
|
||||
touch /Users/accusys/momentry/log/{service_name}.error.log
|
||||
```
|
||||
|
||||
### 步驟 2:創建 Plist 文件
|
||||
|
||||
```bash
|
||||
# 複製模板並編輯
|
||||
cp /Users/accusys/momentry_core_0.1/momentry_runtime/plist/template.service.plist \
|
||||
/Users/accusys/momentry_core_0.1/momentry_runtime/plist/com.momentry.{service_name}.plist
|
||||
|
||||
# 編輯 plist 文件
|
||||
vim /Users/accusys/momentry_core_0.1/momentry_runtime/plist/com.momentry.{service_name}.plist
|
||||
```
|
||||
|
||||
### 步驟 3:複製到系統 LaunchDaemons
|
||||
|
||||
```bash
|
||||
# 複製到 /Library/LaunchDaemons/
|
||||
sudo cp /Users/accusys/momentry_core_0.1/momentry_runtime/plist/com.momentry.{service_name}.plist \
|
||||
/Library/LaunchDaemons/
|
||||
```
|
||||
|
||||
### 步驟 4:載入服務
|
||||
|
||||
```bash
|
||||
# 載入服務
|
||||
sudo launchctl load /Library/LaunchDaemons/com.momentry.{service_name}.plist
|
||||
|
||||
# 驗證服務狀態
|
||||
launchctl list | grep momentry
|
||||
```
|
||||
|
||||
### 步驟 5:添加監控
|
||||
|
||||
在 `monitor/config/monitor_config.yaml` 中添加服務配置:
|
||||
|
||||
```yaml
|
||||
service:
|
||||
services:
|
||||
- name: "{service_name}"
|
||||
type: "http" # 或 "process", "tcp"
|
||||
port: {port_number}
|
||||
host: "localhost"
|
||||
check_url: "http://localhost:{port}/health"
|
||||
timeout: 5
|
||||
enabled: true
|
||||
```
|
||||
|
||||
### 步驟 6:添加文檔
|
||||
|
||||
在 `docs/INSTALL_{SERVICE_NAME}.md` 中記錄:
|
||||
- 安裝步驟
|
||||
- 配置說明
|
||||
- 健康檢查命令
|
||||
- 故障排除
|
||||
|
||||
---
|
||||
|
||||
## 六、服務分類
|
||||
|
||||
### 按功能分類
|
||||
|
||||
| 類別 | 服務 |
|
||||
|------|------|
|
||||
| 資料庫 | PostgreSQL, Redis, MariaDB, MongoDB |
|
||||
| 應用 | n8n, Gitea, SFTPGo |
|
||||
| 網頁 | Caddy, PHP-FPM |
|
||||
| AI/ML | Ollama, Qdrant |
|
||||
| 遠程 | RustDesk |
|
||||
|
||||
### 按運行方式分類
|
||||
|
||||
| 運行方式 | 數量 | 服務 |
|
||||
|----------|------|------|
|
||||
| 開機自動運行 | 15 | PostgreSQL, Redis, n8n, Caddy, Gitea, SFTPGo, Ollama, Qdrant, MariaDB, PHP-FPM, RustDesk, MongoDB, Agent |
|
||||
| 登入時運行 | 0 | (暫無) |
|
||||
|
||||
---
|
||||
|
||||
## 七、監控要求
|
||||
|
||||
每個服務必須提供:
|
||||
|
||||
### 7.1 健康檢查
|
||||
|
||||
在 `monitor/service/health_check.sh` 中添加檢查函數:
|
||||
|
||||
```bash
|
||||
check_{service_name}() {
|
||||
local start=$(date +%s%N)
|
||||
if nc -z localhost {port} > /dev/null 2>&1; then
|
||||
local end=$(date +%s%N)
|
||||
local ms=$(( (end - start) / 1000000 ))
|
||||
echo -e "${GREEN}✓${NC} {service_name} ({port}) - ${ms}ms"
|
||||
record_service "{service_name}" "up" "$ms" ""
|
||||
return 0
|
||||
else
|
||||
echo -e "${RED}✗${NC} {service_name} ({port}) - Down"
|
||||
record_service "{service_name}" "down" "0" "Connection failed"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
```
|
||||
|
||||
### 7.2 數據庫記錄
|
||||
|
||||
```sql
|
||||
-- 添加服務監控記錄函數
|
||||
record_service() {
|
||||
local service=$1
|
||||
local status=$2
|
||||
local response_time=$3
|
||||
local error_msg=$4
|
||||
|
||||
psql -U accusys -h localhost -d momentry << EOF
|
||||
INSERT INTO monitor_services (service_name, service_type, status, response_time_ms, error_message, checked_at)
|
||||
VALUES ('$service', 'service', '$status', $response_time, '$error_msg', NOW());
|
||||
EOF
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 八、服務管理命令
|
||||
|
||||
### 8.1 基本操作
|
||||
|
||||
```bash
|
||||
# 啟動服務 (使用 launchctl bootstrap)
|
||||
sudo launchctl bootstrap system /Library/LaunchDaemons/com.momentry.{service}.plist
|
||||
|
||||
# 停止服務 (使用 launchctl bootout)
|
||||
sudo launchctl bootout system/com.momentry.{service}.plist
|
||||
|
||||
# 重新載入服務
|
||||
sudo launchctl bootout system/com.momentry.{service}.plist
|
||||
sudo launchctl bootstrap system /Library/LaunchDaemons/com.momentry.{service}.plist
|
||||
|
||||
# 查看服務狀態
|
||||
launchctl list | grep com.momentry
|
||||
|
||||
# 查看特定服務狀態
|
||||
launchctl list | grep com.momentry.{service}
|
||||
|
||||
# 查看服務日誌
|
||||
tail -f /Users/accusys/momentry/log/{service}.log
|
||||
tail -f /Users/accusys/momentry/log/{service}.error.log
|
||||
```
|
||||
|
||||
### 8.2 批量管理
|
||||
|
||||
```bash
|
||||
# 啟動所有 Momentry 服務
|
||||
for plist in /Library/LaunchDaemons/com.momentry.*.plist; do
|
||||
sudo launchctl bootstrap system "$plist"
|
||||
done
|
||||
|
||||
# 停止所有 Momentry 服務
|
||||
for svc in $(launchctl list | grep com.momentry | awk '{print $3}'); do
|
||||
sudo launchctl bootout system/$svc 2>/dev/null
|
||||
done
|
||||
|
||||
# 查看所有 Momentry 服務狀態
|
||||
launchctl list | grep com.momentry
|
||||
```
|
||||
|
||||
### 8.2 故障排除
|
||||
|
||||
```bash
|
||||
# 檢查服務是否運行
|
||||
pgrep -f "{service_process_name}"
|
||||
|
||||
# 檢查端口是否監聽
|
||||
lsof -i :{port}
|
||||
|
||||
# 檢查錯誤日誌
|
||||
tail -100 /Users/accusys/momentry/log/{service}.error.log
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 九、服務備份作業
|
||||
|
||||
### 9.1 備份內容
|
||||
|
||||
每個服務需要備份的內容:
|
||||
|
||||
| 類別 | 路徑 | 說明 |
|
||||
|------|------|------|
|
||||
| 數據 | `/Users/accusys/momentry/var/{service}/` | 服務運行數據 |
|
||||
| 配置 | `/Users/accusys/momentry/etc/{service}/` | 服務配置文件 |
|
||||
| Plist | `/Library/LaunchDaemons/com.momentry.{service}.plist` | 啟動配置 |
|
||||
| 日誌 | `/Users/accusys/momentry/log/{service}.log` | 運行日誌 |
|
||||
|
||||
### 9.2 備份命名規範
|
||||
|
||||
**格式**: `{service}_{type}_{YYYYMMDD}_{HHMMSS}[_{suffix}].{ext}`
|
||||
|
||||
**組成部分**:
|
||||
| 位置 | 說明 | 範例 |
|
||||
|------|------|------|
|
||||
| `{service}` | 服務名稱 (小寫) | `postgresql`, `redis`, `n8n` |
|
||||
| `{type}` | 備份類型 | `full`, `db`, `cfg`, `data` |
|
||||
| `{YYYYMMDD}` | 備份日期 | `20260315` |
|
||||
| `{HHMMSS}` | 備份時間 (24小時制) | `030000` |
|
||||
| `{suffix}` | 可選標記 | `incremental`, `verified` |
|
||||
| `{ext}` | 檔案擴展名 | `sql.gz`, `tar.gz`, `rdb`, `zip` |
|
||||
|
||||
**類型說明**:
|
||||
| 類型 | 說明 | 包含內容 |
|
||||
|------|------|---------|
|
||||
| `full` | 完整備份 | 數據 + 配置 + 日誌 |
|
||||
| `db` | 數據庫備份 | 資料庫導出 (sql, rdb) |
|
||||
| `cfg` | 配置備份 | 配置文件 |
|
||||
| `data` | 數據備份 | var 目錄 |
|
||||
|
||||
**範例**:
|
||||
```
|
||||
postgresql_db_20260315_030000.sql.gz # PostgreSQL 完整資料庫 (壓縮)
|
||||
redis_rdb_20260315_030000.rdb # Redis RDB 快照
|
||||
n8n_full_20260315_030000.tar.gz # n8n 完整備份
|
||||
mariadb_db_wordpress_20260315_030000.sql.gz # MariaDB WP 資料庫
|
||||
gitea_full_20260315_030000.zip # Gitea 完整備份
|
||||
qdrant_snapshot_20260315_030000.tar.gz # Qdrant 向量庫
|
||||
ollama_cfg_20260315_030000.tar.gz # Ollama 配置
|
||||
caddy_cfg_20260315_030000.tar.gz # Caddy 配置
|
||||
```
|
||||
|
||||
**可信斷點標記**:
|
||||
- 檔名本身即為可信時間點
|
||||
- 還原時直接使用檔名中的時間戳
|
||||
- 建議配合 `backup_registry` 資料庫記錄完整元數據
|
||||
|
||||
**校驗和命名**:
|
||||
```
|
||||
postgresql_db_20260315_030000.sql.gz.sha256
|
||||
```
|
||||
|
||||
### 9.3 備份腳本
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# 標準化備份腳本範本
|
||||
# 遵循命名規範: {service}_{type}_{YYYYMMDD}_{HHMMSS}.{ext}
|
||||
|
||||
set -e
|
||||
|
||||
SERVICE_NAME="{service_name}"
|
||||
BACKUP_TYPE="{type}" # full, db, cfg, data
|
||||
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
|
||||
BACKUP_DIR="/Users/accusys/momentry/backup/${SERVICE_NAME}"
|
||||
|
||||
mkdir -p "$BACKUP_DIR"
|
||||
|
||||
# 根據類型執行備份
|
||||
case "$BACKUP_TYPE" in
|
||||
full)
|
||||
echo "[$TIMESTAMP] 執行 $SERVICE_NAME 完整備份..."
|
||||
tar -czf "$BACKUP_DIR/${SERVICE_NAME}_full_${TIMESTAMP}.tar.gz" \
|
||||
/Users/accusys/momentry/var/${SERVICE_NAME}/ \
|
||||
/Users/accusys/momentry/etc/${SERVICE_NAME}/ 2>/dev/null
|
||||
;;
|
||||
db)
|
||||
echo "[$TIMESTAMP] 執行 $SERVICE_NAME 資料庫備份..."
|
||||
if [ "$SERVICE_NAME" = "postgresql" ]; then
|
||||
pg_dump -U accusys ${SERVICE_NAME} | gzip > \
|
||||
"$BACKUP_DIR/${SERVICE_NAME}_db_${TIMESTAMP}.sql.gz"
|
||||
elif [ "$SERVICE_NAME" = "mariadb" ]; then
|
||||
mysqldump -u root -p --all-databases | gzip > \
|
||||
"$BACKUP_DIR/${SERVICE_NAME}_db_${TIMESTAMP}.sql.gz"
|
||||
elif [ "$SERVICE_NAME" = "redis" ]; then
|
||||
redis-cli -a accusys SAVE
|
||||
cp /opt/homebrew/var/db/redis/dump.rdb \
|
||||
"$BACKUP_DIR/${SERVICE_NAME}_rdb_${TIMESTAMP}.rdb"
|
||||
fi
|
||||
;;
|
||||
cfg)
|
||||
echo "[$TIMESTAMP] 執行 $SERVICE_NAME 配置備份..."
|
||||
tar -czf "$BACKUP_DIR/${SERVICE_NAME}_cfg_${TIMESTAMP}.tar.gz" \
|
||||
/Users/accusys/momentry/etc/${SERVICE_NAME}/ 2>/dev/null
|
||||
;;
|
||||
data)
|
||||
echo "[$TIMESTAMP] 執行 $SERVICE_NAME 數據備份..."
|
||||
tar -czf "$BACKUP_DIR/${SERVICE_NAME}_data_${TIMESTAMP}.tar.gz" \
|
||||
/Users/accusys/momentry/var/${SERVICE_NAME}/ 2>/dev/null
|
||||
;;
|
||||
esac
|
||||
|
||||
# 生成校驗和
|
||||
if [ -f "$BACKUP_DIR/${SERVICE_NAME}_${BACKUP_TYPE}_${TIMESTAMP}"* ]; then
|
||||
sha256sum "$BACKUP_DIR/${SERVICE_NAME}_${BACKUP_TYPE}_${TIMESTAMP}"* > \
|
||||
"$BACKUP_DIR/${SERVICE_NAME}_${BACKUP_TYPE}_${TIMESTAMP}.sha256"
|
||||
fi
|
||||
|
||||
# 清理舊備份 (保留 30 天)
|
||||
find "$BACKUP_DIR" -name "*_${TIMESTAMP%%_*}_*.tar.gz" -mtime +30 -delete 2>/dev/null
|
||||
find "$BACKUP_DIR" -name "*_${TIMESTAMP%%_*}_*.sql.gz" -mtime +30 -delete 2>/dev/null
|
||||
find "$BACKUP_DIR" -name "*_${TIMESTAMP%%_*}_*.rdb" -mtime +30 -delete 2>/dev/null
|
||||
find "$BACKUP_DIR" -name "*.sha256" -mtime +30 -delete 2>/dev/null
|
||||
|
||||
echo "備份完成: ${SERVICE_NAME}_${BACKUP_TYPE}_${TIMESTAMP}"
|
||||
```
|
||||
|
||||
### 9.4 備份排程
|
||||
|
||||
建議使用 cron 進行自動備份:
|
||||
|
||||
```bash
|
||||
# 編輯 crontab
|
||||
crontab -e
|
||||
|
||||
# 添加備份任務 (每天凌晨 3 點)
|
||||
0 3 * * * /Users/accusys/momentry/scripts/backup_{service}.sh >> /Users/accusys/momentry/log/backup.log 2>&1
|
||||
|
||||
# 每週日凌晨 3 點執行完整備份
|
||||
0 3 * * 0 /Users/accusys/momentry/scripts/backup_{service}.sh full >> /Users/accusys/momentry/log/backup.log 2>&1
|
||||
```
|
||||
|
||||
### 9.5 備份驗證
|
||||
|
||||
```bash
|
||||
# 查看備份列表 (按時間排序)
|
||||
ls -lt /Users/accusys/momentry/backup/{service}/
|
||||
|
||||
# 驗證備份完整性
|
||||
# 1. 檢查校驗和
|
||||
sha256sum -c /Users/accusys/momentry/backup/{service}/*.sha256
|
||||
|
||||
# 2. 驗證 tar 壓縮
|
||||
tar -tzf /Users/accusys/momentry/backup/{service}/{service}_full_20260315_030000.tar.gz
|
||||
|
||||
# 3. 驗證 SQL 備份
|
||||
zcat /Users/accusys/momentry/backup/{service}/{service}_db_20260315_030000.sql.gz | head -5
|
||||
|
||||
# 驗證備份完整性
|
||||
tar -tzf /Users/accusys/momentry/backup/{service}/{service}_var_20260315.tar.gz
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 十、服務完整刪除作業
|
||||
|
||||
### 10.1 刪除前確認
|
||||
|
||||
**警告**:此操作不可逆,請確保已完成備份!
|
||||
|
||||
- [ ] 確認服務已停止運行
|
||||
- [ ] 確認數據已備份
|
||||
- [ ] 確認無其他服務依賴此服務
|
||||
|
||||
### 10.2 刪除步驟
|
||||
|
||||
**步驟 1:停止服務**
|
||||
|
||||
```bash
|
||||
# 停止服務
|
||||
sudo launchctl unload /Library/LaunchDaemons/com.momentry.{service}.plist
|
||||
|
||||
# 驗證服務已停止
|
||||
launchctl list | grep momentry.{service}
|
||||
```
|
||||
|
||||
**步驟 2:刪除 Plist**
|
||||
|
||||
```bash
|
||||
# 刪除系統 Plist
|
||||
sudo rm /Library/LaunchDaemons/com.momentry.{service}.plist
|
||||
|
||||
# 刪除專案 Plist
|
||||
rm /Users/accusys/momentry_core_0.1/momentry_runtime/plist/com.momentry.{service}.plist
|
||||
```
|
||||
|
||||
**步驟 3:刪除數據和配置**
|
||||
|
||||
```bash
|
||||
# 刪除數據目錄
|
||||
sudo rm -rf /Users/accusys/momentry/var/{service}/
|
||||
|
||||
# 刪除配置目錄
|
||||
sudo rm -rf /Users/accusys/momentry/etc/{service}/
|
||||
|
||||
# 刪除日誌
|
||||
rm -f /Users/accusys/momentry/log/{service}.log
|
||||
rm -f /Users/accusys/momentry/log/{service}.error.log
|
||||
```
|
||||
|
||||
**步驟 4:清理監控配置**
|
||||
|
||||
```bash
|
||||
# 從監控配置中移除服務
|
||||
vim /Users/accusys/momentry_core_0.1/monitor/config/monitor_config.yaml
|
||||
# 刪除該服務的監控配置
|
||||
|
||||
# 從監控腳本中移除
|
||||
vim /Users/accusys/momentry_core_0.1/monitor/service/health_check.sh
|
||||
# 移除該服務的檢查函數
|
||||
```
|
||||
|
||||
**步驟 5:清理監控數據(可選)**
|
||||
|
||||
```bash
|
||||
# 保留歷史數據還是刪除?
|
||||
# 刪除監控數據
|
||||
psql -U accusys -h localhost -d momentry -c "
|
||||
DELETE FROM monitor_services WHERE service_name = '{service}';
|
||||
"
|
||||
```
|
||||
|
||||
### 10.3 驗證刪除
|
||||
|
||||
```bash
|
||||
# 確認服務已停止
|
||||
launchctl list | grep momentry.{service}
|
||||
|
||||
# 確認目錄已刪除
|
||||
ls /Users/accusys/momentry/var/{service}/ 2>/dev/null || echo "已刪除"
|
||||
|
||||
# 確認 Plist 已刪除
|
||||
ls /Library/LaunchDaemons/com.momentry.{service}.plist 2>/dev/null || echo "已刪除"
|
||||
```
|
||||
|
||||
### 10.4 完整刪除腳本
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
SERVICE_NAME="{service_name}"
|
||||
|
||||
echo "========== 服務完整刪除 =========="
|
||||
echo "服務: $SERVICE_NAME"
|
||||
echo "警告:此操作不可逆!"
|
||||
read -p "確認繼續 (yes/no): " confirm
|
||||
|
||||
if [ "$confirm" != "yes" ]; then
|
||||
echo "取消刪除"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# 停止服務
|
||||
echo "[1/6] 停止服務..."
|
||||
sudo launchctl unload /Library/LaunchDaemons/com.momentry.${SERVICE_NAME}.plist 2>/dev/null
|
||||
|
||||
# 刪除 Plist
|
||||
echo "[2/6] 刪除 Plist..."
|
||||
sudo rm -f /Library/LaunchDaemons/com.momentry.${SERVICE_NAME}.plist
|
||||
rm -f /Users/accusys/momentry_core_0.1/momentry_runtime/plist/com.momentry.${SERVICE_NAME}.plist
|
||||
|
||||
# 刪除數據
|
||||
echo "[3/6] 刪除數據..."
|
||||
sudo rm -rf /Users/accusys/momentry/var/${SERVICE_NAME}/
|
||||
|
||||
# 刪除配置
|
||||
echo "[4/6] 刪除配置..."
|
||||
sudo rm -rf /Users/accusys/momentry/etc/${SERVICE_NAME}/
|
||||
|
||||
# 刪除日誌
|
||||
echo "[5/6] 刪除日誌..."
|
||||
rm -f /Users/accusys/momentry/log/${SERVICE_NAME}.log
|
||||
rm -f /Users/accusys/momentry/log/${SERVICE_NAME}.error.log
|
||||
|
||||
# 清理監控數據
|
||||
echo "[6/6] 清理監控數據..."
|
||||
psql -U accusys -h localhost -d momentry -c "
|
||||
DELETE FROM monitor_services WHERE service_name = '${SERVICE_NAME}';
|
||||
" 2>/dev/null
|
||||
|
||||
echo "========== 刪除完成 =========="
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 十一、檢查清單
|
||||
|
||||
添加新服務時,請確認以下項目:
|
||||
|
||||
- [ ] 創建服務目錄 (`var/`, `etc/`)
|
||||
- [ ] 配置日誌文件 (`.log` + `.error.log`)
|
||||
- [ ] 創建 plist 文件,UserName 設為 `accusys`
|
||||
- [ ] 複製到 `/Library/LaunchDaemons/`
|
||||
- [ ] 使用 launchctl 載入服務
|
||||
- [ ] 驗證服務運行
|
||||
- [ ] 添加監控配置
|
||||
- [ ] 測試監控腳本
|
||||
- [ ] 創建安裝文檔
|
||||
- [ ] 更新 SERVICES.md 服務清單
|
||||
- [ ] 更新 MOMENTRY_INTEGRATION_GUIDE.md
|
||||
|
||||
---
|
||||
|
||||
## 十二、模板文件
|
||||
|
||||
### Plist 模板位置
|
||||
|
||||
```
|
||||
/Users/accusys/momentry_core_0.1/momentry_runtime/plist/
|
||||
├── template.service.plist # 服務模板
|
||||
├── com.momentry.redis.plist # 服務示例
|
||||
└── com.momentry.n8n.main.plist # 複雜服務示例
|
||||
```
|
||||
|
||||
### 創建模板命令
|
||||
|
||||
```bash
|
||||
# 創建服務模板
|
||||
cat > /Users/accusys/momentry_core_0.1/momentry_runtime/plist/template.service.plist << 'EOF'
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>Label</key>
|
||||
<string>com.momentry.SERVICE_NAME</string>
|
||||
<key>UserName</key>
|
||||
<string>accusys</string>
|
||||
<key>WorkingDirectory</key>
|
||||
<string>/Users/accusys/momentry/var/SERVICE_NAME</string>
|
||||
<key>ProgramArguments</key>
|
||||
<array>
|
||||
<string>/path/to/executable</string>
|
||||
</array>
|
||||
<key>RunAtLoad</key>
|
||||
<true/>
|
||||
<key>KeepAlive</key>
|
||||
<true/>
|
||||
<key>StandardOutPath</key>
|
||||
<string>/Users/accusys/momentry/log/SERVICE_NAME.log</string>
|
||||
<key>StandardErrorPath</key>
|
||||
<string>/Users/accusys/momentry/log/SERVICE_NAME.error.log</string>
|
||||
</dict>
|
||||
</plist>
|
||||
EOF
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 十一、版本歷史
|
||||
|
||||
| 版本 | 日期 | 內容 |
|
||||
|------|------|------|
|
||||
| 1.0 | 2026-03-15 | 初始版本 |
|
||||
| 2.0 | 2026-03-15 | 統一 Plist 位置、移除 root/用戶區分、加入運行方式分類 |
|
||||
| 2.1 | 2026-03-15 | 新增服務備份作業、服務完整刪除作業 |
|
||||
| 2.1 | 2026-03-24 | 更新 launchctl 命令,使用 `bootstrap`/`bootout` 替代 `load`/`unload` | |
|
||||
@@ -1,504 +0,0 @@
|
||||
# SFTPGo Demo 用戶指南
|
||||
|
||||
## Web 管理介面
|
||||
|
||||
**URL**: https://sftpgo.momentry.ddns.net
|
||||
|
||||
### 登入方式
|
||||
|
||||
| 角色 | 用戶名 | 密碼 |
|
||||
|------|--------|------|
|
||||
| **Demo 用戶** | `demo` | `demopassword123` |
|
||||
|
||||
### 可用功能
|
||||
|
||||
- 瀏覽個人目錄結構
|
||||
- 上傳、下載檔案
|
||||
- 查看上傳記錄
|
||||
|
||||
---
|
||||
|
||||
## 快速連線資訊
|
||||
|
||||
| 項目 | 值 |
|
||||
|------|-----|
|
||||
| **主機** | `sftpgo.momentry.ddns.net` |
|
||||
| **SFTP 連接埠** | `2022` |
|
||||
| **用戶名** | `demo` |
|
||||
| **密碼** | `demopassword123` |
|
||||
| **主目錄** | `/demo` |
|
||||
|
||||
---
|
||||
|
||||
## 連線方式
|
||||
|
||||
### 1. 命令列 SFTP
|
||||
|
||||
```bash
|
||||
# 使用密碼連線
|
||||
sshpass -p "demopassword123" sftp -P 2022 demo@sftpgo.momentry.ddns.net
|
||||
|
||||
# 使用金鑰連線 (需先設定)
|
||||
sftp -P 2022 -i ~/.ssh/id_rsa demo@sftpgo.momentry.ddns.net
|
||||
```
|
||||
|
||||
### 2. FileZilla
|
||||
|
||||
1. **主機**: `sftp://sftpgo.momentry.ddns.net`
|
||||
2. **連接埠**: `2022`
|
||||
3. **協定**: `SFTP`
|
||||
4. **登入類型**: `一般`
|
||||
5. **用戶名**: `demo`
|
||||
6. **密碼**: `demopassword123`
|
||||
|
||||
### 3. Cyberduck (macOS)
|
||||
|
||||
1. 選擇 **連線 > 新連線**
|
||||
2. 協定選擇 **SFTP (SSH File Transfer Protocol)**
|
||||
3. 伺服器: `sftpgo.momentry.ddns.net`
|
||||
4. 連接埠: `2022`
|
||||
5. 使用者名稱: `demo`
|
||||
6. 密碼: `demopassword123`
|
||||
|
||||
### 4. curl 上傳
|
||||
|
||||
```bash
|
||||
curl -u demo:demopassword123 \
|
||||
-T /path/to/video.mp4 \
|
||||
sftp://sftpgo.momentry.ddns.net:2022/demo/
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## SFTP 基本操作
|
||||
|
||||
### 連線後常用指令
|
||||
|
||||
```bash
|
||||
# 進入互動式模式
|
||||
sftp demo@sftpgo.momentry.ddns.net -P 2022
|
||||
|
||||
# 常用指令
|
||||
sftp> pwd # 顯示目前目錄
|
||||
sftp> ls # 列出檔案
|
||||
sftp> ls -la # 詳細列表
|
||||
sftp> cd uploads # 切換目錄
|
||||
sftp> mkdir videos # 建立目錄
|
||||
sftp> put local.mp4 # 上傳檔案
|
||||
sftp> get remote.mp4 # 下載檔案
|
||||
sftp> rm old.mp4 # 刪除檔案
|
||||
sftp> exit # 斷線
|
||||
```
|
||||
|
||||
### 批次上傳
|
||||
|
||||
```bash
|
||||
# 上傳多個檔案
|
||||
sshpass -p "demopassword123" sftp -P 2022 demo@sftpgo.momentry.ddns.net <<EOF
|
||||
cd uploads
|
||||
put video1.mp4
|
||||
put video2.mp4
|
||||
put video3.mp4
|
||||
bye
|
||||
EOF
|
||||
|
||||
# 使用 glob 上傳
|
||||
sshpass -p "demopassword123" sftp -P 2022 demo@sftpgo.momentry.ddns.net <<EOF
|
||||
mput /path/to/videos/*.mp4
|
||||
bye
|
||||
EOF
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 自動上傳腳本
|
||||
|
||||
### Bash 腳本
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# upload.sh - 上傳視頻到 Momentry
|
||||
|
||||
HOST="sftpgo.momentry.ddns.net"
|
||||
PORT="2022"
|
||||
USER="demo"
|
||||
PASS="demopassword123"
|
||||
REMOTE_DIR="/demo/uploads"
|
||||
|
||||
# 要上傳的檔案
|
||||
FILE="$1"
|
||||
|
||||
if [ -z "$FILE" ]; then
|
||||
echo "用法: $0 <檔案路徑>"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
sshpass -p "$PASS" sftp -P $PORT $USER@$HOST <<EOF
|
||||
mkdir $REMOTE_DIR
|
||||
cd $REMOTE_DIR
|
||||
put "$FILE"
|
||||
bye
|
||||
EOF
|
||||
|
||||
echo "上傳完成: $FILE"
|
||||
```
|
||||
|
||||
使用方式:
|
||||
```bash
|
||||
chmod +x upload.sh
|
||||
./upload.sh /path/to/video.mp4
|
||||
```
|
||||
|
||||
### Python 腳本
|
||||
|
||||
```python
|
||||
#!/usr/bin/env python3
|
||||
"""上傳檔案到 Momentry SFTP"""
|
||||
|
||||
import paramiko
|
||||
import sys
|
||||
import os
|
||||
|
||||
def upload_file(local_path, remote_dir="/demo/uploads"):
|
||||
host = "sftpgo.momentry.ddns.net"
|
||||
port = 2022
|
||||
username = "demo"
|
||||
password = "demopassword123"
|
||||
|
||||
transport = paramiko.Transport((host, port))
|
||||
transport.connect(username=username, password=password)
|
||||
sftp = paramiko.SFTPClient.from_transport(transport)
|
||||
|
||||
filename = os.path.basename(local_path)
|
||||
remote_path = f"{remote_dir}/{filename}"
|
||||
|
||||
sftp.put(local_path, remote_path)
|
||||
print(f"已上傳: {filename} -> {remote_path}")
|
||||
|
||||
sftp.close()
|
||||
transport.close()
|
||||
|
||||
if __name__ == "__main__":
|
||||
if len(sys.argv) < 2:
|
||||
print("用法: python upload_sftp.py <檔案路徑>")
|
||||
sys.exit(1)
|
||||
|
||||
upload_file(sys.argv[1])
|
||||
```
|
||||
|
||||
安裝依賴:
|
||||
```bash
|
||||
pip install paramiko
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## WebDAV 替代方案
|
||||
|
||||
如果 SFTP 連線有問題,可使用 WebDAV:
|
||||
|
||||
| 項目 | 值 |
|
||||
|------|-----|
|
||||
| **URL** | `https://momentry.ddns.net/webdav/` |
|
||||
| **用戶名** | `demo` |
|
||||
| **密碼** | `demopassword123` |
|
||||
|
||||
### curl 使用 WebDAV
|
||||
|
||||
```bash
|
||||
# 上傳
|
||||
curl -u demo:demopassword123 \
|
||||
-T video.mp4 \
|
||||
"https://momentry.ddns.net/webdav/demo/uploads/"
|
||||
|
||||
# 下載
|
||||
curl -u demo:demopassword123 \
|
||||
-o video.mp4 \
|
||||
"https://momentry.ddns.net/webdav/demo/uploads/video.mp4"
|
||||
|
||||
# 列出目錄
|
||||
curl -u demo:demopassword123 \
|
||||
-X PROPFIND \
|
||||
"https://momentry.ddns.net/webdav/demo/" \
|
||||
-H "Depth: 1"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 故障排除
|
||||
|
||||
### 連線被拒絕
|
||||
|
||||
```bash
|
||||
# 檢查 SFTPGo 是否運行
|
||||
curl -s http://localhost:8080/api/v2/status | jq .status
|
||||
|
||||
# 檢查連接埠
|
||||
nc -zv momentry.ddns.net 2022
|
||||
```
|
||||
|
||||
### 認證失敗
|
||||
|
||||
確認密碼是否正確:
|
||||
```bash
|
||||
# 測試認證
|
||||
curl -u demo:demopassword123 \
|
||||
"https://momentry.ddns.net/webdav/" -I
|
||||
```
|
||||
|
||||
### 權限不足
|
||||
|
||||
上傳目錄可能需要先建立:
|
||||
```bash
|
||||
sshpass -p "demopassword123" sftp -P 2022 demo@sftpgo.momentry.ddns.net <<EOF
|
||||
mkdir uploads
|
||||
mkdir videos
|
||||
bye
|
||||
EOF
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 檔案上傳後自動化
|
||||
|
||||
上傳後,SFTPGo 會自動:
|
||||
1. 觸發 Hook 腳本
|
||||
2. 記錄上傳事件到 `/Users/accusys/sftpgo_test/hook.log`
|
||||
3. 呼叫 Momentry Core API 註冊視頻
|
||||
|
||||
查看上傳日誌:
|
||||
```bash
|
||||
tail -f /Users/accusys/sftpgo_test/hook.log
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 管理手冊
|
||||
|
||||
### 管理員帳戶
|
||||
|
||||
| 角色 | 用戶名 | 密碼 | 說明 |
|
||||
|------|--------|------|------|
|
||||
| **WebAdmin** | `admin` | `Test3200Test3200` | SFTPGo 管理介面 |
|
||||
|
||||
**WebAdmin URL**: https://sftpgo.momentry.ddns.net
|
||||
|
||||
### Admin 創建方式
|
||||
|
||||
根據官方文檔,SFTPGo 有兩種方式創建管理員:
|
||||
|
||||
#### 方式 1: Web UI (首次設定)
|
||||
|
||||
1. 訪問 `http://localhost:8080/web/admin`
|
||||
2. 如果沒有管理員,會顯示設定畫面
|
||||
3. 輸入用戶名和密碼創建第一個管理員
|
||||
|
||||
#### 方式 2: 自動創建 (推薦)
|
||||
|
||||
需要同時滿足以下條件:
|
||||
1. 配置文件中設定 `"create_default_admin": true`
|
||||
2. 設定環境變數 `SFTPGO_DEFAULT_ADMIN_USERNAME` 和 `SFTPGO_DEFAULT_ADMIN_PASSWORD`
|
||||
|
||||
### 設定步驟
|
||||
|
||||
#### Step 1: 確保配置文件正確
|
||||
|
||||
確認 `/Users/accusys/momentry/etc/sftpgo/sftpgo.json` 中有:
|
||||
```json
|
||||
{
|
||||
"data_provider": {
|
||||
"create_default_admin": true
|
||||
},
|
||||
"httpd": {
|
||||
"setup": {
|
||||
"installation_code": "Test3200Test3200"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Step 2: 更新 plist 加入環境變數
|
||||
|
||||
編輯 `/Library/LaunchDaemons/com.momentry.sftpgo.plist`,加入:
|
||||
```xml
|
||||
<key>EnvironmentVariables</key>
|
||||
<dict>
|
||||
<key>SFTPGO_DEFAULT_ADMIN_USERNAME</key>
|
||||
<string>admin</string>
|
||||
<key>SFTPGO_DEFAULT_ADMIN_PASSWORD</key>
|
||||
<string>Test3200Test3200</string>
|
||||
</dict>
|
||||
```
|
||||
|
||||
#### Step 3: 重啟 SFTPGo
|
||||
|
||||
```bash
|
||||
launchctl unload homebrew.mxcl.sftpgo
|
||||
launchctl load homebrew.mxcl.sftpgo
|
||||
```
|
||||
|
||||
#### Step 4: 驗證管理員
|
||||
|
||||
```bash
|
||||
curl -s -X GET "http://localhost:8080/api/v2/token" \
|
||||
-u "admin:Test3200Test3200"
|
||||
```
|
||||
|
||||
成功回應:
|
||||
```json
|
||||
{
|
||||
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
|
||||
"token_type": "bearer",
|
||||
"expires_in": 1200
|
||||
}
|
||||
```
|
||||
|
||||
### REST API 認證流程
|
||||
|
||||
#### 1. 獲取 Token
|
||||
|
||||
```bash
|
||||
curl -s -X GET "http://localhost:8080/api/v2/token" \
|
||||
-u "admin:Test3200Test3200"
|
||||
```
|
||||
|
||||
#### 2. 使用 Token 訪問 API
|
||||
|
||||
```bash
|
||||
TOKEN=$(curl -s -X GET "http://localhost:8080/api/v2/token" \
|
||||
-u "admin:Test3200Test3200" | jq -r '.access_token')
|
||||
|
||||
# 查看所有用戶
|
||||
curl -s http://localhost:8080/api/v2/users \
|
||||
-H "Authorization: Bearer $TOKEN"
|
||||
|
||||
# 查看系統狀態
|
||||
curl -s http://localhost:8080/api/v2/status \
|
||||
-H "Authorization: Bearer $TOKEN"
|
||||
```
|
||||
|
||||
### 常用管理操作
|
||||
|
||||
#### 查看所有用戶
|
||||
|
||||
```bash
|
||||
TOKEN=$(curl -s -X GET "http://localhost:8080/api/v2/token" \
|
||||
-u "admin:Test3200Test3200" | jq -r '.access_token')
|
||||
|
||||
curl -s http://localhost:8080/api/v2/users \
|
||||
-H "Authorization: Bearer $TOKEN" | jq .
|
||||
```
|
||||
|
||||
#### 建立新用戶
|
||||
|
||||
```bash
|
||||
TOKEN=$(curl -s -X GET "http://localhost:8080/api/v2/token" \
|
||||
-u "admin:Test3200Test3200" | jq -r '.access_token')
|
||||
|
||||
curl -s -X POST http://localhost:8080/api/v2/users \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"username": "newuser",
|
||||
"password": "userpassword123",
|
||||
"email": "user@example.com",
|
||||
"status": 1,
|
||||
"home_dir": "/Users/accusys/momentry/var/sftpgo/data/newuser",
|
||||
"uid": 501,
|
||||
"gid": 20,
|
||||
"permissions": {
|
||||
"/": ["*"]
|
||||
}
|
||||
}'
|
||||
```
|
||||
|
||||
#### 建立用戶組
|
||||
|
||||
```bash
|
||||
TOKEN=$(curl -s -X GET "http://localhost:8080/api/v2/token" \
|
||||
-u "admin:Test3200Test3200" | jq -r '.access_token')
|
||||
|
||||
curl -s -X POST http://localhost:8080/api/v2/groups \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"name": "editors",
|
||||
"description": "Editor group with upload permissions"
|
||||
}'
|
||||
```
|
||||
|
||||
#### 刪除用戶
|
||||
|
||||
```bash
|
||||
TOKEN=$(curl -s -X GET "http://localhost:8080/api/v2/token" \
|
||||
-u "admin:Test3200Test3200" | jq -r '.access_token')
|
||||
|
||||
curl -s -X DELETE http://localhost:8080/api/v2/users/username \
|
||||
-H "Authorization: Bearer $TOKEN"
|
||||
```
|
||||
|
||||
#### 修改用戶密碼
|
||||
|
||||
```bash
|
||||
TOKEN=$(curl -s -X GET "http://localhost:8080/api/v2/token" \
|
||||
-u "admin:Test3200Test3200" | jq -r '.access_token')
|
||||
|
||||
curl -s -X PUT http://localhost:8080/api/v2/users/demo \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"password": "newpassword456"
|
||||
}'
|
||||
```
|
||||
|
||||
### 重要設定
|
||||
|
||||
| 設定項目 | 值 | 說明 |
|
||||
|----------|-----|------|
|
||||
| **SFTP 連接埠** | `2022` | SSH 檔案傳輸協定 |
|
||||
| **HTTP/WebDAV 連接埠** | `8080` | 內部 HTTP 服務 |
|
||||
| **WebAdmin 連接埠** | `8080` | 管理介面 (`/web/admin`) |
|
||||
| **WebClient 連接埠** | `8080` | 用戶介面 (`/web/client`) |
|
||||
| **資料庫** | PostgreSQL | 用戶和設定儲存 |
|
||||
| **Hook 腳本** | `/Users/accusys/sftpgo_test/register_hook.sh` | 上傳後自動化處理 |
|
||||
| **安裝碼** | `Test3200Test3200` | 首次設定管理員所需 |
|
||||
| **create_default_admin** | `true` | 自動創建管理員 |
|
||||
| **Token 有效期** | 1200 秒 (20分鐘) | JWT 過期時間 |
|
||||
|
||||
### 用戶目錄結構
|
||||
|
||||
所有 SFTPGo 用戶資料統一存放在 `/Users/accusys/momentry/var/sftpgo/data/` 目錄下:
|
||||
|
||||
| 用戶 | 資料夾路徑 | 密碼 | 說明 |
|
||||
|------|------------|------|------|
|
||||
| **demo** | `/Users/accusys/momentry/var/sftpgo/data/demo` | `demopassword123` | Demo 用戶上傳目錄 |
|
||||
| **momentry** | `/Users/accusys/momentry/var/sftpgo/data/momentry` | `momentry123` | Momentry 系統用戶 |
|
||||
| **warren** | `/Users/accusys/momentry/var/sftpgo/data/warren` | `warren123` | 其他用戶 |
|
||||
|
||||
### API Token 獲取方式
|
||||
|
||||
```bash
|
||||
# 注意:使用 GET 而非 POST
|
||||
curl -s -X GET "http://localhost:8080/api/v2/token" \
|
||||
-u "admin:Test3200Test3200" | jq .access_token
|
||||
```
|
||||
|
||||
### 故障排除
|
||||
|
||||
| 問題 | 解決方案 |
|
||||
|------|----------|
|
||||
| 無法獲取 Token | 確認環境變數已正確設定並重啟 SFTPGo |
|
||||
| SFTP 連線被拒絕 | 檢查 SFTPGo 服務: `launchctl list \| grep sftpgo` |
|
||||
| 無法登入 WebAdmin | 確認 admin 用戶存在: 檢查 plist 中環境變數是否正確 |
|
||||
| 上傳失敗 | 檢查 Hook 腳本: `tail -f /Users/accusys/momentry/log/sftpgo.error.log` |
|
||||
| 權限不足 | 檢查用戶權限或更新 `permissions` 設定 |
|
||||
| API 返回 401 | Token 過期,需重新獲取: `curl -X POST .../token -u "admin:pass"` | |
|
||||
|
||||
---
|
||||
|
||||
## 安全注意事項
|
||||
|
||||
- **密碼保護**: `demopassword123` 為 demo 帳戶密碼
|
||||
- **限制存取**: Demo 用戶只能訪問 `/demo` 目錄
|
||||
- **監控**: 所有上傳都有日誌記錄
|
||||
- **生產環境**: 正式環境應使用更強的密碼和金鑰認證
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,425 +0,0 @@
|
||||
# 統一會員系統 + 影片歸屬追蹤實作計畫
|
||||
|
||||
| 項目 | 內容 |
|
||||
|------|------|
|
||||
| 建立者 | Warren |
|
||||
| 建立時間 | 2026-03-24 |
|
||||
| 文件版本 | V1.0 |
|
||||
| 狀態 | 待確認 |
|
||||
|
||||
---
|
||||
|
||||
## 版本歷史
|
||||
|
||||
| 版本 | 日期 | 目的 | 操作人 |
|
||||
|------|------|------|--------|
|
||||
| V1.0 | 2026-03-24 | 創建實作計畫 | OpenCode |
|
||||
|
||||
---
|
||||
|
||||
## 1. 背景與目標
|
||||
|
||||
### 1.1 現有問題
|
||||
|
||||
目前 Momentry 生態系統中,各服務有獨立的用戶管理:
|
||||
|
||||
| 服務 | 用戶系統 | 問題 |
|
||||
|------|----------|------|
|
||||
| WordPress | wp_users (2 admin) | 無會員系統,無 API 認證 |
|
||||
| SFTPGo | users 表 (3 users) | 獨立管理 |
|
||||
| n8n | users 表 | 獨立管理 |
|
||||
| Gitea | `user` 表 | 獨立管理 |
|
||||
| Momentry Core | api_keys (未啟用) | 無 user 關聯 |
|
||||
|
||||
**問題**:
|
||||
1. 無法追蹤影片歸屬(誰上傳的影片)
|
||||
2. 無法實作 per-user 配額管理
|
||||
3. API 端點全部公開,無認證
|
||||
4. 用戶創建需要多處操作
|
||||
|
||||
### 1.2 目標
|
||||
|
||||
建立統一的會員系統,讓 WordPress 成為唯一登入入口:
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ 目標架構 │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ WordPress (會員系統) │
|
||||
│ │ │
|
||||
│ ├─► SFTPGo (檔案上傳) │
|
||||
│ ├─► Momentry Core (影片處理) │
|
||||
│ └─► n8n (自動化流程) │
|
||||
│ │
|
||||
│ 統一的 user_id 追蹤 │
|
||||
│ │ │
|
||||
│ └─► videos 表關聯 user_id │
|
||||
│ └─► monitor_jobs 表關聯 user_id │
|
||||
│ └─► per-user 配額管理 │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. 現有系統分析
|
||||
|
||||
### 2.1 WordPress
|
||||
|
||||
| 項目 | 狀態 |
|
||||
|------|------|
|
||||
| 安裝插件 | Elementor, Akismet, Code Snippets, All-in-One WP Migration |
|
||||
| 用戶表 | wp_users (2 users: wp_user, sc_demo) |
|
||||
| 會員插件 | 無 |
|
||||
| REST API | 標準端點 (`/wp-json/wp/v2/users`) |
|
||||
| 認證方式 | Cookie / Application Passwords |
|
||||
| JWT | 無 |
|
||||
|
||||
### 2.2 SFTPGo
|
||||
|
||||
| 項目 | 值 |
|
||||
|------|-----|
|
||||
| 用戶數 | 3 (demo, warren, momentry) |
|
||||
| API | REST API v2 (`/api/v2/users`) |
|
||||
| Admin | admin:Test3200Test3200 |
|
||||
| Hook | `/Users/accusys/sftpgo_test/register_hook.sh` |
|
||||
|
||||
### 2.3 Momentry Core
|
||||
|
||||
| 項目 | 狀態 |
|
||||
|------|------|
|
||||
| api_keys 表 | 已存在 |
|
||||
| users 表 | 不存在 |
|
||||
| videos.user_id | 不存在 |
|
||||
| API 認證 | 未啟用(所有端點公開) |
|
||||
|
||||
---
|
||||
|
||||
## 3. 實作計畫
|
||||
|
||||
### Phase 1: WordPress 認證機制啟用
|
||||
|
||||
#### 1.1 啟用 Application Passwords
|
||||
|
||||
**WordPress 5.6+ 內建功能**,無需額外插件。
|
||||
|
||||
```php
|
||||
// wp-config.php (如需自訂設定)
|
||||
define('WP APPLICATION_PASSWORDS_ENABLED', true);
|
||||
```
|
||||
|
||||
**使用方式**:
|
||||
```bash
|
||||
# Basic Auth 格式
|
||||
curl -X POST "https://wp.momentry.ddns.net/wp-json/wp/v2/users" \
|
||||
-u "username:application_password" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"username": "newuser", "email": "user@example.com", "password": "password"}'
|
||||
```
|
||||
|
||||
#### 1.2 測試 WordPress REST API
|
||||
|
||||
```bash
|
||||
# 取得用戶列表(需要 admin 權限)
|
||||
curl -s -u "wp_user:xxxx xxxx xxxx xxxx xxxx xxxx" \
|
||||
"https://wp.momentry.ddns.net/wp-json/wp/v2/users"
|
||||
|
||||
# 創建新用戶
|
||||
curl -X POST "https://wp.momentry.ddns.net/wp-json/wp/v2/users" \
|
||||
-u "wp_user:xxxx xxxx xxxx xxxx xxxx xxxx" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"username": "testuser",
|
||||
"email": "test@example.com",
|
||||
"password": "TestPass123!",
|
||||
"roles": ["subscriber"]
|
||||
}'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Phase 2: 資料庫結構調整
|
||||
|
||||
#### 2.1 新增 users 表(Momentry Core)
|
||||
|
||||
```sql
|
||||
-- migrations/002_user_management.sql
|
||||
|
||||
CREATE TABLE users (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
wordpress_id BIGINT UNIQUE NOT NULL,
|
||||
username VARCHAR(60) NOT NULL,
|
||||
email VARCHAR(100) NOT NULL,
|
||||
api_key_hash VARCHAR(64),
|
||||
quota_size BIGINT DEFAULT 10737418240, -- 10GB
|
||||
quota_used BIGINT DEFAULT 0,
|
||||
sftpgo_username VARCHAR(60),
|
||||
status VARCHAR(20) DEFAULT 'active', -- active, suspended, deleted
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX idx_users_wordpress_id ON users(wordpress_id);
|
||||
CREATE INDEX idx_users_username ON users(username);
|
||||
|
||||
-- videos 表新增 user_id
|
||||
ALTER TABLE videos ADD COLUMN user_id BIGINT REFERENCES users(id);
|
||||
CREATE INDEX idx_videos_user_id ON videos(user_id);
|
||||
|
||||
-- monitor_jobs 表新增 user_id
|
||||
ALTER TABLE monitor_jobs ADD COLUMN user_id BIGINT REFERENCES users(id);
|
||||
CREATE INDEX idx_monitor_jobs_user_id ON monitor_jobs(user_id);
|
||||
|
||||
-- api_keys 表新增 user_id
|
||||
ALTER TABLE api_keys ADD COLUMN user_id BIGINT REFERENCES users(id);
|
||||
```
|
||||
|
||||
#### 2.2 更新 api_keys 表結構
|
||||
|
||||
```sql
|
||||
-- 新增欄位
|
||||
ALTER TABLE api_keys ADD COLUMN user_id BIGINT REFERENCES users(id);
|
||||
ALTER TABLE api_keys ADD COLUMN wordpress_id BIGINT;
|
||||
ALTER TABLE api_keys ADD COLUMN sftpgo_username VARCHAR(60);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Phase 3: API 認證中介層
|
||||
|
||||
#### 3.1 中介層設計
|
||||
|
||||
```rust
|
||||
// src/api/middleware/auth.rs
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct AuthState {
|
||||
pub db: Arc<PostgresDb>,
|
||||
pub cache: Arc<RedisCache>,
|
||||
}
|
||||
|
||||
pub async fn auth_middleware(
|
||||
req: Request,
|
||||
next: Next,
|
||||
state: AuthState,
|
||||
) -> Result<Response, StatusCode> {
|
||||
// 1. 從 Header 提取 API Key
|
||||
// Header: X-API-Key: muser_xxx
|
||||
// 或: Authorization: Bearer muser_xxx
|
||||
|
||||
// 2. 驗證並取得 user_id
|
||||
let user_id = validate_api_key(&req, &state).await?;
|
||||
|
||||
// 3. 附加到 request extensions
|
||||
req.extensions_mut().insert(UserContext { user_id });
|
||||
|
||||
// 4. 執行 handler
|
||||
next.call(req).await
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct UserContext {
|
||||
pub user_id: i64,
|
||||
}
|
||||
```
|
||||
|
||||
#### 3.2 API Key 格式更新
|
||||
|
||||
```
|
||||
新格式: muser_{uuid}_{timestamp}_{random}_{user_id_hash}
|
||||
```
|
||||
|
||||
| 欄位 | 說明 |
|
||||
|------|------|
|
||||
| 前綴 | `muser_` = User 類型 |
|
||||
| uuid | 唯一識別碼 |
|
||||
| timestamp | 創建時間戳 |
|
||||
| random | 隨機字串 |
|
||||
| user_id_hash | 壓縮的 user_id |
|
||||
|
||||
---
|
||||
|
||||
### Phase 4: 更新 Register API
|
||||
|
||||
#### 4.1 修改 register 端點
|
||||
|
||||
```rust
|
||||
// POST /api/v1/register
|
||||
pub async fn register(
|
||||
State(state): State<ApiState>,
|
||||
Json(req): Json<RegisterRequest>,
|
||||
Extension(ctx): Extension<UserContext>, // 新增
|
||||
) -> Result<Json<RegisterResponse>, StatusCode> {
|
||||
// ... 現有邏輯 ...
|
||||
|
||||
// 驗證用戶配額
|
||||
let user = state.db.get_user(ctx.user_id).await?;
|
||||
if user.quota_used + file_size > user.quota_size {
|
||||
return Err(StatusCode::FORBIDDEN);
|
||||
}
|
||||
|
||||
// 關聯 user_id 到影片
|
||||
let video_uuid = state.db.create_video(req, Some(ctx.user_id)).await?;
|
||||
|
||||
// 建立 processing job(帶 user_id)
|
||||
state.db.create_monitor_job(
|
||||
job_type: "auto_ingestion",
|
||||
video_uuid,
|
||||
user_id: Some(ctx.user_id),
|
||||
processors: vec!["asr", "cut", "yolo", "ocr", "face", "pose"],
|
||||
).await?;
|
||||
|
||||
Ok(Json(RegisterResponse { uuid: video_uuid }))
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Phase 5: n8n 自動化流程
|
||||
|
||||
#### 5.1 用戶註冊 Workflow
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ WordPress 用戶註冊自動化流程 │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ Trigger: Webhook (或 WordPress Plugin) │
|
||||
│ │
|
||||
│ Step 1: 驗證管理員權限 │
|
||||
│ └─► 檢查 WordPress REST API 憑證 │
|
||||
│ │
|
||||
│ Step 2: 在 Momentry Core 建立用戶記錄 │
|
||||
│ └─► POST /api/v1/admin/users │
|
||||
│ └─► 產生 API Key │
|
||||
│ │
|
||||
│ Step 3: 在 SFTPGo 建立用戶 │
|
||||
│ └─► POST /api/v2/users (SFTPGo API) │
|
||||
│ └─► 設定 home_dir: /data/{username} │
|
||||
│ │
|
||||
│ Step 4: 更新用戶記錄 │
|
||||
│ └─► 關聯 sftpgo_username │
|
||||
│ │
|
||||
│ Step 5: 發送歡迎 email │
|
||||
│ └─► 包含 SFTP 登入資訊 │
|
||||
│ └─► 包含 API Key │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
#### 5.2 SFTPGo Hook 更新
|
||||
|
||||
```bash
|
||||
# /Users/accusys/sftpgo_test/register_hook.sh
|
||||
|
||||
# 修改為傳遞 user_id
|
||||
curl -X POST "http://localhost:3002/api/v1/register" \
|
||||
-H "X-API-Key: ${SFTPGO_USER_API_KEY}" \
|
||||
-H "X-SFTPGo-User: ${SFTPGO_USERNAME}" \
|
||||
-d "{\"path\": \"${SFTPGO_FILE_PATH}\"}"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. 實作優先順序
|
||||
|
||||
| Phase | 任務 | 複雜度 | 優先級 | 預估工時 |
|
||||
|-------|------|--------|--------|----------|
|
||||
| 1.1 | 測試 WordPress Application Passwords | 低 | P0 | 1h |
|
||||
| 1.2 | 為 WordPress 產生 Application Password | 低 | P0 | 0.5h |
|
||||
| 2.1 | 建立 users 表 migration | 中 | P0 | 2h |
|
||||
| 2.2 | 更新 videos, monitor_jobs 表 | 低 | P0 | 1h |
|
||||
| 3.1 | 實作 API auth middleware | 中 | P0 | 4h |
|
||||
| 3.2 | 更新 register API 接受 user_id | 低 | P0 | 2h |
|
||||
| 4 | 建立 admin users API | 中 | P1 | 4h |
|
||||
| 5.1 | 建立 n8n 用戶註冊 workflow | 中 | P1 | 6h |
|
||||
| 5.2 | 更新 SFTPGo hook | 低 | P1 | 2h |
|
||||
| 6 | 實作配額管理 | 中 | P2 | 4h |
|
||||
| 7 | 測試與驗證 | 中 | P2 | 4h |
|
||||
|
||||
**總預估工時**: ~30.5h
|
||||
|
||||
---
|
||||
|
||||
## 5. 待確認事項
|
||||
|
||||
### 5.1 WordPress 用戶建立方式
|
||||
|
||||
- [ ] 手動在 wp-admin 建立?還是透過 Elementor 表單?
|
||||
- [ ] 是否需要 email 驗證?
|
||||
- [ ] 初始角色設定(subscriber / contributor)?
|
||||
|
||||
### 5.2 API Key 格式
|
||||
|
||||
- [ ] 維持現有 `muser_` 前綴格式?
|
||||
- [ ] 還是建立新的用戶專用 key 格式?
|
||||
- [ ] 是否需要 JWT token?
|
||||
|
||||
### 5.3 SFTPGo 整合
|
||||
|
||||
- [ ] 每個 WordPress 用戶對應一個 SFTPGo 用戶?
|
||||
- [ ] home_dir 命名規則?(如 `data/{wordpress_username}`)
|
||||
- [ ] SFTPGo 配額是否同步?
|
||||
|
||||
### 5.4 配額管理
|
||||
|
||||
- [ ] 每人預設 10GB 空間?
|
||||
- [ ] 超出配額如何處理?(阻止上傳 / 警告)
|
||||
- [ ] 配額用完後是否暫停 SFTPGo 用戶?
|
||||
|
||||
### 5.5 資料同步
|
||||
|
||||
- [ ] WordPress 用戶刪除時是否同步刪除其他系統?
|
||||
- [ ] 用戶停權時的處理流程?
|
||||
|
||||
---
|
||||
|
||||
## 6. 參考文件
|
||||
|
||||
### 內部文件
|
||||
|
||||
| 文件 | 用途 |
|
||||
|------|------|
|
||||
| `docs/PENDING_ISSUES.md` | 待解決問題追蹤 |
|
||||
| `docs/API_KEY_MANAGEMENT.md` | API Key 管理系統 |
|
||||
| `docs/API_REFERENCE.md` | API 端點參考 |
|
||||
| `docs/SFTPGO_DEMO_USER.md` | SFTPGo 用戶設定 |
|
||||
| `docs/N8N_INTEGRATION_GUIDE.md` | n8n 整合指南 |
|
||||
| `docs/INSTALL_WORDPRESS.md` | WordPress 安裝指南 |
|
||||
|
||||
### 外部資源
|
||||
|
||||
| 資源 | URL |
|
||||
|------|-----|
|
||||
| WordPress REST API | https://developer.wordpress.org/rest-api/ |
|
||||
| WordPress Application Passwords | https://developer.wordpress.org/rest-api/using-the-rest-api/authentication/#authentication-plugins |
|
||||
| SFTPGo REST API | https://docs.sftpgo.com/latest/rest-api/ |
|
||||
|
||||
---
|
||||
|
||||
## 7. 附錄
|
||||
|
||||
### A. 現有使用者資料
|
||||
|
||||
#### WordPress (wp_users)
|
||||
| ID | user_login | user_email | display_name |
|
||||
|----|------------|------------|--------------|
|
||||
| 1 | wp_user | marketing@accusys.com.tw | wp_user |
|
||||
| 2 | sc_demo | susan.cheng@accusys.com.tw | Susan Cheng |
|
||||
|
||||
#### SFTPGo (users)
|
||||
| username | email | home_dir | status |
|
||||
|----------|-------|----------|--------|
|
||||
| demo | demo@momentry.local | /Users/accusys/momentry/var/sftpgo/data/demo | Active |
|
||||
| warren | warren@momentry.local | /Users/accusys/momentry/var/sftpgo/data/warren | Active |
|
||||
| momentry | system@momentry.local | /Users/accusys/momentry/var/sftpgo/data/momentry | Active |
|
||||
|
||||
### B. 服務端口
|
||||
|
||||
| 服務 | Port | URL |
|
||||
|------|------|-----|
|
||||
| WordPress | 9000 (PHP-FPM) | https://wp.momentry.ddns.net |
|
||||
| SFTPGo | 8080 | http://localhost:8080 |
|
||||
| Momentry API | 3002 | http://localhost:3002 |
|
||||
| n8n | 5678 | http://localhost:5678 |
|
||||
@@ -1,481 +0,0 @@
|
||||
# Momentry 使用手冊
|
||||
|
||||
| 項目 | 內容 |
|
||||
|------|------|
|
||||
| 建立者 | Warren |
|
||||
| 建立時間 | 2026-03-21 |
|
||||
| 文件版本 | V1.0 |
|
||||
|
||||
---
|
||||
|
||||
## 版本歷史
|
||||
|
||||
| 版本 | 日期 | 目的 | 操作人 | 工具/模型 |
|
||||
|------|------|------|--------|-----------|
|
||||
| V1.0 | 2026-03-21 | 創建使用手冊 | Warren | OpenCode |
|
||||
|
||||
---
|
||||
|
||||
**目標讀者**: 系統管理員、開發者
|
||||
|
||||
---
|
||||
|
||||
## 目錄
|
||||
|
||||
1. [快速開始](#1-快速開始)
|
||||
2. [安裝與設定](#2-安裝與設定)
|
||||
3. [CLI 命令參考](#3-cli-命令參考)
|
||||
4. [影片管理](#4-影片管理)
|
||||
5. [API Key 管理](#5-api-key-管理)
|
||||
6. [第三方整合](#6-第三方整合)
|
||||
7. [監控與維護](#7-監控與維護)
|
||||
8. [疑難排解](#8-疑難排解)
|
||||
|
||||
---
|
||||
|
||||
## 1. 快速開始
|
||||
|
||||
### 1.1 最小啟動流程
|
||||
|
||||
```bash
|
||||
# 1. 啟動服務
|
||||
sudo launchctl load /Library/LaunchDaemons/com.momentry.postgresql.plist
|
||||
sudo launchctl load /Library/LaunchDaemons/com.momentry.redis.plist
|
||||
|
||||
# 2. 設定環境變數
|
||||
source .env
|
||||
|
||||
# 3. 啟動 API 伺服器
|
||||
momentry server --host 127.0.0.1 --port 3000
|
||||
|
||||
# 4. 建立 API Key
|
||||
momentry api-key create my-first-key --key-type user --ttl 90
|
||||
```
|
||||
|
||||
### 1.2 驗證安裝
|
||||
|
||||
```bash
|
||||
# 檢查系統狀態
|
||||
curl http://localhost:3002/health
|
||||
|
||||
# 檢查版本
|
||||
momentry --help
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. 安裝與設定
|
||||
|
||||
### 2.1 環境需求
|
||||
|
||||
| 項目 | 需求 |
|
||||
|------|------|
|
||||
| 作業系統 | macOS (Apple Silicon) |
|
||||
| Rust | 1.70+ |
|
||||
| PostgreSQL | 15+ |
|
||||
| Redis | 7+ |
|
||||
| Python | 3.11+ (用於 AI 處理) |
|
||||
|
||||
### 2.2 安裝步驟
|
||||
|
||||
```bash
|
||||
# 1. 複製專案
|
||||
git clone <repository-url>
|
||||
cd momentry_core_0.1
|
||||
|
||||
# 2. 編譯
|
||||
cargo build --release
|
||||
|
||||
# 3. 安裝到系統
|
||||
cp target/release/momentry /usr/local/bin/
|
||||
```
|
||||
|
||||
### 2.3 環境變數設定
|
||||
|
||||
建立 `.env` 檔案:
|
||||
|
||||
```bash
|
||||
# Database
|
||||
DATABASE_URL=postgres://accusys@localhost:5432/momentry
|
||||
|
||||
# Redis
|
||||
REDIS_URL=redis://:accusys@localhost:6379
|
||||
|
||||
# Gitea (選用)
|
||||
GITEA_URL=http://localhost:3000
|
||||
|
||||
# n8n (選用)
|
||||
N8N_URL=https://n8n.momentry.ddns.net
|
||||
|
||||
# API Server
|
||||
API_HOST=127.0.0.1
|
||||
API_PORT=3000
|
||||
|
||||
# 監控目錄
|
||||
WATCH_DIRECTORIES=~/Videos
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. CLI 命令參考
|
||||
|
||||
### 3.1 一般命令
|
||||
|
||||
| 命令 | 說明 |
|
||||
|------|------|
|
||||
| `momentry --help` | 顯示幫助 |
|
||||
| `momentry server` | 啟動 API 伺服器 |
|
||||
| `momentry register <path>` | 註冊影片 |
|
||||
| `momentry process <uuid>` | 處理影片 |
|
||||
| `momentry query <text>` | RAG 查詢 |
|
||||
|
||||
### 3.2 影片管理
|
||||
|
||||
```bash
|
||||
# 註冊影片
|
||||
momentry register /path/to/video.mp4
|
||||
|
||||
# 處理影片
|
||||
momentry process <uuid>
|
||||
|
||||
# 生成縮圖
|
||||
momentry thumbnails <uuid> --count 6
|
||||
|
||||
# 查看狀態
|
||||
momentry status <uuid>
|
||||
```
|
||||
|
||||
### 3.3 API Key 管理
|
||||
|
||||
```bash
|
||||
# 建立 Key
|
||||
momentry api-key create <name> --key-type <type> --ttl <days>
|
||||
|
||||
# 列出 Keys
|
||||
momentry api-key list
|
||||
|
||||
# 驗證 Key
|
||||
momentry api-key validate --key <key>
|
||||
|
||||
# 撤銷 Key
|
||||
momentry api-key revoke --key <key>
|
||||
|
||||
# 請求輪換
|
||||
momentry api-key rotate --key <key>
|
||||
|
||||
# 統計資訊
|
||||
momentry api-key stats
|
||||
```
|
||||
|
||||
### 3.4 Gitea 整合
|
||||
|
||||
```bash
|
||||
# 建立 Token
|
||||
momentry gitea create \
|
||||
--username <user> \
|
||||
--password <pwd> \
|
||||
--token-name <name> \
|
||||
--scopes "read:repository,write:repository"
|
||||
|
||||
# 列出 Tokens
|
||||
momentry gitea list --username <user> --password <pwd>
|
||||
|
||||
# 刪除 Token
|
||||
momentry gitea delete \
|
||||
--username <user> \
|
||||
--password <pwd> \
|
||||
--token-name <name>
|
||||
```
|
||||
|
||||
### 3.5 n8n 整合
|
||||
|
||||
```bash
|
||||
# 建立 API Key
|
||||
momentry n8n create \
|
||||
--api-key <existing-key> \
|
||||
--label <name> \
|
||||
--expires-in-days 90
|
||||
|
||||
# 列出 Keys
|
||||
momentry n8n list --api-key <key>
|
||||
|
||||
# 刪除 Key
|
||||
momentry n8n delete --api-key <key> --label <name>
|
||||
```
|
||||
|
||||
### 3.6 備份管理
|
||||
|
||||
```bash
|
||||
# 列出備份
|
||||
momentry backup list
|
||||
|
||||
# 清理舊備份
|
||||
momentry backup cleanup --days 30
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. 影片管理
|
||||
|
||||
### 4.1 影片生命週期
|
||||
|
||||
```
|
||||
上傳 → 註冊 → 處理 → 儲存 → 查詢
|
||||
│ │ │ │ │
|
||||
▼ ▼ ▼ ▼ ▼
|
||||
檔案 資料庫 AI分析 向量庫 RAG
|
||||
```
|
||||
|
||||
### 4.2 註冊影片
|
||||
|
||||
```bash
|
||||
# 自動偵測格式
|
||||
momentry register ~/Videos/my-video.mp4
|
||||
|
||||
# 輸出:
|
||||
# UUID: a1b2c3d4e5f6g7h8
|
||||
# Duration: 120.5s
|
||||
# Resolution: 1920x1080
|
||||
```
|
||||
|
||||
### 4.3 處理流程
|
||||
|
||||
處理包含以下階段:
|
||||
|
||||
| 階段 | 說明 | 時間 (約) |
|
||||
|------|------|-----------|
|
||||
| Probe | 影片資訊分析 | 5s |
|
||||
| ASR | 語音辨識 | 視長度 |
|
||||
| OCR | 文字辨識 | 視長度 |
|
||||
| YOLO | 物件偵測 | 視長度 |
|
||||
| Cut | 場景切割 | 30s |
|
||||
| Chunk | 內容分段 | 10s |
|
||||
| Vector | 向量化 | 20s |
|
||||
|
||||
### 4.4 查詢影片
|
||||
|
||||
```bash
|
||||
# RAG 查詢
|
||||
momentry query "影片中有什麼內容?"
|
||||
|
||||
# 取得特定影片
|
||||
momentry resolve <uuid>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. API Key 管理
|
||||
|
||||
### 5.1 Key 類型
|
||||
|
||||
| 類型 | 前綴 | 用途 | 預設 TTL |
|
||||
|------|------|------|----------|
|
||||
| System | `msys_` | 系統內部 | 365 天 |
|
||||
| User | `muser_` | 個人用戶 | 90 天 |
|
||||
| Service | `msvc_` | 服務間通訊 | 180 天 |
|
||||
| Integration | `mint_` | 第三方整合 | 30 天 |
|
||||
| Emergency | `memg_` | 緊急存取 | 1 天 |
|
||||
|
||||
### 5.2 建立 Key
|
||||
|
||||
```bash
|
||||
# 一般 Key
|
||||
momentry api-key create my-service --key-type service --ttl 90
|
||||
|
||||
# 緊急 Key (24小時有效)
|
||||
momentry api-key create emergency-access --key-type emergency
|
||||
|
||||
# 輸出:
|
||||
# ✅ API Key created successfully!
|
||||
# Key ID: msvc_xxxxxxxx
|
||||
# API Key: msvc_xxxxxxxx_xxxxx_xxxxx
|
||||
# Expires: 2026-06-19
|
||||
```
|
||||
|
||||
### 5.3 Key 生命週期
|
||||
|
||||
```
|
||||
建立 → 使用 → 過期/撤銷 → 清理
|
||||
│ │ │ │
|
||||
▼ ▼ ▼ ▼
|
||||
資料庫 驗證 停用 定期刪除
|
||||
```
|
||||
|
||||
### 5.4 安全建議
|
||||
|
||||
| 建議 | 說明 |
|
||||
|------|------|
|
||||
| 定期輪換 | 每 90 天更新 Key |
|
||||
| 最小權限 | 只授予必要權限 |
|
||||
| 監控使用 | 定期檢查使用統計 |
|
||||
| 及時撤銷 | 異常時立即撤銷 |
|
||||
|
||||
---
|
||||
|
||||
## 6. 第三方整合
|
||||
|
||||
### 6.1 Gitea
|
||||
|
||||
```bash
|
||||
# 建立 CI/CD 用 Token
|
||||
momentry gitea create \
|
||||
--username admin \
|
||||
--password "your-password" \
|
||||
--token-name "ci-pipeline" \
|
||||
--scopes "read:repository,write:repository"
|
||||
|
||||
# 在 CI 中使用
|
||||
export GITEA_TOKEN="token-sha1-value"
|
||||
curl -H "Authorization: token $GITEA_TOKEN" \
|
||||
http://localhost:3000/api/v1/user
|
||||
```
|
||||
|
||||
### 6.2 n8n
|
||||
|
||||
```bash
|
||||
# 建立工作流用 Key
|
||||
momentry n8n create \
|
||||
--api-key "existing-n8n-key" \
|
||||
--label "workflow-key" \
|
||||
--expires-in-days 90
|
||||
|
||||
# 在 n8n 中使用
|
||||
# HTTP Request Header: X-N8N-API-Key: <key>
|
||||
```
|
||||
|
||||
### 6.3 Webhook 通知
|
||||
|
||||
```bash
|
||||
# 設定 Webhook
|
||||
export WEBHOOK_URL="https://n8n.example.com/webhook/alerts"
|
||||
export WEBHOOK_EVENTS="anomaly_detected,key_expired"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. 監控與維護
|
||||
|
||||
### 7.1 系統監控
|
||||
|
||||
```bash
|
||||
# 檢查服務狀態
|
||||
ps aux | grep momentry
|
||||
ps aux | grep postgres
|
||||
redis-cli -a accusys ping
|
||||
|
||||
# 檢查日誌
|
||||
tail -f /Users/accusys/momentry/log/momentry.log
|
||||
tail -f /Users/accusys/momentry/log/redis.log
|
||||
```
|
||||
|
||||
### 7.2 資料庫維護
|
||||
|
||||
```bash
|
||||
# 檢查資料庫大小
|
||||
psql -U accusys -d momentry -c "SELECT pg_size_pretty(pg_database_size('momentry'));"
|
||||
|
||||
# 清理過期記錄
|
||||
momentry api-key stats # 檢查統計
|
||||
# 定期清理由系統自動執行
|
||||
```
|
||||
|
||||
### 7.3 備份
|
||||
|
||||
```bash
|
||||
# 手動備份 PostgreSQL
|
||||
pg_dump -U accusys momentry > backup_$(date +%Y%m%d).sql
|
||||
|
||||
# 恢復備份
|
||||
psql -U accusys momentry < backup_20260321.sql
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. 疑難排解
|
||||
|
||||
### 8.1 常見問題
|
||||
|
||||
#### Q: 無法連接資料庫
|
||||
|
||||
```bash
|
||||
# 檢查 PostgreSQL 狀態
|
||||
pg_isready -h localhost -p 5432
|
||||
|
||||
# 檢查連線
|
||||
psql -U accusys -d momentry -c "SELECT 1;"
|
||||
```
|
||||
|
||||
#### Q: Redis 連線失敗
|
||||
|
||||
```bash
|
||||
# 檢查 Redis 狀態
|
||||
redis-cli -a accusys ping
|
||||
|
||||
# 檢查認證
|
||||
redis-cli -a accusys INFO server | grep redis_version
|
||||
```
|
||||
|
||||
#### Q: API Key 驗證失敗
|
||||
|
||||
```bash
|
||||
# 檢查 Key 狀態
|
||||
momentry api-key validate --key "your-key"
|
||||
|
||||
# 檢查是否過期
|
||||
momentry api-key list
|
||||
```
|
||||
|
||||
### 8.2 錯誤碼對照
|
||||
|
||||
| 錯誤碼 | 說明 | 解決方式 |
|
||||
|--------|------|----------|
|
||||
| `E001` | 資料庫連線失敗 | 檢查 PostgreSQL |
|
||||
| `E002` | Redis 連線失敗 | 檢查 Redis |
|
||||
| `E003` | API Key 無效 | 重新建立 Key |
|
||||
| `E004` | 影片不存在 | 檢查 UUID |
|
||||
| `E005` | 處理失敗 | 檢查日誌 |
|
||||
|
||||
### 8.3 日誌位置
|
||||
|
||||
| 日誌 | 路徑 |
|
||||
|------|------|
|
||||
| Momentry | `/Users/accusys/momentry/log/momentry.log` |
|
||||
| PostgreSQL | `/Users/accusys/momentry/log/postgresql.log` |
|
||||
| Redis | `/Users/accusys/momentry/log/redis.log` |
|
||||
| Gitea | `/Users/accusys/momentry/log/gitea.log` |
|
||||
|
||||
---
|
||||
|
||||
## 附錄
|
||||
|
||||
### A. 完整命令列表
|
||||
|
||||
```bash
|
||||
momentry --help
|
||||
momentry register --help
|
||||
momentry process --help
|
||||
momentry api-key --help
|
||||
momentry gitea --help
|
||||
momentry n8n --help
|
||||
momentry backup --help
|
||||
```
|
||||
|
||||
### B. 環境變數總覽
|
||||
|
||||
| 變數 | 預設值 | 說明 |
|
||||
|------|--------|------|
|
||||
| `DATABASE_URL` | `postgres://accusys@localhost:5432/momentry` | PostgreSQL |
|
||||
| `REDIS_URL` | `redis://:accusys@localhost:6379` | Redis |
|
||||
| `GITEA_URL` | `http://localhost:3000` | Gitea |
|
||||
| `N8N_URL` | `https://n8n.momentry.ddns.net` | n8n |
|
||||
| `API_HOST` | `127.0.0.1` | API 主機 |
|
||||
| `API_PORT` | `3000` | API 埠號 |
|
||||
|
||||
### C. 相關文件
|
||||
|
||||
| 文件 | 說明 |
|
||||
|------|------|
|
||||
| `docs/API_CURL_EXAMPLES.md` | API curl 範例 |
|
||||
| `docs/N8N_INTEGRATION_GUIDE.md` | n8n 整合指南 |
|
||||
| `docs/API_KEY_MANAGEMENT.md` | API Key 設計 |
|
||||
| `CHANGELOG.md` | 版本記錄 |
|
||||
@@ -1,261 +0,0 @@
|
||||
# Momentry Core 版本管理規範
|
||||
|
||||
| 項目 | 內容 |
|
||||
|------|------|
|
||||
| 建立者 | Warren |
|
||||
| 建立時間 | 2026-03-23 |
|
||||
| 文件版本 | V1.0 |
|
||||
|
||||
---
|
||||
|
||||
## 版本歷史
|
||||
|
||||
| 版本 | 日期 | 目的 | 操作人 | 工具/模型 |
|
||||
|------|------|------|--------|-----------|
|
||||
| V1.0 | 2026-03-23 | 創建版本管理規範 | Warren | OpenCode |
|
||||
|
||||
---
|
||||
|
||||
## 1. 版本與通訊埠對照表
|
||||
|
||||
| 版本 | Binary | Port | Redis Prefix | 用途 |
|
||||
|------|--------|------|--------------|------|
|
||||
| **Production** | `momentry` | **3002** | `momentry:` | 正式環境 |
|
||||
| **Development** | `momentry_playground` | **3003** | `momentry_dev:` | 開發測試 |
|
||||
|
||||
### 通訊埠嚴禁事項
|
||||
- ❌ 開發版嚴禁使用 3002
|
||||
- ❌ 任何 `cargo run` 直接啟動的 server 嚴禁綁定 3002
|
||||
- ❌ Debug build 嚴禁部署到 3002
|
||||
|
||||
---
|
||||
|
||||
## 2. 開發環境隔離原則
|
||||
|
||||
### 2.1 開發流程
|
||||
```bash
|
||||
# 永遠在 3003 開發測試
|
||||
cd /Users/accusys/momentry_core_0.1
|
||||
|
||||
# 開發版啟動 (3003)
|
||||
cargo run --bin momentry_playground -- server
|
||||
# 或
|
||||
cargo run --bin momentry -- server --port 3003
|
||||
```
|
||||
|
||||
### 2.2 測試完成後
|
||||
1. 確認所有功能在 3003 正常運作
|
||||
2. 進行 `cargo clippy --lib` 檢查
|
||||
3. 進行 `cargo test --lib` 測試
|
||||
4. 確認後才能進行 release
|
||||
|
||||
### 2.3 環境變數隔離
|
||||
```bash
|
||||
# Development
|
||||
export MOMENTRY_SERVER_PORT=3003
|
||||
export MOMENTRY_REDIS_PREFIX=momentry_dev:
|
||||
|
||||
# Production (launchd 管理,勿手動設定)
|
||||
# MOMENTRY_SERVER_PORT=3002
|
||||
# MOMENTRY_REDIS_PREFIX=momentry:
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. Release 版本管理
|
||||
|
||||
### 3.1 Release 前檢查清單
|
||||
|
||||
```
|
||||
□ 開發版 (3003) 功能測試完成
|
||||
□ cargo clippy --lib 通過
|
||||
□ cargo test --lib 通過
|
||||
□ cargo fmt -- --check 通過
|
||||
□ 所有修改已 commit 到 Gitea
|
||||
```
|
||||
|
||||
### 3.2 Release 流程
|
||||
|
||||
```bash
|
||||
# 1. 確保目前是乾淨的工作目錄
|
||||
git status
|
||||
|
||||
# 2. 備份當前 production binary
|
||||
BACKUP_DIR="/Users/accusys/momentry/backup/bin"
|
||||
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
|
||||
cp /Users/accusys/momentry/bin/momentry "${BACKUP_DIR}/momentry_${TIMESTAMP}"
|
||||
|
||||
# 3. 停止 production server
|
||||
sudo launchctl unload /Library/LaunchDaemons/com.momentry.api.plist
|
||||
# 或
|
||||
pkill -f "target/release/momentry server"
|
||||
|
||||
# 4. 編譯 release 版本
|
||||
cargo build --release --bin momentry
|
||||
|
||||
# 5. 部署到正式位置
|
||||
cp target/release/momentry /Users/accusys/momentry/bin/momentry
|
||||
|
||||
# 6. 啟動 production server
|
||||
sudo launchctl load /Library/LaunchDaemons/com.momentry.api.plist
|
||||
|
||||
# 7. 驗證
|
||||
curl http://localhost:3002/health
|
||||
```
|
||||
|
||||
### 3.3 Backup 存放位置
|
||||
```
|
||||
/Users/accusys/momentry/backup/bin/
|
||||
├── momentry_20260325_143000 (backup)
|
||||
├── momentry_20260324_100000
|
||||
├── momentry_20260323_090000
|
||||
└── ...
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Gitea 版本控制
|
||||
|
||||
### 4.1 Commit 規範
|
||||
```
|
||||
feat: 新功能
|
||||
fix: 錯誤修復
|
||||
refactor: 重構
|
||||
docs: 文件更新
|
||||
chore: 杂项
|
||||
test: 测试
|
||||
```
|
||||
|
||||
### 4.2 Release Tag 規範
|
||||
```bash
|
||||
# 建立 release tag
|
||||
git tag -a v0.1.1 -m "Release v0.1.1 - API Key Authentication"
|
||||
git push origin v0.1.1
|
||||
```
|
||||
|
||||
### 4.3 版本號命名
|
||||
```
|
||||
v{major}.{minor}.{patch}
|
||||
│ │ └── Patch version (bug fix)
|
||||
│ └───────── Minor version (新功能)
|
||||
└──────────────── Major version (破壞性變更)
|
||||
```
|
||||
|
||||
### 4.4 Gitea Release 建立
|
||||
1. 在 Gitea Repo > Releases > New Release
|
||||
2. 選擇對應的 Tag
|
||||
3. 填寫 Release Notes
|
||||
4. 上傳 compiled binary(如需要)
|
||||
|
||||
---
|
||||
|
||||
## 5. 服務管理
|
||||
|
||||
### 5.1 Production Service (launchd)
|
||||
```bash
|
||||
# Plist 位置
|
||||
/Library/LaunchDaemons/com.momentry.api.plist
|
||||
|
||||
# 管理指令
|
||||
sudo launchctl load /Library/LaunchDaemons/com.momentry.api.plist # 啟動
|
||||
sudo launchctl unload /Library/LaunchDaemons/com.momentry.api.plist # 停止
|
||||
sudo launchctl list | grep momentry # 狀態
|
||||
```
|
||||
|
||||
### 5.2 緊急回滾
|
||||
```bash
|
||||
# 1. 停止當前服務
|
||||
sudo launchctl unload /Library/LaunchDaemons/com.momentry.api.plist
|
||||
|
||||
# 2. 恢復上一個 backup
|
||||
BACKUP_FILE=$(ls -t /Users/accusys/momentry/backup/bin/ | head -1)
|
||||
cp "/Users/accusys/momentry/backup/bin/${BACKUP_FILE}" /Users/accusys/momentry/bin/momentry
|
||||
|
||||
# 3. 重啟服務
|
||||
sudo launchctl load /Library/LaunchDaemons/com.momentry.api.plist
|
||||
|
||||
# 4. 驗證
|
||||
curl http://localhost:3002/health
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. 快速參考卡片
|
||||
|
||||
### Development
|
||||
```bash
|
||||
# 啟動開發版
|
||||
cd /Users/accusys/momentry_core_0.1
|
||||
cargo run --bin momentry_playground -- server
|
||||
|
||||
# 或手動指定 port
|
||||
cargo run --bin momentry -- server --port 3003
|
||||
|
||||
# 測試端點
|
||||
curl -H "X-API-Key: muser_68600856036340bcafc01930eb4bd839_1774418104_97221b69" \
|
||||
http://localhost:3003/api/v1/jobs
|
||||
```
|
||||
|
||||
### Production
|
||||
```bash
|
||||
# 查看狀態
|
||||
sudo launchctl list | grep momentry
|
||||
|
||||
# 重啟服務
|
||||
sudo launchctl unload /Library/LaunchDaemons/com.momentry.api.plist
|
||||
sudo launchctl load /Library/LaunchDaemons/com.momentry.api.plist
|
||||
|
||||
# 查看日誌
|
||||
tail -f /Users/accusys/momentry/log/momentry_release.log
|
||||
```
|
||||
|
||||
### 常用指令
|
||||
```bash
|
||||
# 檢查 port 使用
|
||||
lsof -i :3002 # Production
|
||||
lsof -i :3003 # Development
|
||||
|
||||
# 檢查 process
|
||||
ps aux | grep momentry | grep server
|
||||
|
||||
# 停止所有 momentry server
|
||||
pkill -9 -f "momentry.*server"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. 禁止事項
|
||||
|
||||
| 項目 | 說明 |
|
||||
|------|------|
|
||||
| ❌ 禁止在 3002 測試 | 3002 是 Production,嚴禁用於測試 |
|
||||
| ❌ 禁止覆蓋 production binary | 使用 backup + deploy 流程 |
|
||||
| ❌ 禁止跳過測試直接 release | 必須完成檢查清單 |
|
||||
| ❌ 禁止在未備份的情況下部署 | 每次部署前必須備份 |
|
||||
|
||||
---
|
||||
|
||||
## 8. 疑難排解
|
||||
|
||||
### Q: 3002 無法綁定怎麼辦?
|
||||
```bash
|
||||
# 檢查誰在使用
|
||||
lsof -i :3002
|
||||
|
||||
# 停止舊的 server
|
||||
pkill -9 -f "momentry.*server"
|
||||
```
|
||||
|
||||
### Q: 如何確認使用的是哪個版本?
|
||||
```bash
|
||||
# 檢查 binary 位置和版本
|
||||
file $(which momentry)
|
||||
./target/release/momentry --version 2>/dev/null || echo "No version flag"
|
||||
```
|
||||
|
||||
### Q: 如何確認有沒有 API Key 驗證?
|
||||
```bash
|
||||
# 沒有 API Key 應該返回 401
|
||||
curl -s http://localhost:3002/api/v1/jobs
|
||||
# HTTP/1.1 401 Unauthorized
|
||||
```
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,246 +0,0 @@
|
||||
# Video Registration
|
||||
|
||||
| 項目 | 內容 |
|
||||
|------|------|
|
||||
| 建立者 | Warren |
|
||||
| 建立時間 | 2026-03-25 |
|
||||
| 文件版本 | V1.1 |
|
||||
|
||||
---
|
||||
|
||||
## 版本歷史
|
||||
|
||||
| 版本 | 日期 | 目的 | 操作人 | 工具/模型 |
|
||||
|------|------|------|--------|-----------|
|
||||
| V1.0 | 2026-03-25 | 創建文件 | Warren | OpenCode |
|
||||
| V1.1 | 2026-03-26 | 修正 curl 範例,新增 API Key 驗證標頭 | OpenCode | deepseek-reasoner |
|
||||
|
||||
---
|
||||
|
||||
## 概述
|
||||
|
||||
影片註冊 API (`POST /api/v1/register`) 用於將影片加入 Momentry Core 系統進行處理。
|
||||
|
||||
## 路徑格式
|
||||
|
||||
### 支援的路徑格式
|
||||
|
||||
| 格式 | 範例 | 說明 |
|
||||
|------|------|------|
|
||||
| 相對路徑 | `./demo/video.mp4` | 推薦格式 |
|
||||
| 相對路徑(無 ./) | `demo/video.mp4` | 自動加上 `./` |
|
||||
| 絕對路徑 | `/Users/.../sftpgo/data/demo/video.mp4` | 支援但不推薦 |
|
||||
|
||||
### 路徑結構
|
||||
|
||||
```
|
||||
./username/filepath
|
||||
│ │ │
|
||||
│ │ └── 檔案路徑(可以是多層目錄)
|
||||
│ └── 使用者名稱(SFTPgo 用戶目錄名稱)
|
||||
└── 相對路徑前綴
|
||||
```
|
||||
|
||||
**範例**:
|
||||
- `./demo/video.mp4` → username=`demo`, filepath=`video.mp4`
|
||||
- `./demo/movies/2024/video.mp4` → username=`demo`, filepath=`movies/2024/video.mp4`
|
||||
- `./warren/project1/interview.mp4` → username=`warren`, filepath=`project1/interview.mp4`
|
||||
|
||||
## UUID 計算
|
||||
|
||||
### 計算規則
|
||||
|
||||
```
|
||||
UUID = SHA256(username/filepath)[0:16]
|
||||
```
|
||||
|
||||
**範例**:
|
||||
```rust
|
||||
// 路徑: ./demo/video.mp4
|
||||
// username: "demo"
|
||||
// filepath: "video.mp4"
|
||||
// key: "demo/video.mp4"
|
||||
// UUID: SHA256("demo/video.mp4")[0:16]
|
||||
```
|
||||
|
||||
### 特性
|
||||
|
||||
| 特性 | 說明 |
|
||||
|------|------|
|
||||
| 用戶隔離 | 不同用戶的相同檔名會產生不同 UUID |
|
||||
| 一致性 | 相同相對路徑一定產生相同 UUID |
|
||||
| 遷移安全 | SFTPgo 資料路徑變更後 UUID 保持一致 |
|
||||
|
||||
### 範例
|
||||
|
||||
```rust
|
||||
// 用戶 demo 的影片
|
||||
compute_uuid_from_relative_path("./demo/video.mp4")
|
||||
// → "9760d0820f0cf9a7"
|
||||
|
||||
// 用戶 warren 的相同檔名影片
|
||||
compute_uuid_from_relative_path("./warren/video.mp4")
|
||||
// → "a1b2c3d4e5f6g7h8" (不同的 UUID)
|
||||
```
|
||||
|
||||
## 重複註冊檢查
|
||||
|
||||
### 行為
|
||||
|
||||
1. 系統檢查 UUID 是否已存在於資料庫
|
||||
2. 如果存在,返回 `already_exists: true` 和現有影片資訊
|
||||
3. 如果不存在,創建新的影片記錄
|
||||
|
||||
### API 回應
|
||||
|
||||
**新註冊**:
|
||||
```json
|
||||
{
|
||||
"uuid": "9760d0820f0cf9a7",
|
||||
"video_id": 18,
|
||||
"job_id": 2,
|
||||
"file_name": "video.mp4",
|
||||
"duration": 159.637188,
|
||||
"width": 640,
|
||||
"height": 360,
|
||||
"already_exists": false
|
||||
}
|
||||
```
|
||||
|
||||
**重複註冊**:
|
||||
```json
|
||||
{
|
||||
"uuid": "9760d0820f0cf9a7",
|
||||
"video_id": 18,
|
||||
"job_id": 2,
|
||||
"file_name": "video.mp4",
|
||||
"duration": 159.637188,
|
||||
"width": 640,
|
||||
"height": 360,
|
||||
"already_exists": true
|
||||
}
|
||||
```
|
||||
|
||||
## SFTPgo 整合
|
||||
|
||||
### 目錄結構
|
||||
|
||||
SFTPgo 的用戶目錄結構:
|
||||
|
||||
```
|
||||
/Users/accusys/momentry/var/sftpgo/data/
|
||||
├── demo/ ← 用戶目錄
|
||||
│ ├── video.mp4
|
||||
│ └── movies/
|
||||
│ └── movie1.mp4
|
||||
├── warren/ ← 用戶目錄
|
||||
│ └── project1/
|
||||
│ └── interview.mp4
|
||||
└── momentry/ ← 用戶目錄
|
||||
└── presentation.mp4
|
||||
```
|
||||
|
||||
### 註冊流程
|
||||
|
||||
1. SFTPgo 用戶上傳檔案到各自的目錄
|
||||
2. n8n 或其他服務調用註冊 API
|
||||
3. 使用相對路徑格式:`./username/filepath`
|
||||
4. 系統計算 UUID 並檢查重複
|
||||
5. 創建處理任務
|
||||
|
||||
## 程式碼範例
|
||||
|
||||
### 註冊影片
|
||||
|
||||
```bash
|
||||
# 使用相對路徑註冊
|
||||
curl -X POST http://localhost:3002/api/v1/register \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "X-API-Key: YOUR_API_KEY" \
|
||||
-d '{"path": "./demo/video.mp4"}'
|
||||
|
||||
# 或使用多層目錄
|
||||
curl -X POST http://localhost:3002/api/v1/register \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "X-API-Key: YOUR_API_KEY" \
|
||||
-d '{"path": "./demo/movies/2024/video.mp4"}'
|
||||
```
|
||||
|
||||
### UUID 計算函數
|
||||
|
||||
```rust
|
||||
// 使用相對路徑計算 UUID
|
||||
pub fn compute_uuid_from_relative_path(relative_path: &str) -> String {
|
||||
let (username, filepath) = extract_user_from_relative_path(relative_path);
|
||||
compute_uuid(&username, &filepath)
|
||||
}
|
||||
|
||||
// 從相對路徑提取用戶名和檔案路徑
|
||||
pub fn extract_user_from_relative_path(relative_path: &str) -> (String, String) {
|
||||
let path = relative_path.strip_prefix("./").unwrap_or(relative_path);
|
||||
let path_buf = PathBuf::from(path);
|
||||
|
||||
let mut components = path_buf.components();
|
||||
let username = components
|
||||
.next()
|
||||
.map(|c| c.as_os_str().to_string_lossy().to_string())
|
||||
.unwrap_or_default();
|
||||
|
||||
let filepath: String = components
|
||||
.map(|c| c.as_os_str().to_string_lossy().to_string())
|
||||
.collect::<Vec<_>>()
|
||||
.join("/");
|
||||
|
||||
(username, filepath)
|
||||
}
|
||||
```
|
||||
|
||||
## 相關 API
|
||||
|
||||
### Probe API(僅探測,不註冊)
|
||||
|
||||
如果只需要取得影片資訊而不註冊,可以使用 Probe API:
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:3002/api/v1/probe \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "X-API-Key: YOUR_API_KEY" \
|
||||
-d '{"path": "./demo/video.mp4"}'
|
||||
```
|
||||
|
||||
**回應範例**:
|
||||
```json
|
||||
{
|
||||
"uuid": "a1b10138a6bbb0cd",
|
||||
"file_name": "video.mp4",
|
||||
"duration": 120.5,
|
||||
"width": 1920,
|
||||
"height": 1080,
|
||||
"fps": 30.0,
|
||||
"cached": false,
|
||||
"format": {...},
|
||||
"streams": [...]
|
||||
}
|
||||
```
|
||||
|
||||
**與 Register API 的差異**:
|
||||
|
||||
| 功能 | Probe API | Register API |
|
||||
|------|-----------|---------------|
|
||||
| 計算 UUID | ✓ | ✓ |
|
||||
| 執行 ffprobe | ✓ | ✓ |
|
||||
| 儲存 probe.json | ✓ | ✓ |
|
||||
| 寫入 videos 表 | ✗ | ✓ |
|
||||
| 建立 monitor_job | ✗ | ✓ |
|
||||
| 返回 job_id | ✗ | ✓ |
|
||||
| 適用場景 | 預覽影片資訊 | 註冊並處理影片 |
|
||||
|
||||
## 相關檔案
|
||||
|
||||
| 檔案 | 說明 |
|
||||
|------|------|
|
||||
| `src/core/storage/uuid.rs` | UUID 計算邏輯 |
|
||||
| `src/api/server.rs` | 註冊與 Probe API 實現 |
|
||||
| `src/core/probe/ffprobe.rs` | ffprobe 整合 |
|
||||
| `docs/SFTPGO_DEMO_USER.md` | SFTPgo 用戶設置 |
|
||||
| `docs/API_ENDPOINTS.md` | API 端點總覽 |
|
||||
@@ -1,102 +0,0 @@
|
||||
# YOLO Resume 功能整合規劃
|
||||
|
||||
## 現有資源
|
||||
|
||||
### 1. video_yolo_player 專案
|
||||
**位置**: `/Users/accusys/video_yolo_player/video_yolo_object_prescan.py`
|
||||
|
||||
**功能**:
|
||||
- ✅ Ctrl+C 暫停並保存進度
|
||||
- ✅ 自動從上一次繼續 (自動偵測 last_processed_frame)
|
||||
- ✅ 可配置自動保存間隔 (預設 30 秒)
|
||||
- ✅ 完整 metadata 追蹤 (處理時間、檢測數量等)
|
||||
- ✅ 互動式詢問是否繼續
|
||||
|
||||
### 2. momentry_core_0.1 專案
|
||||
**位置**: `/Users/accusys/momentry_core_0.1/scripts/yolo_processor.py`
|
||||
|
||||
## 整合狀態
|
||||
|
||||
| 項目 | 狀態 | 完成日期 |
|
||||
|------|------|----------|
|
||||
| Python script 整合 | ✅ 已完成 | 2026-03-22 |
|
||||
| Rust JSON 格式支援 | ✅ 已完成 | 2026-03-22 |
|
||||
| Auto-save 功能 (時間) | ✅ 已完成 | 2026-03-22 |
|
||||
| Auto-save 功能 (frame) | ✅ 已完成 | 2026-03-22 |
|
||||
| Ctrl+C 信號處理 | ✅ 已完成 | 2026-03-22 |
|
||||
| --force 參數 | ✅ 已完成 | 2026-03-22 |
|
||||
| --auto-save-frames 參數 | ✅ 已完成 | 2026-03-22 |
|
||||
|
||||
## 已實作功能
|
||||
|
||||
### momentry_core_0.1/scripts/yolo_processor.py
|
||||
|
||||
```python
|
||||
# 新增功能:
|
||||
def load_existing_data(output_file) # 載入現有資料
|
||||
def save_detection_data(output_file) # 保存進度
|
||||
def signal_handler(signum, frame) # Ctrl+C 處理
|
||||
|
||||
# 新增參數:
|
||||
--auto-save 30 # 自動保存間隔 (秒)
|
||||
--auto-save-frames 300 # 每 N frames 保存一次 (先到為準)
|
||||
--force # 強制從頭開始
|
||||
```
|
||||
|
||||
### 輸出格式
|
||||
|
||||
```json
|
||||
{
|
||||
"metadata": {
|
||||
"video_path": "...",
|
||||
"fps": 24.0,
|
||||
"total_frames": 1000,
|
||||
"status": "in_progress" | "completed" | "interrupted",
|
||||
"total_detections": 5000,
|
||||
"processing_time": 120.5,
|
||||
"auto_save_count": 4,
|
||||
"auto_save_interval": 30,
|
||||
"auto_save_frames": 300,
|
||||
"last_saved_frame": 12500,
|
||||
"last_saved_at": "ISO timestamp"
|
||||
},
|
||||
"frames": {
|
||||
"1": { "frame_number": 1, "time_seconds": 0.0, "detections": [...] },
|
||||
"2": { "frame_number": 2, "time_seconds": 0.042, "detections": [...] }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Auto-save 觸發條件
|
||||
|
||||
**滿足以下任一條件就會寫入磁碟:**
|
||||
1. 距離上次儲存已超過 `--auto-save` 秒數
|
||||
2. 距離上次儲存已處理超過 `--auto-save-frames` 個 frames
|
||||
|
||||
### 使用方式
|
||||
|
||||
```bash
|
||||
# 第一次執行 (預設: 30秒 或 300 frames)
|
||||
python yolo_processor.py video.mp4 output.json --uuid "abc123"
|
||||
|
||||
# 中斷後繼續 (自動偵測)
|
||||
python yolo_processor.py video.mp4 output.json --uuid "abc123"
|
||||
|
||||
# 強制從頭開始
|
||||
python yolo_processor.py video.mp4 output.json --force
|
||||
|
||||
# 自訂自動保存間隔
|
||||
python yolo_processor.py video.mp4 output.json --auto-save 10 --auto-save-frames 100
|
||||
```
|
||||
|
||||
### Rust 端支援
|
||||
|
||||
`check_json_completeness()` 已更新以支援新格式:
|
||||
- 讀取 `frames` dict 的最後一個 key 作為 `last_processed_frame`
|
||||
- 檢查 `metadata.status` 判斷是否完成
|
||||
|
||||
## 待測試項目
|
||||
|
||||
- [ ] 實際執行中斷後繼續
|
||||
- [ ] 驗證 large video (如 Old_Time_Movie_Show) 繼續處理
|
||||
- [ ] 驗證 Rust --resume 參數正確傳遞
|
||||
@@ -1,123 +0,0 @@
|
||||
{
|
||||
"name": "Momentry Video Search - Simple",
|
||||
"nodes": [
|
||||
{
|
||||
"parameters": {
|
||||
"httpMethod": "POST",
|
||||
"path": "video-search-simple",
|
||||
"responseMode": "lastNode",
|
||||
"options": {}
|
||||
},
|
||||
"id": "webhook-simple",
|
||||
"name": "Webhook (Simple)",
|
||||
"type": "n8n-nodes-base.webhook",
|
||||
"typeVersion": 1,
|
||||
"position": [
|
||||
250,
|
||||
300
|
||||
],
|
||||
"webhookId": "video-search-simple"
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"url": "http://localhost:3002/api/v1/n8n/search",
|
||||
"method": "POST",
|
||||
"sendBody": true,
|
||||
"bodyParameters": {
|
||||
"parameters": [
|
||||
{
|
||||
"name": "query",
|
||||
"value": "={{ $json.body }}"
|
||||
},
|
||||
{
|
||||
"name": "limit",
|
||||
"value": 5
|
||||
}
|
||||
]
|
||||
},
|
||||
"options": {
|
||||
"headers": {
|
||||
"Content-Type": "application/json",
|
||||
"X-API-Key": "demo_api_key_12345"
|
||||
}
|
||||
}
|
||||
},
|
||||
"id": "http-search",
|
||||
"name": "搜尋 Momentry",
|
||||
"type": "n8n-nodes-base.httpRequest",
|
||||
"typeVersion": 3,
|
||||
"position": [
|
||||
500,
|
||||
300
|
||||
]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"jsCode": "// 處理 Momentry 搜尋結果\nconst data = $input.first().json;\nconst hits = data.hits;\n\nif (!hits || hits.length === 0) {\n return {\n json: {\n success: false,\n message: '找不到相關結果',\n query: data.query\n }\n };\n}\n\n// 格式化結果\nconst formattedResults = hits.map((hit, idx) => {\n return {\n index: idx + 1,\n id: hit.id,\n title: hit.title,\n text: hit.text,\n startTime: hit.start,\n endTime: hit.end,\n relevance: Math.round(hit.score * 100) + '%',\n file_path: hit.file_path\n };\n});\n\nreturn {\n json: {\n success: true,\n query: data.query,\n totalFound: data.count,\n results: formattedResults\n }\n};"
|
||||
},
|
||||
"id": "code-process-simple",
|
||||
"name": "處理結果",
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 1,
|
||||
"position": [
|
||||
750,
|
||||
300
|
||||
]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"respondWith": "json",
|
||||
"responseBody": "={{ $json }}"
|
||||
},
|
||||
"id": "respond-webhook",
|
||||
"name": "回傳結果",
|
||||
"type": "n8n-nodes-base.respondToWebhook",
|
||||
"typeVersion": 1,
|
||||
"position": [
|
||||
1000,
|
||||
300
|
||||
]
|
||||
}
|
||||
],
|
||||
"connections": {
|
||||
"Webhook (Simple)": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "搜尋 Momentry",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"搜尋 Momentry": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "處理結果",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"處理結果": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "回傳結果",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
"active": false,
|
||||
"settings": {},
|
||||
"id": "momentry-video-search-simple",
|
||||
"versionId": "1",
|
||||
"createdAt": "2026-03-23T00:00:00.000Z",
|
||||
"updatedAt": "2026-03-23T00:00:00.000Z"
|
||||
}
|
||||
@@ -1,89 +0,0 @@
|
||||
{
|
||||
"name": "Momentry Video Search Test",
|
||||
"nodes": [
|
||||
{
|
||||
"parameters": {
|
||||
"httpMethod": "POST",
|
||||
"path": "video-search-test",
|
||||
"responseMode": "lastNode",
|
||||
"options": {}
|
||||
},
|
||||
"name": "Webhook",
|
||||
"type": "n8n-nodes-base.webhook",
|
||||
"typeVersion": 1,
|
||||
"position": [250, 300]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"url": "http://localhost:3002/api/v1/n8n/search",
|
||||
"method": "POST",
|
||||
"sendBody": true,
|
||||
"specifyBody": "json",
|
||||
"jsonBody": "={{ JSON.stringify({ query: $json.body.query || \"test\", limit: $json.body.limit || 5 }) }}",
|
||||
"options": {}
|
||||
},
|
||||
"name": "HTTP Request",
|
||||
"type": "n8n-nodes-base.httpRequest",
|
||||
"typeVersion": 4.1,
|
||||
"position": [500, 300]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"jsCode": "return $input.first().json;"
|
||||
},
|
||||
"name": "Code",
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 2,
|
||||
"position": [750, 300]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"respondWith": "json",
|
||||
"responseBody": "={{ JSON.stringify($json) }}"
|
||||
},
|
||||
"name": "Respond to Webhook",
|
||||
"type": "n8n-nodes-base.respondToWebhook",
|
||||
"typeVersion": 1.5,
|
||||
"position": [1000, 300]
|
||||
}
|
||||
],
|
||||
"connections": {
|
||||
"Webhook": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "HTTP Request",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"HTTP Request": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Code",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Code": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Respond to Webhook",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
"executionOrder": "v1"
|
||||
},
|
||||
"staticData": null
|
||||
}
|
||||
@@ -1,109 +0,0 @@
|
||||
{
|
||||
"name": "Momentry Video RAG MCP",
|
||||
"nodes": [
|
||||
{
|
||||
"parameters": {
|
||||
"httpMethod": "POST",
|
||||
"path": "video-rag-mcp",
|
||||
"responseMode": "lastNode",
|
||||
"options": {}
|
||||
},
|
||||
"name": "Webhook Trigger",
|
||||
"type": "n8n-nodes-base.webhook",
|
||||
"typeVersion": 1,
|
||||
"position": [
|
||||
250,
|
||||
300
|
||||
]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"url": "http://localhost:3002/api/v1/n8n/search",
|
||||
"method": "POST",
|
||||
"sendBody": true,
|
||||
"contentType": "json",
|
||||
"body": "={{ JSON.stringify({query: $json.body.query || $json.body, limit: $json.body.limit || 5, uuid: $json.body.uuid}) }}",
|
||||
"options": {
|
||||
"timeout": 30000,
|
||||
"headers": {
|
||||
"X-API-Key": "demo_api_key_12345"
|
||||
}
|
||||
}
|
||||
},
|
||||
"name": "Search Momentry Core",
|
||||
"type": "n8n-nodes-base.httpRequest",
|
||||
"typeVersion": 4.1,
|
||||
"position": [
|
||||
500,
|
||||
300
|
||||
]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"jsCode": "// Process Momentry Core search results\nconst data = $input.first().json;\nconst hits = data.hits || [];\n\nif (hits.length === 0) {\n return {\n json: {\n success: false,\n message: 'No relevant results found',\n query: data.query,\n results: []\n }\n };\n}\n\n// Format results for RAG\nconst formattedResults = hits.map((hit, idx) => {\n return {\n index: idx + 1,\n id: hit.id || hit.chunk_id,\n title: hit.title || 'Unknown Video',\n text: hit.text || hit.content || '',\n startTime: hit.start_time || hit.start || 0,\n endTime: hit.end_time || hit.end || 0,\n relevance: Math.round((hit.score || 0) * 100) + '%',\n videoUuid: hit.video_uuid || hit.uuid,\n file_path: hit.file_path || ''\n };\n});\n\n// Build context for RAG\nconst context = formattedResults\n .map(r => \\`[\\${r.index}] \\${r.text} (Video: \\${r.title}, Time: \\${r.startTime}s-\\${r.endTime}s)\\`)\n .join('\\n\\n');\n\nreturn {\n json: {\n success: true,\n query: data.query,\n totalFound: data.count || hits.length,\n context: context,\n results: formattedResults\n }\n};"
|
||||
},
|
||||
"name": "Process RAG Results",
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 2,
|
||||
"position": [
|
||||
750,
|
||||
300
|
||||
]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"respondWith": "json",
|
||||
"responseBody": "={{ JSON.stringify($json) }}",
|
||||
"options": {
|
||||
"statusCode": 200
|
||||
}
|
||||
},
|
||||
"name": "Respond to Webhook",
|
||||
"type": "n8n-nodes-base.respondToWebhook",
|
||||
"typeVersion": 1.5,
|
||||
"position": [
|
||||
1000,
|
||||
300
|
||||
]
|
||||
}
|
||||
],
|
||||
"connections": {
|
||||
"Webhook Trigger": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Search Momentry Core",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Search Momentry Core": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Process RAG Results",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Process RAG Results": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Respond to Webhook",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
"executionOrder": "v1"
|
||||
},
|
||||
"staticData": null
|
||||
}
|
||||
@@ -1,138 +0,0 @@
|
||||
{
|
||||
"name": "Momentry Video Search",
|
||||
"nodes": [
|
||||
{
|
||||
"parameters": {
|
||||
"httpMethod": "POST",
|
||||
"path": "video-search",
|
||||
"responseMode": "lastNode",
|
||||
"options": {}
|
||||
},
|
||||
"id": "webhook-trigger",
|
||||
"name": "Webhook",
|
||||
"type": "n8n-nodes-base.webhook",
|
||||
"typeVersion": 1,
|
||||
"position": [
|
||||
250,
|
||||
300
|
||||
],
|
||||
"webhookId": "video-search"
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"url": "http://localhost:3002/api/v1/n8n/search",
|
||||
"method": "POST",
|
||||
"sendBody": true,
|
||||
"bodyParameters": {
|
||||
"parameters": [
|
||||
{
|
||||
"name": "query",
|
||||
"value": "={{ $json.body }}"
|
||||
},
|
||||
{
|
||||
"name": "limit",
|
||||
"value": 5
|
||||
}
|
||||
]
|
||||
},
|
||||
"options": {
|
||||
"headers": {
|
||||
"Content-Type": "application/json",
|
||||
"X-API-Key": "demo_api_key_12345"
|
||||
}
|
||||
}
|
||||
},
|
||||
"id": "http-request-search",
|
||||
"name": "搜尋 Momentry",
|
||||
"type": "n8n-nodes-base.httpRequest",
|
||||
"typeVersion": 3,
|
||||
"position": [
|
||||
500,
|
||||
300
|
||||
]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"jsCode": "const hits = $input.first().json.hits;\n\nif (!hits || hits.length === 0) {\n return {\n json: {\n message: '找不到相關結果',\n query: $input.first().json.query\n }\n };\n}\n\nconst results = hits.map((hit, index) => {\n return {\n number: index + 1,\n text: hit.text,\n start: hit.start,\n end: hit.end,\n score: Math.round(hit.score * 100) + '%',\n video_title: hit.title,\n file_path: hit.file_path\n };\n});\n\nreturn {\n json: {\n query: $input.first().json.query,\n count: $input.first().json.count,\n results: results\n }\n};"
|
||||
},
|
||||
"id": "code-process",
|
||||
"name": "處理結果",
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 1,
|
||||
"position": [
|
||||
750,
|
||||
300
|
||||
]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"method": "POST",
|
||||
"url": "https://api.telegram.org/bot{{ $env.TELEGRAM_BOT_TOKEN }}/sendMessage",
|
||||
"sendBody": true,
|
||||
"bodyParameters": {
|
||||
"parameters": [
|
||||
{
|
||||
"name": "chat_id",
|
||||
"value": "={{ $json.chat_id }}"
|
||||
},
|
||||
{
|
||||
"name": "text",
|
||||
"value": "=🎬 搜尋結果: \"{{ $json.query }}\"\n\n{{ $json.results.map(r => r.number + '️⃣ \"' + r.text.substring(0, 50) + '...\"\n⏱ ' + r.start + 's - ' + r.end + 's\n📊 相關度: ' + r.score).join('\n\n') }}"
|
||||
}
|
||||
]
|
||||
},
|
||||
"options": {}
|
||||
},
|
||||
"id": "telegram-send",
|
||||
"name": "Telegram 通知",
|
||||
"type": "n8n-nodes-base.httpRequest",
|
||||
"typeVersion": 3,
|
||||
"position": [
|
||||
1000,
|
||||
300
|
||||
],
|
||||
"continueOnFail": true
|
||||
}
|
||||
],
|
||||
"connections": {
|
||||
"Webhook": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "搜尋 Momentry",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"搜尋 Momentry": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "處理結果",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"處理結果": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Telegram 通知",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
"active": false,
|
||||
"settings": {},
|
||||
"id": "momentry-video-search",
|
||||
"versionId": "1",
|
||||
"createdAt": "2026-03-23T00:00:00.000Z",
|
||||
"updatedAt": "2026-03-23T00:00:00.000Z"
|
||||
}
|
||||
100
docs/test_all.sh
100
docs/test_all.sh
@@ -1,100 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
echo "=========================================="
|
||||
echo "Momentry Core API 測試腳本"
|
||||
echo "=========================================="
|
||||
echo ""
|
||||
|
||||
# 顏色定義
|
||||
GREEN='\033[0;32m'
|
||||
RED='\033[0;31m'
|
||||
YELLOW='\033[1;33m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
# 測試 1: Momentry Core 搜尋
|
||||
echo -e "${YELLOW}測試 1: Momentry Core 搜尋 API${NC}"
|
||||
echo "URL: http://localhost:3002/api/v1/n8n/search"
|
||||
echo ""
|
||||
RESPONSE=$(curl -s -X POST http://localhost:3002/api/v1/n8n/search \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"query":"charade","limit":2}')
|
||||
|
||||
if echo "$RESPONSE" | grep -q '"hits"'; then
|
||||
echo -e "${GREEN}✅ 成功!${NC}"
|
||||
echo "$RESPONSE" | python3 -m json.tool | grep -E '"query"|"count"' | head -3
|
||||
echo ""
|
||||
echo "前 2 個結果:"
|
||||
echo "$RESPONSE" | python3 -c "
|
||||
import sys, json
|
||||
data = json.load(sys.stdin)
|
||||
for i, hit in enumerate(data.get('hits', [])[:2]):
|
||||
print(f\" [{i+1}] {hit.get('text', '')[:50]}...\")
|
||||
print(f\" 時間: {hit.get('start')}s - {hit.get('end')}s\")
|
||||
print(f\" 影片: {hit.get('title')}\")
|
||||
"
|
||||
else
|
||||
echo -e "${RED}❌ 失敗${NC}"
|
||||
echo "$RESPONSE"
|
||||
fi
|
||||
echo ""
|
||||
|
||||
# 測試 2: 列出影片
|
||||
echo -e "${YELLOW}測試 2: 列出所有影片${NC}"
|
||||
echo "URL: http://localhost:3002/api/v1/videos"
|
||||
echo ""
|
||||
RESPONSE=$(curl -s http://localhost:3002/api/v1/videos)
|
||||
|
||||
if echo "$RESPONSE" | grep -q '"videos"'; then
|
||||
echo -e "${GREEN}✅ 成功!${NC}"
|
||||
echo "$RESPONSE" | python3 -c "
|
||||
import sys, json
|
||||
data = json.load(sys.stdin)
|
||||
print(f\"找到 {len(data.get('videos', []))} 個影片:\")
|
||||
for v in data.get('videos', []):
|
||||
print(f\" - {v.get('file_name')} (UUID: {v.get('uuid')[:8]}...)\")
|
||||
"
|
||||
else
|
||||
echo -e "${RED}❌ 失敗${NC}"
|
||||
fi
|
||||
echo ""
|
||||
|
||||
# 測試 3: n8n Webhook (Test Mode)
|
||||
echo -e "${YELLOW}測試 3: n8n Webhook (Test Mode)${NC}"
|
||||
echo "URL: http://localhost:5678/webhook-test/video-rag-mcp"
|
||||
echo ""
|
||||
echo "⚠️ 注意: 請先在 n8n UI 中點擊 'Execute workflow' 按鈕"
|
||||
echo ""
|
||||
|
||||
RESPONSE=$(curl -s -X POST http://localhost:5678/webhook-test/video-rag-mcp \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"query":"charade","limit":2}')
|
||||
|
||||
if echo "$RESPONSE" | grep -q '"success": true'; then
|
||||
echo -e "${GREEN}✅ Webhook 測試成功!${NC}"
|
||||
echo "$RESPONSE" | python3 -c "
|
||||
import sys, json
|
||||
data = json.load(sys.stdin)
|
||||
print(f\"查詢: {data.get('query')}\")
|
||||
print(f\"找到: {data.get('totalFound')} 個結果\")
|
||||
print(f\"Context 長度: {len(data.get('context', ''))} 字元\")
|
||||
"
|
||||
elif echo "$RESPONSE" | grep -q '404'; then
|
||||
echo -e "${RED}❌ Webhook 未找到${NC}"
|
||||
echo "請在 n8n UI 中:"
|
||||
echo " 1. 開啟 'Momentry Video RAG MCP' 工作流程"
|
||||
echo " 2. 點擊 'Execute workflow' 按鈕"
|
||||
echo " 3. 30 秒內再次執行此腳本"
|
||||
else
|
||||
echo -e "${RED}❌ 錯誤${NC}"
|
||||
echo "$RESPONSE" | python3 -m json.tool 2>/dev/null || echo "$RESPONSE"
|
||||
fi
|
||||
echo ""
|
||||
|
||||
echo "=========================================="
|
||||
echo "測試完成!"
|
||||
echo "=========================================="
|
||||
echo ""
|
||||
echo "快速參考:"
|
||||
echo " Momentry API: http://localhost:3002/api/v1"
|
||||
echo " n8n UI: https://n8n.momentry.ddns.net"
|
||||
echo " Webhook Test: http://localhost:5678/webhook-test/video-rag-mcp"
|
||||
@@ -1,33 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Test Momentry Core API directly
|
||||
# This bypasses n8n and tests the API directly
|
||||
|
||||
echo "=========================================="
|
||||
echo "Testing Momentry Core API Directly"
|
||||
echo "=========================================="
|
||||
echo ""
|
||||
|
||||
# Test 1: Health check
|
||||
echo "Test 1: Health Check"
|
||||
curl -s http://localhost:3002/api/v1/health 2>&1 | head -5
|
||||
echo ""
|
||||
|
||||
# Test 2: Search API
|
||||
echo "Test 2: Search API"
|
||||
echo "Query: 'charade'"
|
||||
curl -s -X POST http://localhost:3002/api/v1/n8n/search \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"query":"charade","limit":3}' | python3 -m json.tool
|
||||
echo ""
|
||||
|
||||
# Test 3: List videos
|
||||
echo "Test 3: List Videos"
|
||||
curl -s http://localhost:3002/api/v1/videos | python3 -m json.tool 2>/dev/null || echo "No videos or endpoint error"
|
||||
echo ""
|
||||
|
||||
echo "=========================================="
|
||||
echo "If all tests show JSON responses, API is working!"
|
||||
echo ""
|
||||
echo "Next step: Use the API in n8n workflows"
|
||||
echo "=========================================="
|
||||
@@ -1,104 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Test Momentry Video RAG MCP Workflow
|
||||
# Usage: ./test_workflow.sh [query] [limit] [uuid]
|
||||
|
||||
N8N_URL="http://localhost:5678"
|
||||
WEBHOOK_PATH="webhook-test/video-rag-mcp"
|
||||
|
||||
echo "⚠️ 注意:使用 Test Webhook URL"
|
||||
echo "(生產環境 Webhook 需要工作流程手動激活後才能使用)"
|
||||
echo ""
|
||||
|
||||
# Default values
|
||||
QUERY="${1:-charade}"
|
||||
LIMIT="${2:-3}"
|
||||
UUID="${3:-}"
|
||||
|
||||
echo "=========================================="
|
||||
echo "Testing Video RAG Workflow"
|
||||
echo "=========================================="
|
||||
echo "Query: $QUERY"
|
||||
echo "Limit: $LIMIT"
|
||||
if [ -n "$UUID" ]; then
|
||||
echo "UUID: $UUID"
|
||||
fi
|
||||
echo ""
|
||||
|
||||
# Build JSON payload
|
||||
if [ -n "$UUID" ]; then
|
||||
PAYLOAD=$(cat <<JSON
|
||||
{
|
||||
"query": "$QUERY",
|
||||
"limit": $LIMIT,
|
||||
"uuid": "$UUID"
|
||||
}
|
||||
JSON
|
||||
)
|
||||
else
|
||||
PAYLOAD=$(cat <<JSON
|
||||
{
|
||||
"query": "$QUERY",
|
||||
"limit": $LIMIT
|
||||
}
|
||||
JSON
|
||||
)
|
||||
fi
|
||||
|
||||
echo "Request:"
|
||||
echo "$PAYLOAD" | python3 -m json.tool 2>/dev/null || echo "$PAYLOAD"
|
||||
echo ""
|
||||
echo "=========================================="
|
||||
echo "Response:"
|
||||
echo "=========================================="
|
||||
echo "URL: ${N8N_URL}/${WEBHOOK_PATH}"
|
||||
echo ""
|
||||
|
||||
# Make the request
|
||||
RESPONSE=$(curl -s -X POST "${N8N_URL}/${WEBHOOK_PATH}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "$PAYLOAD")
|
||||
|
||||
# Format and display response
|
||||
echo "$RESPONSE" | python3 -m json.tool 2>/dev/null || echo "$RESPONSE"
|
||||
|
||||
echo ""
|
||||
echo "=========================================="
|
||||
|
||||
# Check if successful
|
||||
if echo "$RESPONSE" | grep -q '"success": true'; then
|
||||
echo "✅ Test PASSED"
|
||||
|
||||
# Extract and display summary
|
||||
echo ""
|
||||
echo "Summary:"
|
||||
echo "$RESPONSE" | python3 -c "
|
||||
import sys, json
|
||||
data = json.load(sys.stdin)
|
||||
if data.get('success'):
|
||||
print(f\" Query: {data.get('query', 'N/A')}\")
|
||||
print(f\" Total Found: {data.get('totalFound', 0)}\")
|
||||
results = data.get('results', [])
|
||||
print(f\" Results Count: {len(results)}\")
|
||||
print()
|
||||
print(' Top Results:')
|
||||
for r in results[:3]:
|
||||
print(f\" [{r.get('index')}] {r.get('title', 'Unknown')}\")
|
||||
text = r.get('text', '')[:50]
|
||||
print(f\" Text: {text}...\")
|
||||
print(f\" Time: {r.get('startTime')}s - {r.get('endTime')}s\")
|
||||
print(f\" Relevance: {r.get('relevance')}\")
|
||||
print()
|
||||
" 2>/dev/null || true
|
||||
elif echo "$RESPONSE" | grep -q '"code": 404'; then
|
||||
echo "❌ Webhook not found"
|
||||
echo ""
|
||||
echo "可能的解決方案:"
|
||||
echo " 1. 在 n8n UI 中開啟工作流程: https://n8n.momentry.ddns.net"
|
||||
echo " 2. 點擊右上角的 'Active' 開關"
|
||||
echo " 3. 或者使用 test webhook: webhook-test/video-rag-mcp"
|
||||
else
|
||||
echo "❌ Test FAILED or no results found"
|
||||
fi
|
||||
|
||||
echo "=========================================="
|
||||
@@ -1,19 +1,11 @@
|
||||
use axum::{
|
||||
extract::State,
|
||||
http::StatusCode,
|
||||
response::Json,
|
||||
routing::post,
|
||||
Router,
|
||||
};
|
||||
use axum::{extract::State, http::StatusCode, response::Json, routing::post, Router};
|
||||
use reqwest::Client;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tracing;
|
||||
|
||||
use crate::api::server::AppState;
|
||||
|
||||
pub fn agent_routes() -> Router<AppState> {
|
||||
Router::new()
|
||||
.route("/api/v1/agents/translate", post(translate_text))
|
||||
Router::new().route("/api/v1/agents/translate", post(translate_text))
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
@@ -35,7 +27,6 @@ async fn translate_text(
|
||||
State(_state): State<AppState>,
|
||||
Json(req): Json<TranslationRequest>,
|
||||
) -> Result<Json<TranslationResponse>, (StatusCode, String)> {
|
||||
|
||||
let system_prompt = "You are a professional translator for Momentry Core, a digital asset management system specializing in video analysis.
|
||||
|
||||
## Guidelines:
|
||||
@@ -64,16 +55,27 @@ async fn translate_text(
|
||||
"stream": false
|
||||
});
|
||||
|
||||
let response = client.post(ollama_url)
|
||||
let response = client
|
||||
.post(ollama_url)
|
||||
.json(&body)
|
||||
.send()
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Failed to call LLM: {}", e)))?;
|
||||
.map_err(|e| {
|
||||
(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("Failed to call LLM: {}", e),
|
||||
)
|
||||
})?;
|
||||
|
||||
let ollama_resp: serde_json::Value = response.json().await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Failed to parse LLM response: {}", e)))?;
|
||||
let ollama_resp: serde_json::Value = response.json().await.map_err(|e| {
|
||||
(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("Failed to parse LLM response: {}", e),
|
||||
)
|
||||
})?;
|
||||
|
||||
let translated_text = ollama_resp.get("response")
|
||||
let translated_text = ollama_resp
|
||||
.get("response")
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or("Translation failed")
|
||||
.to_string();
|
||||
|
||||
@@ -16,7 +16,7 @@ use crate::core::processor::face_recognition::{
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct FaceRecognitionRequest {
|
||||
pub video_uuid: String,
|
||||
pub file_uuid: String,
|
||||
pub enable_recognition: Option<bool>,
|
||||
pub enable_tracking: Option<bool>,
|
||||
pub enable_clustering: Option<bool>,
|
||||
@@ -33,7 +33,7 @@ pub struct FaceRecognitionResponse {
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct FaceRegistrationRequest {
|
||||
pub video_uuid: String,
|
||||
pub file_uuid: String,
|
||||
pub name: String,
|
||||
pub metadata: Option<serde_json::Value>,
|
||||
}
|
||||
@@ -47,7 +47,7 @@ pub struct FaceRegistrationApiResponse {
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct FaceSearchRequest {
|
||||
pub video_uuid: String,
|
||||
pub file_uuid: String,
|
||||
pub embedding: Vec<f32>,
|
||||
pub similarity_threshold: Option<f64>,
|
||||
pub limit: Option<i32>,
|
||||
@@ -71,7 +71,7 @@ pub struct FaceSearchResult {
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct FaceListQuery {
|
||||
pub video_uuid: String,
|
||||
pub file_uuid: String,
|
||||
pub page: Option<usize>,
|
||||
pub page_size: Option<usize>,
|
||||
pub active_only: Option<bool>,
|
||||
@@ -106,7 +106,7 @@ pub fn face_recognition_routes() -> Router<crate::api::server::AppState> {
|
||||
.route("/api/v1/face/:face_id", get(get_face_details))
|
||||
.route("/api/v1/face/:face_id", axum::routing::delete(delete_face))
|
||||
.route(
|
||||
"/api/v1/face/results/:video_uuid",
|
||||
"/api/v1/face/results/:file_uuid",
|
||||
get(get_recognition_results),
|
||||
)
|
||||
}
|
||||
@@ -119,7 +119,7 @@ async fn recognize_faces(
|
||||
|
||||
tracing::info!(
|
||||
"[FACE_RECOGNITION] Starting recognition for video: {}, processing_id: {}",
|
||||
request.video_uuid,
|
||||
request.file_uuid,
|
||||
processing_id
|
||||
);
|
||||
|
||||
@@ -134,12 +134,12 @@ async fn recognize_faces(
|
||||
}
|
||||
};
|
||||
|
||||
let video_record = match db.get_video_by_uuid(&request.video_uuid).await {
|
||||
let video_record = match db.get_video_by_uuid(&request.file_uuid).await {
|
||||
Ok(Some(record)) => record,
|
||||
Ok(None) => {
|
||||
return Err((
|
||||
StatusCode::NOT_FOUND,
|
||||
format!("Video not found: {}", request.video_uuid),
|
||||
format!("Video not found: {}", request.file_uuid),
|
||||
))
|
||||
}
|
||||
Err(e) => {
|
||||
@@ -178,13 +178,13 @@ async fn recognize_faces(
|
||||
};
|
||||
|
||||
// Store results in database
|
||||
if let Err(e) = store_recognition_results(&db, &request.video_uuid, &result).await {
|
||||
if let Err(e) = store_recognition_results(&db, &request.file_uuid, &result).await {
|
||||
tracing::warn!("Failed to store recognition results: {}", e);
|
||||
}
|
||||
|
||||
Ok(Json(FaceRecognitionResponse {
|
||||
success: true,
|
||||
message: format!("Face recognition completed for {}", request.video_uuid),
|
||||
message: format!("Face recognition completed for {}", request.file_uuid),
|
||||
result: Some(result),
|
||||
processing_id,
|
||||
}))
|
||||
@@ -334,7 +334,7 @@ async fn register_face_api(
|
||||
.bind(&name)
|
||||
.bind(&embedding_str)
|
||||
.bind(&attrs_json)
|
||||
.bind(&metadata.unwrap_or(serde_json::json!({})))
|
||||
.bind(serde_json::to_string(&metadata.unwrap_or(serde_json::json!({}))).unwrap())
|
||||
.execute(db.pool())
|
||||
.await
|
||||
{
|
||||
@@ -694,7 +694,7 @@ async fn delete_face(
|
||||
|
||||
async fn get_recognition_results(
|
||||
State(_state): State<crate::api::server::AppState>,
|
||||
Path(video_uuid): Path<String>,
|
||||
Path(file_uuid): Path<String>,
|
||||
) -> Result<Json<serde_json::Value>, (StatusCode, String)> {
|
||||
let db = match PostgresDb::init().await {
|
||||
Ok(db) => db,
|
||||
@@ -708,7 +708,7 @@ async fn get_recognition_results(
|
||||
|
||||
let query = r#"
|
||||
SELECT
|
||||
video_uuid,
|
||||
file_uuid,
|
||||
frame_count,
|
||||
fps,
|
||||
total_faces,
|
||||
@@ -718,7 +718,7 @@ async fn get_recognition_results(
|
||||
processing_time_secs,
|
||||
created_at
|
||||
FROM face_recognition_results
|
||||
WHERE video_uuid = $1
|
||||
WHERE file_uuid = $1
|
||||
ORDER BY created_at DESC
|
||||
LIMIT 1
|
||||
"#;
|
||||
@@ -734,7 +734,7 @@ async fn get_recognition_results(
|
||||
Option<f64>,
|
||||
chrono::DateTime<chrono::Utc>,
|
||||
)> = match sqlx::query_as(query)
|
||||
.bind(&video_uuid)
|
||||
.bind(&file_uuid)
|
||||
.fetch_optional(db.pool())
|
||||
.await
|
||||
{
|
||||
@@ -749,7 +749,7 @@ async fn get_recognition_results(
|
||||
|
||||
match result {
|
||||
Some((
|
||||
video_uuid,
|
||||
file_uuid,
|
||||
frame_count,
|
||||
fps,
|
||||
total_faces,
|
||||
@@ -761,7 +761,7 @@ async fn get_recognition_results(
|
||||
)) => {
|
||||
let response = serde_json::json!({
|
||||
"success": true,
|
||||
"video_uuid": video_uuid,
|
||||
"file_uuid": file_uuid,
|
||||
"frame_count": frame_count,
|
||||
"fps": fps,
|
||||
"total_faces": total_faces,
|
||||
@@ -776,14 +776,14 @@ async fn get_recognition_results(
|
||||
}
|
||||
None => Err((
|
||||
StatusCode::NOT_FOUND,
|
||||
format!("No recognition results found for video: {}", video_uuid),
|
||||
format!("No recognition results found for video: {}", file_uuid),
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
async fn store_recognition_results(
|
||||
db: &PostgresDb,
|
||||
video_uuid: &str,
|
||||
file_uuid: &str,
|
||||
result: &FaceRecognitionResult,
|
||||
) -> Result<(), anyhow::Error> {
|
||||
let total_faces = result.frames.iter().map(|f| f.faces.len()).sum::<usize>();
|
||||
@@ -796,7 +796,7 @@ async fn store_recognition_results(
|
||||
|
||||
let query = r#"
|
||||
INSERT INTO face_recognition_results (
|
||||
video_uuid,
|
||||
file_uuid,
|
||||
frame_count,
|
||||
fps,
|
||||
total_faces,
|
||||
@@ -804,7 +804,7 @@ async fn store_recognition_results(
|
||||
clusters_count,
|
||||
result_data
|
||||
) VALUES ($1, $2, $3, $4, $5, $6, $7)
|
||||
ON CONFLICT (video_uuid) DO UPDATE SET
|
||||
ON CONFLICT (file_uuid) DO UPDATE SET
|
||||
frame_count = EXCLUDED.frame_count,
|
||||
fps = EXCLUDED.fps,
|
||||
total_faces = EXCLUDED.total_faces,
|
||||
@@ -815,7 +815,7 @@ async fn store_recognition_results(
|
||||
"#;
|
||||
|
||||
sqlx::query(query)
|
||||
.bind(video_uuid)
|
||||
.bind(file_uuid)
|
||||
.bind(result.frame_count as i64)
|
||||
.bind(result.fps)
|
||||
.bind(total_faces as i32)
|
||||
@@ -840,7 +840,7 @@ async fn store_recognition_results(
|
||||
|
||||
let insert_query = r#"
|
||||
INSERT INTO face_detections (
|
||||
video_uuid,
|
||||
file_uuid,
|
||||
frame_number,
|
||||
timestamp_secs,
|
||||
face_id,
|
||||
@@ -854,7 +854,7 @@ async fn store_recognition_results(
|
||||
identity_confidence,
|
||||
cluster_id
|
||||
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10::vector, $11, $12, $13)
|
||||
ON CONFLICT (video_uuid, frame_number, x, y, width, height) DO UPDATE SET
|
||||
ON CONFLICT (file_uuid, frame_number, x, y, width, height) DO UPDATE SET
|
||||
face_id = EXCLUDED.face_id,
|
||||
confidence = EXCLUDED.confidence,
|
||||
embedding = EXCLUDED.embedding,
|
||||
@@ -874,7 +874,7 @@ async fn store_recognition_results(
|
||||
.map(|c| c.cluster_id.clone());
|
||||
|
||||
sqlx::query(insert_query)
|
||||
.bind(video_uuid)
|
||||
.bind(file_uuid)
|
||||
.bind(frame.frame as i64)
|
||||
.bind(frame.timestamp)
|
||||
.bind(face.face_id.as_deref())
|
||||
@@ -908,7 +908,7 @@ async fn store_recognition_results(
|
||||
let cluster_query = r#"
|
||||
INSERT INTO face_clusters (
|
||||
cluster_id,
|
||||
video_uuid,
|
||||
file_uuid,
|
||||
centroid,
|
||||
size,
|
||||
representative_face_id,
|
||||
@@ -923,7 +923,7 @@ async fn store_recognition_results(
|
||||
|
||||
sqlx::query(cluster_query)
|
||||
.bind(&cluster.cluster_id)
|
||||
.bind(video_uuid)
|
||||
.bind(file_uuid)
|
||||
.bind(¢roid_str)
|
||||
.bind(cluster.size as i32)
|
||||
.bind(cluster.representative_face_id.as_deref())
|
||||
|
||||
673
src/api/five_w1h_agent_api.rs
Normal file
673
src/api/five_w1h_agent_api.rs
Normal file
@@ -0,0 +1,673 @@
|
||||
use axum::{
|
||||
extract::State,
|
||||
http::StatusCode,
|
||||
response::Json,
|
||||
routing::{get, post},
|
||||
Router,
|
||||
};
|
||||
use reqwest::Client;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sqlx::Row;
|
||||
|
||||
use crate::api::server::AppState;
|
||||
use crate::core::db::PostgresDb;
|
||||
|
||||
pub fn five_w1h_agent_routes() -> Router<AppState> {
|
||||
Router::new()
|
||||
.route("/api/v1/agents/5w1h/analyze", post(analyze_5w1h))
|
||||
.route("/api/v1/agents/5w1h/batch", post(batch_analyze_5w1h))
|
||||
.route("/api/v1/agents/5w1h/status", get(get_5w1h_status))
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct Analyze5W1HRequest {
|
||||
pub file_uuid: String,
|
||||
pub scene_group_size: Option<usize>,
|
||||
pub model: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct Analyze5W1HResponse {
|
||||
pub success: bool,
|
||||
pub file_uuid: String,
|
||||
pub summaries: Vec<SummaryChunk>,
|
||||
pub processing_status: FiveW1HProcessingStatus,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct SummaryChunk {
|
||||
pub chunk_id: String,
|
||||
pub summary: String,
|
||||
pub analysis_5w1h: FiveW1HAnalysis,
|
||||
pub start_frame: i64,
|
||||
pub end_frame: i64,
|
||||
pub start_time: f64,
|
||||
pub end_time: f64,
|
||||
pub fps: f64,
|
||||
pub scene_count: usize,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct FiveW1HAnalysis {
|
||||
pub who: Vec<String>,
|
||||
pub what: Vec<String>,
|
||||
pub location: Vec<String>,
|
||||
pub when: String,
|
||||
pub why: String,
|
||||
pub how: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct FiveW1HProcessingStatus {
|
||||
pub status: String,
|
||||
pub scenes_processed: i32,
|
||||
pub scenes_total: i32,
|
||||
pub progress_pct: f64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct BatchAnalyze5W1HRequest {
|
||||
pub file_uuids: Vec<String>,
|
||||
pub scene_group_size: Option<usize>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct BatchAnalyze5W1HResponse {
|
||||
pub success: bool,
|
||||
pub jobs: Vec<BatchJobStatus>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct BatchJobStatus {
|
||||
pub file_uuid: String,
|
||||
pub status: String,
|
||||
pub message: String,
|
||||
}
|
||||
|
||||
async fn analyze_5w1h(
|
||||
State(state): State<AppState>,
|
||||
Json(req): Json<Analyze5W1HRequest>,
|
||||
) -> Result<Json<Analyze5W1HResponse>, (StatusCode, String)> {
|
||||
let db = PostgresDb::from_pool(state.db.pool().clone());
|
||||
|
||||
let scene_group_size = req.scene_group_size.unwrap_or(7);
|
||||
let model = req.model.unwrap_or_else(|| "gemma4:latest".to_string());
|
||||
|
||||
let rule3_chunks = fetch_rule3_chunks(&db, &req.file_uuid)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
|
||||
|
||||
if rule3_chunks.is_empty() {
|
||||
return Err((
|
||||
StatusCode::BAD_REQUEST,
|
||||
"No Rule 3 chunks found for this video".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
let scenes_total = rule3_chunks.len() as i32;
|
||||
|
||||
update_agent_status(&db, &req.file_uuid, "running", 0, scenes_total, 0.0)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
|
||||
|
||||
let summaries =
|
||||
process_scene_groups(&db, &req.file_uuid, &rule3_chunks, scene_group_size, &model)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
tracing::error!("Failed to process scene groups: {}", e);
|
||||
(StatusCode::INTERNAL_SERVER_ERROR, e.to_string())
|
||||
})?;
|
||||
|
||||
let scenes_processed = rule3_chunks.len() as i32;
|
||||
let progress_pct = 100.0;
|
||||
|
||||
update_agent_status(
|
||||
&db,
|
||||
&req.file_uuid,
|
||||
"completed",
|
||||
scenes_processed,
|
||||
scenes_total,
|
||||
progress_pct,
|
||||
)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
|
||||
|
||||
Ok(Json(Analyze5W1HResponse {
|
||||
success: true,
|
||||
file_uuid: req.file_uuid,
|
||||
summaries,
|
||||
processing_status: FiveW1HProcessingStatus {
|
||||
status: "completed".to_string(),
|
||||
scenes_processed,
|
||||
scenes_total,
|
||||
progress_pct,
|
||||
},
|
||||
}))
|
||||
}
|
||||
|
||||
async fn batch_analyze_5w1h(
|
||||
State(state): State<AppState>,
|
||||
Json(req): Json<BatchAnalyze5W1HRequest>,
|
||||
) -> Result<Json<BatchAnalyze5W1HResponse>, (StatusCode, String)> {
|
||||
let scene_group_size = req.scene_group_size.unwrap_or(7);
|
||||
let jobs: Vec<BatchJobStatus> = req
|
||||
.file_uuids
|
||||
.iter()
|
||||
.map(|uuid| {
|
||||
let uuid_clone = uuid.clone();
|
||||
let db_clone = PostgresDb::from_pool(state.db.pool().clone());
|
||||
let group_size = scene_group_size;
|
||||
|
||||
tokio::spawn(async move {
|
||||
let _ = process_single_video_5w1h(&db_clone, &uuid_clone, group_size).await;
|
||||
});
|
||||
|
||||
BatchJobStatus {
|
||||
file_uuid: uuid.clone(),
|
||||
status: "queued".to_string(),
|
||||
message: "Job queued for async processing".to_string(),
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
Ok(Json(BatchAnalyze5W1HResponse {
|
||||
success: true,
|
||||
jobs,
|
||||
}))
|
||||
}
|
||||
|
||||
async fn get_5w1h_status(
|
||||
State(state): State<AppState>,
|
||||
) -> Result<Json<serde_json::Value>, (StatusCode, String)> {
|
||||
let db = PostgresDb::from_pool(state.db.pool().clone());
|
||||
|
||||
let videos_with_5w1h = fetch_videos_with_5w1h_status(&db)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
|
||||
|
||||
Ok(Json(serde_json::json!({
|
||||
"success": true,
|
||||
"videos": videos_with_5w1h
|
||||
})))
|
||||
}
|
||||
|
||||
async fn fetch_rule3_chunks(db: &PostgresDb, file_uuid: &str) -> anyhow::Result<Vec<Rule3Chunk>> {
|
||||
let table = crate::core::db::schema::table_name("chunks");
|
||||
let query = format!(
|
||||
r#"
|
||||
SELECT chunk_id, start_frame, end_frame, fps, content, metadata
|
||||
FROM {}
|
||||
WHERE uuid = $1 AND (chunk_type = 'scene' OR chunk_type = 'cut')
|
||||
ORDER BY start_frame
|
||||
"#,
|
||||
table
|
||||
);
|
||||
|
||||
let rows = sqlx::query(&query)
|
||||
.bind(file_uuid)
|
||||
.fetch_all(db.pool())
|
||||
.await?;
|
||||
|
||||
let chunks: Vec<Rule3Chunk> = rows
|
||||
.iter()
|
||||
.map(|row| {
|
||||
let chunk_id: String = row.try_get("chunk_id").unwrap_or_default();
|
||||
let start_frame: i64 = row.try_get("start_frame").unwrap_or(0);
|
||||
let end_frame: i64 = row.try_get("end_frame").unwrap_or(0);
|
||||
let fps: f64 = row.try_get("fps").unwrap_or(30.0);
|
||||
let content: serde_json::Value =
|
||||
row.try_get("content").unwrap_or(serde_json::Value::Null);
|
||||
let metadata: serde_json::Value =
|
||||
row.try_get("metadata").unwrap_or(serde_json::Value::Null);
|
||||
|
||||
let summary = content
|
||||
.get("data")
|
||||
.and_then(|d| d.get("scene_number"))
|
||||
.and_then(|s| s.as_u64())
|
||||
.map(|n| format!("Scene {}", n))
|
||||
.unwrap_or_else(|| {
|
||||
content
|
||||
.get("data")
|
||||
.and_then(|d| d.get("summary"))
|
||||
.and_then(|s| s.as_str())
|
||||
.unwrap_or("No summary")
|
||||
.to_string()
|
||||
});
|
||||
|
||||
Rule3Chunk {
|
||||
chunk_id,
|
||||
start_frame,
|
||||
end_frame,
|
||||
fps,
|
||||
summary,
|
||||
metadata,
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
Ok(chunks)
|
||||
}
|
||||
|
||||
async fn process_scene_groups(
|
||||
db: &PostgresDb,
|
||||
file_uuid: &str,
|
||||
rule3_chunks: &[Rule3Chunk],
|
||||
group_size: usize,
|
||||
model: &str,
|
||||
) -> anyhow::Result<Vec<SummaryChunk>> {
|
||||
let mut summaries = Vec::new();
|
||||
let chunks_total = rule3_chunks.len();
|
||||
|
||||
for (group_idx, group) in rule3_chunks.chunks(group_size).enumerate() {
|
||||
let context_text = group
|
||||
.iter()
|
||||
.map(|c| c.summary.clone())
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n\n");
|
||||
|
||||
let faces = aggregate_faces(group);
|
||||
let objects = aggregate_objects(group);
|
||||
|
||||
let llm_result = call_llm_for_5w1h(&context_text, &faces, &objects, model).await?;
|
||||
|
||||
let start_frame = group.first().map(|c| c.start_frame).unwrap_or(0);
|
||||
let end_frame = group.last().map(|c| c.end_frame).unwrap_or(0);
|
||||
let fps = group.first().map(|c| c.fps).unwrap_or(30.0);
|
||||
|
||||
let start_time = start_frame as f64 / fps;
|
||||
let end_time = end_frame as f64 / fps;
|
||||
|
||||
let chunk_id = format!("summary_{}_{}", file_uuid, group_idx);
|
||||
|
||||
let summary = SummaryChunk {
|
||||
chunk_id: chunk_id.clone(),
|
||||
summary: llm_result.summary,
|
||||
analysis_5w1h: llm_result.analysis_5w1h,
|
||||
start_frame,
|
||||
end_frame,
|
||||
start_time,
|
||||
end_time,
|
||||
fps,
|
||||
scene_count: group.len(),
|
||||
};
|
||||
|
||||
store_summary_chunk(db, file_uuid, &summary, group).await?;
|
||||
|
||||
let scenes_processed = ((group_idx + 1) * group_size).min(chunks_total) as i32;
|
||||
let progress_pct = (scenes_processed as f64 / chunks_total as f64) * 100.0;
|
||||
|
||||
update_agent_status(
|
||||
db,
|
||||
file_uuid,
|
||||
"running",
|
||||
scenes_processed,
|
||||
chunks_total as i32,
|
||||
progress_pct,
|
||||
)
|
||||
.await?;
|
||||
|
||||
summaries.push(summary);
|
||||
}
|
||||
|
||||
Ok(summaries)
|
||||
}
|
||||
|
||||
async fn call_llm_for_5w1h(
|
||||
context_text: &str,
|
||||
faces: &[String],
|
||||
objects: &[String],
|
||||
model: &str,
|
||||
) -> anyhow::Result<LLM5W1HResult> {
|
||||
let system_prompt = r#"You are a video scene analysis assistant for Momentry Core.
|
||||
|
||||
## Task:
|
||||
Analyze the provided video scenes and extract structured 5W1H information.
|
||||
|
||||
## Output Format (JSON):
|
||||
{
|
||||
"summary": "A brief 2-3 sentence summary of these scenes",
|
||||
"5w1h": {
|
||||
"who": ["List of main characters/actors"],
|
||||
"what": ["List of main events/actions"],
|
||||
"where": ["List of locations/settings"],
|
||||
"when": "Time of day or temporal context",
|
||||
"why": "Motivation or reason for events",
|
||||
"how": "Method or process used"
|
||||
}
|
||||
}
|
||||
|
||||
## Guidelines:
|
||||
- Keep summaries concise and natural
|
||||
- Extract key information, not details
|
||||
- Return ONLY valid JSON, no explanations"#;
|
||||
|
||||
let prompt = format!(
|
||||
r#"Analyze these video scenes:
|
||||
|
||||
## Scene Summaries:
|
||||
{}
|
||||
|
||||
## Detected Faces:
|
||||
{}
|
||||
|
||||
## Detected Objects:
|
||||
{}
|
||||
|
||||
Return the 5W1H analysis in JSON format."#,
|
||||
context_text,
|
||||
faces.join(", "),
|
||||
objects.join(", ")
|
||||
);
|
||||
|
||||
let client = Client::new();
|
||||
let ollama_url = "http://localhost:11434/api/generate";
|
||||
|
||||
let body = serde_json::json!({
|
||||
"model": model,
|
||||
"prompt": prompt,
|
||||
"system": system_prompt,
|
||||
"stream": false,
|
||||
"format": "json"
|
||||
});
|
||||
|
||||
let response = client
|
||||
.post(ollama_url)
|
||||
.json(&body)
|
||||
.timeout(std::time::Duration::from_secs(60))
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
let ollama_resp: serde_json::Value = response.json().await?;
|
||||
|
||||
let response_text = ollama_resp
|
||||
.get("response")
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or("{}");
|
||||
|
||||
let parsed: serde_json::Value = serde_json::from_str(response_text)?;
|
||||
|
||||
let summary = parsed
|
||||
.get("summary")
|
||||
.and_then(|s| s.as_str())
|
||||
.unwrap_or("No summary generated")
|
||||
.to_string();
|
||||
|
||||
let analysis_5w1h = FiveW1HAnalysis {
|
||||
who: parsed
|
||||
.get("5w1h")
|
||||
.and_then(|w| w.get("who"))
|
||||
.and_then(|w| w.as_array())
|
||||
.map(|arr| {
|
||||
arr.iter()
|
||||
.filter_map(|v| v.as_str().map(|s| s.to_string()))
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default(),
|
||||
what: parsed
|
||||
.get("5w1h")
|
||||
.and_then(|w| w.get("what"))
|
||||
.and_then(|w| w.as_array())
|
||||
.map(|arr| {
|
||||
arr.iter()
|
||||
.filter_map(|v| v.as_str().map(|s| s.to_string()))
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default(),
|
||||
location: parsed
|
||||
.get("5w1h")
|
||||
.and_then(|w| w.get("where"))
|
||||
.and_then(|w| w.as_array())
|
||||
.map(|arr| {
|
||||
arr.iter()
|
||||
.filter_map(|v| v.as_str().map(|s| s.to_string()))
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default(),
|
||||
when: parsed
|
||||
.get("5w1h")
|
||||
.and_then(|w| w.get("when"))
|
||||
.and_then(|w| w.as_str())
|
||||
.unwrap_or("unknown")
|
||||
.to_string(),
|
||||
why: parsed
|
||||
.get("5w1h")
|
||||
.and_then(|w| w.get("why"))
|
||||
.and_then(|w| w.as_str())
|
||||
.unwrap_or("unknown")
|
||||
.to_string(),
|
||||
how: parsed
|
||||
.get("5w1h")
|
||||
.and_then(|w| w.get("how"))
|
||||
.and_then(|w| w.as_str())
|
||||
.unwrap_or("unknown")
|
||||
.to_string(),
|
||||
};
|
||||
|
||||
Ok(LLM5W1HResult {
|
||||
summary,
|
||||
analysis_5w1h,
|
||||
})
|
||||
}
|
||||
|
||||
async fn store_summary_chunk(
|
||||
db: &PostgresDb,
|
||||
file_uuid: &str,
|
||||
summary: &SummaryChunk,
|
||||
group: &[Rule3Chunk],
|
||||
) -> anyhow::Result<()> {
|
||||
let table = crate::core::db::schema::table_name("chunks");
|
||||
|
||||
let content = serde_json::json!({
|
||||
"rule": "rule4",
|
||||
"data": {
|
||||
"summary": summary.summary,
|
||||
"5w1h": {
|
||||
"who": summary.analysis_5w1h.who,
|
||||
"what": summary.analysis_5w1h.what,
|
||||
"where": summary.analysis_5w1h.location,
|
||||
"when": summary.analysis_5w1h.when,
|
||||
"why": summary.analysis_5w1h.why,
|
||||
"how": summary.analysis_5w1h.how,
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let metadata = serde_json::json!({
|
||||
"scene_count": summary.scene_count,
|
||||
"scene_chunk_ids": group.iter().map(|c| c.chunk_id.clone()).collect::<Vec<_>>(),
|
||||
});
|
||||
|
||||
let query = format!(
|
||||
r#"
|
||||
INSERT INTO {} (
|
||||
uuid, chunk_id, chunk_index, chunk_type,
|
||||
start_frame, end_frame, fps, start_time, end_time, content, metadata
|
||||
)
|
||||
VALUES ($1, $2, $3, 'summary', $4, $5, $6, $7, $8, $9::jsonb, $10::jsonb)
|
||||
ON CONFLICT (uuid, chunk_id) DO UPDATE SET
|
||||
content = EXCLUDED.content,
|
||||
metadata = EXCLUDED.metadata,
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
"#,
|
||||
table
|
||||
);
|
||||
|
||||
let start_time = summary.start_time;
|
||||
let end_time = summary.end_time;
|
||||
|
||||
sqlx::query(&query)
|
||||
.bind(file_uuid)
|
||||
.bind(&summary.chunk_id)
|
||||
.bind(0)
|
||||
.bind(summary.start_frame)
|
||||
.bind(summary.end_frame)
|
||||
.bind(summary.fps)
|
||||
.bind(start_time)
|
||||
.bind(end_time)
|
||||
.bind(&content)
|
||||
.bind(&metadata)
|
||||
.execute(db.pool())
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn update_agent_status(
|
||||
db: &PostgresDb,
|
||||
file_uuid: &str,
|
||||
status: &str,
|
||||
scenes_processed: i32,
|
||||
scenes_total: i32,
|
||||
progress_pct: f64,
|
||||
) -> anyhow::Result<()> {
|
||||
let table = crate::core::db::schema::table_name("videos");
|
||||
|
||||
let agent_status = serde_json::json!({
|
||||
"five_w1h": {
|
||||
"status": status,
|
||||
"scenes_processed": scenes_processed,
|
||||
"scenes_total": scenes_total,
|
||||
"progress_pct": progress_pct
|
||||
}
|
||||
});
|
||||
|
||||
let query = format!(
|
||||
r#"
|
||||
UPDATE {}
|
||||
SET processing_status = jsonb_set(
|
||||
COALESCE(processing_status, '{{}}'::jsonb),
|
||||
'{{agents}}',
|
||||
$1::jsonb
|
||||
)
|
||||
WHERE uuid = $2
|
||||
"#,
|
||||
table
|
||||
);
|
||||
|
||||
sqlx::query(&query)
|
||||
.bind(&agent_status)
|
||||
.bind(file_uuid)
|
||||
.execute(db.pool())
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn fetch_videos_with_5w1h_status(db: &PostgresDb) -> anyhow::Result<Vec<serde_json::Value>> {
|
||||
let table = crate::core::db::schema::table_name("videos");
|
||||
|
||||
let query = format!(
|
||||
r#"
|
||||
SELECT uuid, processing_status->'agents'->'five_w1h' as agent_status
|
||||
FROM {}
|
||||
WHERE processing_status->'agents'->'five_w1h' IS NOT NULL
|
||||
ORDER BY updated_at DESC
|
||||
LIMIT 50
|
||||
"#,
|
||||
table
|
||||
);
|
||||
|
||||
let rows = sqlx::query(&query).fetch_all(db.pool()).await?;
|
||||
|
||||
let videos: Vec<serde_json::Value> = rows
|
||||
.iter()
|
||||
.map(|row| {
|
||||
let uuid: String = row.try_get("uuid").unwrap_or_default();
|
||||
let status: Option<serde_json::Value> = row.try_get("agent_status").ok();
|
||||
|
||||
serde_json::json!({
|
||||
"uuid": uuid,
|
||||
"five_w1h_status": status
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
|
||||
Ok(videos)
|
||||
}
|
||||
|
||||
async fn process_single_video_5w1h(
|
||||
db: &PostgresDb,
|
||||
file_uuid: &str,
|
||||
scene_group_size: usize,
|
||||
) -> anyhow::Result<()> {
|
||||
let rule3_chunks = fetch_rule3_chunks(db, file_uuid).await?;
|
||||
|
||||
if rule3_chunks.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let scenes_total = rule3_chunks.len() as i32;
|
||||
|
||||
update_agent_status(db, file_uuid, "running", 0, scenes_total, 0.0).await?;
|
||||
|
||||
let _ = process_scene_groups(
|
||||
db,
|
||||
file_uuid,
|
||||
&rule3_chunks,
|
||||
scene_group_size,
|
||||
"gemma4:latest",
|
||||
)
|
||||
.await?;
|
||||
|
||||
update_agent_status(
|
||||
db,
|
||||
file_uuid,
|
||||
"completed",
|
||||
scenes_total,
|
||||
scenes_total,
|
||||
100.0,
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn aggregate_faces(group: &[Rule3Chunk]) -> Vec<String> {
|
||||
group
|
||||
.iter()
|
||||
.flat_map(|c| {
|
||||
c.metadata
|
||||
.get("faces")
|
||||
.and_then(|f| f.as_array())
|
||||
.map(|arr| {
|
||||
arr.iter()
|
||||
.filter_map(|v| v.as_str().map(|s| s.to_string()))
|
||||
.collect::<Vec<_>>()
|
||||
})
|
||||
.unwrap_or_default()
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn aggregate_objects(group: &[Rule3Chunk]) -> Vec<String> {
|
||||
group
|
||||
.iter()
|
||||
.flat_map(|c| {
|
||||
c.metadata
|
||||
.get("objects")
|
||||
.and_then(|o| o.as_array())
|
||||
.map(|arr| {
|
||||
arr.iter()
|
||||
.filter_map(|v| v.as_str().map(|s| s.to_string()))
|
||||
.collect::<Vec<_>>()
|
||||
})
|
||||
.unwrap_or_default()
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct Rule3Chunk {
|
||||
chunk_id: String,
|
||||
start_frame: i64,
|
||||
end_frame: i64,
|
||||
fps: f64,
|
||||
summary: String,
|
||||
metadata: serde_json::Value,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct LLM5W1HResult {
|
||||
summary: String,
|
||||
analysis_5w1h: FiveW1HAnalysis,
|
||||
}
|
||||
@@ -1,22 +1,31 @@
|
||||
use axum::{
|
||||
extract::{Query, State},
|
||||
http::StatusCode,
|
||||
response::Json,
|
||||
body::Body,
|
||||
extract::{Path, Query, State},
|
||||
http::{header, StatusCode},
|
||||
response::{IntoResponse, Json},
|
||||
routing::{get, post},
|
||||
Router,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::process::Command;
|
||||
|
||||
use crate::core::db::{schema, Database, PostgresDb};
|
||||
use crate::core::db::{Database, PostgresDb};
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct RegisterFromPersonRequest {
|
||||
pub video_uuid: String,
|
||||
pub file_uuid: String,
|
||||
pub person_id: String,
|
||||
pub identity_name: String,
|
||||
pub metadata: Option<serde_json::Value>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct RegisterFromFaceRequest {
|
||||
pub face_json_path: String,
|
||||
pub identity_name: String,
|
||||
pub schema: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct RegisterFromPersonResponse {
|
||||
pub success: bool,
|
||||
@@ -26,10 +35,135 @@ pub struct RegisterFromPersonResponse {
|
||||
pub person_id: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct RegisterFromFaceResponse {
|
||||
pub success: bool,
|
||||
pub message: String,
|
||||
pub identity_uuid: Option<String>,
|
||||
pub identity_name: String,
|
||||
pub total_vectors: Option<i32>,
|
||||
pub angle_coverage: Option<Vec<String>>,
|
||||
pub quality_avg: Option<f64>,
|
||||
}
|
||||
|
||||
pub fn identity_routes() -> Router<crate::api::server::AppState> {
|
||||
Router::new()
|
||||
.route("/api/v1/identities/from-person", post(register_from_person))
|
||||
.route("/api/v1/identities/from-face", post(register_from_face))
|
||||
.route("/api/v1/identities", get(list_identities))
|
||||
.route("/api/v1/faces/candidates", get(list_face_candidates))
|
||||
.route(
|
||||
"/api/v1/identities/:identity_id/faces",
|
||||
get(get_identity_faces),
|
||||
)
|
||||
.route("/api/v1/faces/:face_id/thumbnail", get(get_face_thumbnail))
|
||||
}
|
||||
|
||||
/// Register a Global Identity from face.json with multi-angle reference vectors.
|
||||
/// Calls select_face_reference_vectors_v2.py for automatic reference selection.
|
||||
async fn register_from_face(
|
||||
State(_state): State<crate::api::server::AppState>,
|
||||
Json(req): Json<RegisterFromFaceRequest>,
|
||||
) -> Result<Json<RegisterFromFaceResponse>, (StatusCode, String)> {
|
||||
let schema = req.schema.unwrap_or("dev".to_string());
|
||||
let python_path =
|
||||
std::env::var("MOMENTRY_PYTHON_PATH").unwrap_or("/opt/homebrew/bin/python3.11".to_string());
|
||||
|
||||
let scripts_dir = std::env::var("MOMENTRY_SCRIPTS_DIR").unwrap_or_else(|_| {
|
||||
let mut path = std::env::current_dir().unwrap_or_default();
|
||||
path.push("scripts");
|
||||
path.to_string_lossy().to_string()
|
||||
});
|
||||
|
||||
let script_path = format!("{}/select_face_reference_vectors_v2.py", scripts_dir);
|
||||
|
||||
tracing::info!(
|
||||
"Registering identity '{}' from face.json: {}",
|
||||
req.identity_name,
|
||||
req.face_json_path
|
||||
);
|
||||
|
||||
let output = Command::new(&python_path)
|
||||
.arg(&script_path)
|
||||
.arg("--face-json")
|
||||
.arg(&req.face_json_path)
|
||||
.arg("--identity-name")
|
||||
.arg(&req.identity_name)
|
||||
.arg("--register")
|
||||
.arg("--schema")
|
||||
.arg(&schema)
|
||||
.output()
|
||||
.map_err(|e| {
|
||||
(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("Failed to execute script: {}", e),
|
||||
)
|
||||
})?;
|
||||
|
||||
if !output.status.success() {
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
return Err((
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("Script failed: {}", stderr),
|
||||
));
|
||||
}
|
||||
|
||||
let db = PostgresDb::init().await.map_err(|e| {
|
||||
(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("DB error: {}", e),
|
||||
)
|
||||
})?;
|
||||
|
||||
let query = r#"
|
||||
SELECT uuid, reference_data->'total_references' as total,
|
||||
reference_data->'angles_covered' as angles,
|
||||
reference_data->'quality_avg' as quality
|
||||
FROM identities
|
||||
WHERE name = $1
|
||||
ORDER BY created_at DESC
|
||||
LIMIT 1
|
||||
"#;
|
||||
|
||||
let row: Option<(String, Option<i32>, Option<Vec<String>>, Option<f64>)> =
|
||||
sqlx::query_as(query)
|
||||
.bind(&req.identity_name)
|
||||
.fetch_optional(db.pool())
|
||||
.await
|
||||
.map_err(|e| {
|
||||
(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("Query error: {}", e),
|
||||
)
|
||||
})?;
|
||||
|
||||
match row {
|
||||
Some((uuid, total, angles, quality)) => Ok(Json(RegisterFromFaceResponse {
|
||||
success: true,
|
||||
message: format!(
|
||||
"Successfully registered identity '{}' with {} reference vectors",
|
||||
req.identity_name,
|
||||
total.unwrap_or(0)
|
||||
),
|
||||
identity_uuid: Some(uuid),
|
||||
identity_name: req.identity_name,
|
||||
total_vectors: total,
|
||||
angle_coverage: angles,
|
||||
quality_avg: quality,
|
||||
})),
|
||||
None => Ok(Json(RegisterFromFaceResponse {
|
||||
success: true,
|
||||
message: format!(
|
||||
"Identity '{}' registered, but details not found",
|
||||
req.identity_name
|
||||
),
|
||||
identity_uuid: None,
|
||||
identity_name: req.identity_name,
|
||||
total_vectors: None,
|
||||
angle_coverage: None,
|
||||
quality_avg: None,
|
||||
})),
|
||||
}
|
||||
}
|
||||
|
||||
/// Register a Global Identity from a specific Person in a video.
|
||||
@@ -61,10 +195,10 @@ async fn register_from_person(
|
||||
|
||||
// 1. Check if Person exists
|
||||
let person_query =
|
||||
"SELECT id, name FROM person_identities WHERE person_id = $1 AND video_uuid = $2";
|
||||
"SELECT id, name FROM person_identities WHERE person_id = $1 AND file_uuid = $2";
|
||||
let person: Option<(i32, Option<String>)> = match sqlx::query_as(person_query)
|
||||
.bind(&req.person_id)
|
||||
.bind(&req.video_uuid)
|
||||
.bind(&req.file_uuid)
|
||||
.fetch_optional(&mut *tx)
|
||||
.await
|
||||
{
|
||||
@@ -84,7 +218,7 @@ async fn register_from_person(
|
||||
StatusCode::NOT_FOUND,
|
||||
format!(
|
||||
"Person '{}' not found in video '{}'",
|
||||
req.person_id, req.video_uuid
|
||||
req.person_id, req.file_uuid
|
||||
),
|
||||
))
|
||||
}
|
||||
@@ -149,7 +283,7 @@ async fn register_from_person(
|
||||
.bind("person_id") // identity_type
|
||||
.bind(&req.person_id) // identity_value
|
||||
.bind(1.0) // confidence
|
||||
.bind(&serde_json::json!({"auto_updated": true}))
|
||||
.bind(serde_json::to_string(&serde_json::json!({"auto_updated": true})).unwrap())
|
||||
.execute(&mut *tx)
|
||||
.await
|
||||
{
|
||||
@@ -286,3 +420,420 @@ pub struct IdentityListResponse {
|
||||
pub page: usize,
|
||||
pub page_size: usize,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct FaceCandidatesQuery {
|
||||
pub file_uuid: Option<String>,
|
||||
pub min_confidence: Option<f64>,
|
||||
pub page: Option<usize>,
|
||||
pub page_size: Option<usize>,
|
||||
pub limit: Option<usize>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct FaceCandidate {
|
||||
pub id: i32,
|
||||
pub face_id: Option<String>,
|
||||
pub file_uuid: String,
|
||||
pub frame_number: i64,
|
||||
pub confidence: f64,
|
||||
pub bbox: Option<serde_json::Value>,
|
||||
pub attributes: Option<serde_json::Value>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct FaceCandidatesResponse {
|
||||
pub candidates: Vec<FaceCandidate>,
|
||||
pub total: i64,
|
||||
pub page: usize,
|
||||
pub page_size: usize,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct IdentityFacesQuery {
|
||||
pub page: Option<usize>,
|
||||
pub page_size: Option<usize>,
|
||||
pub limit: Option<usize>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct IdentityFace {
|
||||
pub id: i32,
|
||||
pub face_id: Option<String>,
|
||||
pub file_uuid: String,
|
||||
pub frame_number: i64,
|
||||
pub confidence: f64,
|
||||
pub bbox: Option<serde_json::Value>,
|
||||
pub attributes: Option<serde_json::Value>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct IdentityFacesResponse {
|
||||
pub identity_id: i32,
|
||||
pub faces: Vec<IdentityFace>,
|
||||
pub total: i64,
|
||||
}
|
||||
|
||||
async fn list_face_candidates(
|
||||
Query(query): Query<FaceCandidatesQuery>,
|
||||
) -> Result<Json<FaceCandidatesResponse>, (StatusCode, String)> {
|
||||
let db = match PostgresDb::init().await {
|
||||
Ok(db) => db,
|
||||
Err(e) => {
|
||||
return Err((
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("Failed to connect to database: {}", e),
|
||||
))
|
||||
}
|
||||
};
|
||||
|
||||
let page = query.page.unwrap_or(1);
|
||||
let page_size = std::cmp::min(query.page_size.unwrap_or(15), 100);
|
||||
let offset = (page - 1) * page_size;
|
||||
let min_confidence = query.min_confidence.unwrap_or(0.5);
|
||||
|
||||
let table = crate::core::db::schema::table_name("face_detections");
|
||||
|
||||
let total: i64 = if let Some(file_uuid) = &query.file_uuid {
|
||||
let count_sql = format!(
|
||||
"SELECT COUNT(*) FROM {} WHERE identity_id IS NULL AND confidence >= $1 AND file_uuid = $2",
|
||||
table
|
||||
);
|
||||
match sqlx::query_scalar(&count_sql)
|
||||
.bind(min_confidence)
|
||||
.bind(file_uuid)
|
||||
.fetch_one(db.pool())
|
||||
.await
|
||||
{
|
||||
Ok(count) => count,
|
||||
Err(e) => {
|
||||
return Err((
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("Count error: {}", e),
|
||||
))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let count_sql = format!(
|
||||
"SELECT COUNT(*) FROM {} WHERE identity_id IS NULL AND confidence >= $1",
|
||||
table
|
||||
);
|
||||
match sqlx::query_scalar(&count_sql)
|
||||
.bind(min_confidence)
|
||||
.fetch_one(db.pool())
|
||||
.await
|
||||
{
|
||||
Ok(count) => count,
|
||||
Err(e) => {
|
||||
return Err((
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("Count error: {}", e),
|
||||
))
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let rows = if let Some(file_uuid) = &query.file_uuid {
|
||||
let sql = format!(
|
||||
"SELECT id, face_id, file_uuid, frame_number, confidence, bbox, attributes
|
||||
FROM {}
|
||||
WHERE identity_id IS NULL AND confidence >= $1 AND file_uuid = $2
|
||||
ORDER BY confidence DESC
|
||||
LIMIT $3 OFFSET $4",
|
||||
table
|
||||
);
|
||||
match sqlx::query_as::<
|
||||
_,
|
||||
(
|
||||
i32,
|
||||
Option<String>,
|
||||
String,
|
||||
i64,
|
||||
f64,
|
||||
Option<serde_json::Value>,
|
||||
Option<serde_json::Value>,
|
||||
),
|
||||
>(&sql)
|
||||
.bind(min_confidence)
|
||||
.bind(file_uuid)
|
||||
.bind(page_size as i64)
|
||||
.bind(offset as i64)
|
||||
.fetch_all(db.pool())
|
||||
.await
|
||||
{
|
||||
Ok(rows) => rows,
|
||||
Err(e) => {
|
||||
return Err((
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("Query error: {}", e),
|
||||
))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let sql = format!(
|
||||
"SELECT id, face_id, file_uuid, frame_number, confidence, bbox, attributes
|
||||
FROM {}
|
||||
WHERE identity_id IS NULL AND confidence >= $1
|
||||
ORDER BY confidence DESC
|
||||
LIMIT $2 OFFSET $3",
|
||||
table
|
||||
);
|
||||
match sqlx::query_as::<
|
||||
_,
|
||||
(
|
||||
i32,
|
||||
Option<String>,
|
||||
String,
|
||||
i64,
|
||||
f64,
|
||||
Option<serde_json::Value>,
|
||||
Option<serde_json::Value>,
|
||||
),
|
||||
>(&sql)
|
||||
.bind(min_confidence)
|
||||
.bind(page_size as i64)
|
||||
.bind(offset as i64)
|
||||
.fetch_all(db.pool())
|
||||
.await
|
||||
{
|
||||
Ok(rows) => rows,
|
||||
Err(e) => {
|
||||
return Err((
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("Query error: {}", e),
|
||||
))
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let candidates: Vec<FaceCandidate> = rows
|
||||
.into_iter()
|
||||
.map(|r| FaceCandidate {
|
||||
id: r.0,
|
||||
face_id: r.1,
|
||||
file_uuid: r.2,
|
||||
frame_number: r.3,
|
||||
confidence: r.4,
|
||||
bbox: r.5,
|
||||
attributes: r.6,
|
||||
})
|
||||
.collect();
|
||||
|
||||
Ok(Json(FaceCandidatesResponse {
|
||||
candidates,
|
||||
total,
|
||||
page,
|
||||
page_size,
|
||||
}))
|
||||
}
|
||||
|
||||
async fn get_identity_faces(
|
||||
axum::extract::Path(identity_id): axum::extract::Path<i32>,
|
||||
Query(query): Query<IdentityFacesQuery>,
|
||||
) -> Result<Json<IdentityFacesResponse>, (StatusCode, String)> {
|
||||
let db = match PostgresDb::init().await {
|
||||
Ok(db) => db,
|
||||
Err(e) => {
|
||||
return Err((
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("Failed to connect to database: {}", e),
|
||||
))
|
||||
}
|
||||
};
|
||||
|
||||
let page_size = std::cmp::min(query.page_size.unwrap_or(100), 1000);
|
||||
let offset = (query.page.unwrap_or(1) - 1) * page_size;
|
||||
|
||||
let table = crate::core::db::schema::table_name("face_detections");
|
||||
|
||||
let count_sql = format!("SELECT COUNT(*) FROM {} WHERE identity_id = $1", table);
|
||||
|
||||
let total: i64 = match sqlx::query_scalar(&count_sql)
|
||||
.bind(identity_id)
|
||||
.fetch_one(db.pool())
|
||||
.await
|
||||
{
|
||||
Ok(count) => count,
|
||||
Err(e) => {
|
||||
return Err((
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("Count error: {}", e),
|
||||
))
|
||||
}
|
||||
};
|
||||
|
||||
let sql = format!(
|
||||
"SELECT id, face_id, file_uuid, frame_number, confidence, bbox, attributes
|
||||
FROM {}
|
||||
WHERE identity_id = $1
|
||||
ORDER BY confidence DESC
|
||||
LIMIT $2 OFFSET $3",
|
||||
table
|
||||
);
|
||||
|
||||
let rows = match sqlx::query_as::<
|
||||
_,
|
||||
(
|
||||
i32,
|
||||
Option<String>,
|
||||
String,
|
||||
i64,
|
||||
f64,
|
||||
Option<serde_json::Value>,
|
||||
Option<serde_json::Value>,
|
||||
),
|
||||
>(&sql)
|
||||
.bind(identity_id)
|
||||
.bind(page_size as i64)
|
||||
.bind(offset as i64)
|
||||
.fetch_all(db.pool())
|
||||
.await
|
||||
{
|
||||
Ok(rows) => rows,
|
||||
Err(e) => {
|
||||
return Err((
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("Query error: {}", e),
|
||||
))
|
||||
}
|
||||
};
|
||||
|
||||
let faces: Vec<IdentityFace> = rows
|
||||
.into_iter()
|
||||
.map(|r| IdentityFace {
|
||||
id: r.0,
|
||||
face_id: r.1,
|
||||
file_uuid: r.2,
|
||||
frame_number: r.3,
|
||||
confidence: r.4,
|
||||
bbox: r.5,
|
||||
attributes: r.6,
|
||||
})
|
||||
.collect();
|
||||
|
||||
Ok(Json(IdentityFacesResponse {
|
||||
identity_id,
|
||||
faces,
|
||||
total,
|
||||
}))
|
||||
}
|
||||
|
||||
async fn get_face_thumbnail(
|
||||
Path(face_id): Path<i32>,
|
||||
) -> Result<impl IntoResponse, (StatusCode, String)> {
|
||||
let db = match PostgresDb::init().await {
|
||||
Ok(db) => db,
|
||||
Err(e) => {
|
||||
return Err((
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("Failed to connect to database: {}", e),
|
||||
))
|
||||
}
|
||||
};
|
||||
|
||||
let table_fd = crate::core::db::schema::table_name("face_detections");
|
||||
let table_v = crate::core::db::schema::table_name("videos");
|
||||
|
||||
let sql = format!(
|
||||
"SELECT fd.frame_number, fd.bbox, v.file_path, v.fps
|
||||
FROM {} fd
|
||||
JOIN {} v ON fd.file_uuid = v.uuid
|
||||
WHERE fd.id = $1",
|
||||
table_fd, table_v
|
||||
);
|
||||
|
||||
let row: Option<(i64, Option<serde_json::Value>, String, f64)> = match sqlx::query_as(&sql)
|
||||
.bind(face_id)
|
||||
.fetch_optional(db.pool())
|
||||
.await
|
||||
{
|
||||
Ok(row) => row,
|
||||
Err(e) => {
|
||||
return Err((
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("Query error: {}", e),
|
||||
))
|
||||
}
|
||||
};
|
||||
|
||||
let (frame_number, bbox_json, file_path, fps) = match row {
|
||||
Some(r) => r,
|
||||
None => return Err((StatusCode::NOT_FOUND, format!("Face {} not found", face_id))),
|
||||
};
|
||||
|
||||
let bbox: Bbox = match bbox_json {
|
||||
Some(json) => serde_json::from_value(json).unwrap_or(Bbox {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 100,
|
||||
height: 100,
|
||||
}),
|
||||
None => Bbox {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 100,
|
||||
height: 100,
|
||||
},
|
||||
};
|
||||
|
||||
let timestamp = frame_number as f64 / fps;
|
||||
|
||||
let crop_filter = format!("crop={}:{}:{}:{}", bbox.width, bbox.height, bbox.x, bbox.y);
|
||||
|
||||
let output = match Command::new("ffmpeg")
|
||||
.args(&[
|
||||
"-ss",
|
||||
×tamp.to_string(),
|
||||
"-i",
|
||||
&file_path,
|
||||
"-vf",
|
||||
&crop_filter,
|
||||
"-frames:v",
|
||||
"1",
|
||||
"-f",
|
||||
"image2pipe",
|
||||
"-vcodec",
|
||||
"mjpeg",
|
||||
"-",
|
||||
])
|
||||
.output()
|
||||
{
|
||||
Ok(o) => o,
|
||||
Err(e) => {
|
||||
return Err((
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("ffmpeg error: {}", e),
|
||||
))
|
||||
}
|
||||
};
|
||||
|
||||
if !output.status.success() {
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
return Err((
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("ffmpeg failed: {}", stderr),
|
||||
));
|
||||
}
|
||||
|
||||
let response = axum::response::Response::builder()
|
||||
.status(StatusCode::OK)
|
||||
.header(header::CONTENT_TYPE, "image/jpeg")
|
||||
.header(header::CACHE_CONTROL, "public, max-age=3600")
|
||||
.body(Body::from(output.stdout))
|
||||
.map_err(|e| {
|
||||
(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("Response error: {}", e),
|
||||
)
|
||||
})?;
|
||||
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct Bbox {
|
||||
x: i32,
|
||||
y: i32,
|
||||
width: i32,
|
||||
height: i32,
|
||||
}
|
||||
|
||||
603
src/api/identity_agent_api.rs
Normal file
603
src/api/identity_agent_api.rs
Normal file
@@ -0,0 +1,603 @@
|
||||
use axum::{
|
||||
extract::State,
|
||||
http::StatusCode,
|
||||
response::Json,
|
||||
routing::{get, post},
|
||||
Router,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sqlx::Row;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use crate::api::server::AppState;
|
||||
|
||||
pub fn identity_agent_routes() -> Router<AppState> {
|
||||
Router::new()
|
||||
.route("/api/v1/agents/identity/analyze", post(analyze_identity))
|
||||
.route("/api/v1/agents/identity/suggest", post(suggest_merges))
|
||||
.route("/api/v1/agents/identity/status", get(get_identity_status))
|
||||
.route(
|
||||
"/api/v1/agents/suggest/clustering",
|
||||
post(suggest_clustering),
|
||||
)
|
||||
.route("/api/v1/agents/suggest/merge", post(suggest_merge))
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct AnalyzeIdentityRequest {
|
||||
pub file_uuid: String,
|
||||
pub auto_merge_threshold: Option<f64>,
|
||||
pub llm_threshold: Option<f64>,
|
||||
pub use_llm: Option<bool>,
|
||||
pub model: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct AnalyzeIdentityResponse {
|
||||
pub success: bool,
|
||||
pub file_uuid: String,
|
||||
pub identities: Vec<IdentityResult>,
|
||||
pub processing_status: IdentityProcessingStatus,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct IdentityResult {
|
||||
pub identity_id: String,
|
||||
pub person_ids: Vec<String>,
|
||||
pub speaker_ids: Vec<String>,
|
||||
pub confidence: f64,
|
||||
pub evidence: IdentityEvidence,
|
||||
pub reasoning: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct IdentityEvidence {
|
||||
pub face_similarity: Option<f64>,
|
||||
pub speaker_overlap: f64,
|
||||
pub time_overlap: f64,
|
||||
pub frame_ratio: f64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct IdentityProcessingStatus {
|
||||
pub status: String,
|
||||
pub persons_analyzed: i32,
|
||||
pub identities_created: i32,
|
||||
pub merges_suggested: i32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct SuggestMergesRequest {
|
||||
pub file_uuid: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct SuggestMergesResponse {
|
||||
pub success: bool,
|
||||
pub file_uuid: String,
|
||||
pub merge_suggestions: Vec<MergeSuggestion>,
|
||||
pub naming_suggestions: Vec<NamingSuggestion>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct MergeSuggestion {
|
||||
pub target_person_id: String,
|
||||
pub source_person_ids: Vec<String>,
|
||||
pub confidence: f64,
|
||||
pub reasons: Vec<String>,
|
||||
pub action: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct NamingSuggestion {
|
||||
pub person_id: String,
|
||||
pub suggested_name: String,
|
||||
pub confidence: f64,
|
||||
pub reasoning: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct IdentityStatusResponse {
|
||||
pub success: bool,
|
||||
pub agent_name: String,
|
||||
pub version: String,
|
||||
pub supported_models: Vec<String>,
|
||||
pub default_thresholds: DefaultThresholds,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct DefaultThresholds {
|
||||
pub auto_merge_threshold: f64,
|
||||
pub llm_threshold: f64,
|
||||
pub face_similarity_threshold: f64,
|
||||
}
|
||||
|
||||
async fn analyze_identity(
|
||||
State(state): State<AppState>,
|
||||
Json(req): Json<AnalyzeIdentityRequest>,
|
||||
) -> Result<Json<AnalyzeIdentityResponse>, (StatusCode, String)> {
|
||||
let output_dir = std::env::var("MOMENTRY_OUTPUT_DIR")
|
||||
.unwrap_or_else(|_| "/Users/accusys/momentry/output".to_string());
|
||||
|
||||
let video_dir = PathBuf::from(&output_dir).join(&req.file_uuid);
|
||||
|
||||
let face_clustered_path = video_dir.join(format!("{}.face_clustered.json", req.file_uuid));
|
||||
let asrx_path = video_dir.join(format!("{}.asrx.json", req.file_uuid));
|
||||
|
||||
if !face_clustered_path.exists() {
|
||||
return Err((
|
||||
StatusCode::NOT_FOUND,
|
||||
format!("Face clustered data not found for video: {}", req.file_uuid),
|
||||
));
|
||||
}
|
||||
|
||||
let face_data: serde_json::Value = std::fs::read_to_string(&face_clustered_path)
|
||||
.map_err(|e| {
|
||||
(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("Failed to read face data: {}", e),
|
||||
)
|
||||
})?
|
||||
.parse()
|
||||
.map_err(|e| {
|
||||
(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("Failed to parse face data: {}", e),
|
||||
)
|
||||
})?;
|
||||
|
||||
let asrx_data: Option<serde_json::Value> = if asrx_path.exists() {
|
||||
Some(
|
||||
std::fs::read_to_string(&asrx_path)
|
||||
.map_err(|e| {
|
||||
(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("Failed to read asrx data: {}", e),
|
||||
)
|
||||
})?
|
||||
.parse()
|
||||
.map_err(|e| {
|
||||
(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
format!("Failed to parse asrx data: {}", e),
|
||||
)
|
||||
})?,
|
||||
)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let persons = extract_persons_from_face_data(&face_data);
|
||||
let speakers = extract_speakers_from_asrx_data(&asrx_data);
|
||||
|
||||
let identities = analyze_person_speaker_overlap(&persons, &speakers);
|
||||
|
||||
let processing_status = IdentityProcessingStatus {
|
||||
status: "completed".to_string(),
|
||||
persons_analyzed: persons.len() as i32,
|
||||
identities_created: identities.len() as i32,
|
||||
merges_suggested: 0,
|
||||
};
|
||||
|
||||
Ok(Json(AnalyzeIdentityResponse {
|
||||
success: true,
|
||||
file_uuid: req.file_uuid.clone(),
|
||||
identities,
|
||||
processing_status,
|
||||
}))
|
||||
}
|
||||
|
||||
async fn suggest_merges(
|
||||
State(state): State<AppState>,
|
||||
Json(req): Json<SuggestMergesRequest>,
|
||||
) -> Result<Json<SuggestMergesResponse>, (StatusCode, String)> {
|
||||
let analyze_req = AnalyzeIdentityRequest {
|
||||
file_uuid: req.file_uuid.clone(),
|
||||
auto_merge_threshold: Some(0.8),
|
||||
llm_threshold: Some(0.5),
|
||||
use_llm: Some(true),
|
||||
model: Some("gemma4".to_string()),
|
||||
};
|
||||
|
||||
let analyze_result = analyze_identity(State(state), Json(analyze_req)).await?;
|
||||
|
||||
let merge_suggestions: Vec<MergeSuggestion> = analyze_result
|
||||
.identities
|
||||
.iter()
|
||||
.filter(|id| id.person_ids.len() > 1)
|
||||
.map(|id| {
|
||||
let reasons = vec![
|
||||
format!(
|
||||
"Shared speaker overlap: {:.0}%",
|
||||
id.evidence.speaker_overlap * 100.0
|
||||
),
|
||||
format!(
|
||||
"Face similarity: {:.2}",
|
||||
id.evidence.face_similarity.unwrap_or(0.0)
|
||||
),
|
||||
format!("Confidence: {:.2}", id.confidence),
|
||||
];
|
||||
|
||||
MergeSuggestion {
|
||||
target_person_id: id.person_ids[0].clone(),
|
||||
source_person_ids: id.person_ids[1..].to_vec(),
|
||||
confidence: id.confidence,
|
||||
reasons,
|
||||
action: if id.confidence > 0.8 {
|
||||
"auto_apply"
|
||||
} else {
|
||||
"review_needed"
|
||||
}
|
||||
.to_string(),
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
Ok(Json(SuggestMergesResponse {
|
||||
success: true,
|
||||
file_uuid: req.file_uuid,
|
||||
merge_suggestions,
|
||||
naming_suggestions: vec![],
|
||||
}))
|
||||
}
|
||||
|
||||
async fn get_identity_status() -> Result<Json<IdentityStatusResponse>, (StatusCode, String)> {
|
||||
Ok(Json(IdentityStatusResponse {
|
||||
success: true,
|
||||
agent_name: "Identity Agent".to_string(),
|
||||
version: "1.0.0".to_string(),
|
||||
supported_models: vec!["gemma4".to_string(), "qwen3".to_string()],
|
||||
default_thresholds: DefaultThresholds {
|
||||
auto_merge_threshold: 0.8,
|
||||
llm_threshold: 0.5,
|
||||
face_similarity_threshold: 0.3,
|
||||
},
|
||||
}))
|
||||
}
|
||||
|
||||
fn extract_persons_from_face_data(face_data: &serde_json::Value) -> Vec<PersonData> {
|
||||
let mut persons = Vec::new();
|
||||
|
||||
if let Some(frames) = face_data.get("frames").and_then(|f| f.as_array()) {
|
||||
let mut person_frames_map: std::collections::HashMap<String, Vec<i32>> =
|
||||
std::collections::HashMap::new();
|
||||
|
||||
for frame in frames {
|
||||
if let Some(frame_num) = frame.get("frame").and_then(|f| f.as_i64()) {
|
||||
if let Some(person_id) = frame.get("person_id").and_then(|p| p.as_str()) {
|
||||
person_frames_map
|
||||
.entry(person_id.to_string())
|
||||
.or_insert_with(Vec::new)
|
||||
.push(frame_num as i32);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (person_id, frames) in person_frames_map {
|
||||
persons.push(PersonData {
|
||||
person_id,
|
||||
frames,
|
||||
avg_embedding: None,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
persons
|
||||
}
|
||||
|
||||
fn extract_speakers_from_asrx_data(asrx_data: &Option<serde_json::Value>) -> Vec<SpeakerData> {
|
||||
let mut speakers = Vec::new();
|
||||
|
||||
if let Some(data) = asrx_data {
|
||||
if let Some(segments) = data.get("segments").and_then(|s| s.as_array()) {
|
||||
let mut speaker_segments_map: std::collections::HashMap<String, Vec<(f64, f64)>> =
|
||||
std::collections::HashMap::new();
|
||||
|
||||
for segment in segments {
|
||||
if let Some(speaker_id) = segment.get("speaker").and_then(|s| s.as_str()) {
|
||||
let start = segment.get("start").and_then(|s| s.as_f64()).unwrap_or(0.0);
|
||||
let end = segment.get("end").and_then(|e| e.as_f64()).unwrap_or(0.0);
|
||||
|
||||
speaker_segments_map
|
||||
.entry(speaker_id.to_string())
|
||||
.or_insert_with(Vec::new)
|
||||
.push((start, end));
|
||||
}
|
||||
}
|
||||
|
||||
for (speaker_id, segments) in speaker_segments_map {
|
||||
speakers.push(SpeakerData {
|
||||
speaker_id,
|
||||
segments,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
speakers
|
||||
}
|
||||
|
||||
fn analyze_person_speaker_overlap(
|
||||
persons: &[PersonData],
|
||||
speakers: &[SpeakerData],
|
||||
) -> Vec<IdentityResult> {
|
||||
let mut identities = Vec::new();
|
||||
|
||||
for (i, person) in persons.iter().enumerate() {
|
||||
let identity_id = format!("identity_{}", i + 1);
|
||||
|
||||
let mut speaker_ids = Vec::new();
|
||||
let mut max_overlap: f64 = 0.0;
|
||||
|
||||
for speaker in speakers {
|
||||
let overlap_frames = calculate_overlap(person, speaker);
|
||||
let overlap_ratio = overlap_frames as f64 / person.frames.len() as f64;
|
||||
|
||||
if overlap_ratio > 0.5 {
|
||||
speaker_ids.push(speaker.speaker_id.clone());
|
||||
max_overlap = max_overlap.max(overlap_ratio);
|
||||
}
|
||||
}
|
||||
|
||||
let confidence = if speaker_ids.len() > 0 {
|
||||
0.7 + max_overlap * 0.2
|
||||
} else {
|
||||
0.5
|
||||
};
|
||||
|
||||
let reasoning = if speaker_ids.len() > 0 {
|
||||
format!(
|
||||
"Person has high overlap with speakers: {}",
|
||||
speaker_ids.join(", ")
|
||||
)
|
||||
} else {
|
||||
"Person has no speaker overlap".to_string()
|
||||
};
|
||||
|
||||
identities.push(IdentityResult {
|
||||
identity_id,
|
||||
person_ids: vec![person.person_id.clone()],
|
||||
speaker_ids,
|
||||
confidence,
|
||||
evidence: IdentityEvidence {
|
||||
face_similarity: None,
|
||||
speaker_overlap: max_overlap,
|
||||
time_overlap: max_overlap,
|
||||
frame_ratio: person.frames.len() as f64 / 1000.0,
|
||||
},
|
||||
reasoning,
|
||||
});
|
||||
}
|
||||
|
||||
identities
|
||||
}
|
||||
|
||||
fn calculate_overlap(person: &PersonData, speaker: &SpeakerData) -> i32 {
|
||||
let mut overlap_count = 0;
|
||||
|
||||
for frame_num in &person.frames {
|
||||
let frame_time = *frame_num as f64 / 23.976;
|
||||
|
||||
for (start, end) in &speaker.segments {
|
||||
if frame_time >= *start && frame_time <= *end {
|
||||
overlap_count += 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
overlap_count
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct SuggestClusteringRequest {
|
||||
pub file_uuid: Option<String>,
|
||||
pub min_cluster_size: Option<usize>,
|
||||
pub similarity_threshold: Option<f64>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct SuggestClusteringResponse {
|
||||
pub success: bool,
|
||||
pub suggestions: Vec<ClusteringSuggestion>,
|
||||
pub total_unclustered: usize,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct ClusteringSuggestion {
|
||||
pub cluster_id: String,
|
||||
pub face_count: usize,
|
||||
pub avg_confidence: f64,
|
||||
pub suggested_name: Option<String>,
|
||||
pub representative_face: Option<String>,
|
||||
}
|
||||
|
||||
async fn suggest_clustering(
|
||||
State(state): State<AppState>,
|
||||
Json(req): Json<SuggestClusteringRequest>,
|
||||
) -> Result<Json<SuggestClusteringResponse>, (StatusCode, String)> {
|
||||
let min_cluster_size = req.min_cluster_size.unwrap_or(3);
|
||||
|
||||
let file_filter = match &req.file_uuid {
|
||||
Some(uuid) => format!("AND fc.file_uuid = '{}'", uuid),
|
||||
None => String::new(),
|
||||
};
|
||||
|
||||
let query = format!(
|
||||
r#"
|
||||
SELECT fc.cluster_id, fc.file_uuid, fc.n_faces, fc.metadata
|
||||
FROM face_clusters fc
|
||||
WHERE fc.n_faces >= $1
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM identities i
|
||||
WHERE i.metadata->>'cluster_id' = fc.cluster_id
|
||||
)
|
||||
{}
|
||||
ORDER BY fc.n_faces DESC
|
||||
"#,
|
||||
file_filter
|
||||
);
|
||||
|
||||
let pool = state.db.pool();
|
||||
let rows = sqlx::query(&query)
|
||||
.bind(min_cluster_size as i64)
|
||||
.fetch_all(pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
|
||||
|
||||
let suggestions: Vec<ClusteringSuggestion> = rows
|
||||
.into_iter()
|
||||
.map(|row| {
|
||||
let cluster_id: String = row.get("cluster_id");
|
||||
let n_faces: i32 = row.get("n_faces");
|
||||
let metadata: serde_json::Value =
|
||||
row.try_get("metadata").unwrap_or(serde_json::Value::Null);
|
||||
|
||||
let avg_confidence = metadata
|
||||
.get("avg_confidence")
|
||||
.and_then(|v| v.as_f64())
|
||||
.unwrap_or(0.0);
|
||||
|
||||
let representative_face = metadata
|
||||
.get("representative_face_id")
|
||||
.and_then(|v| v.as_str())
|
||||
.map(|s| s.to_string());
|
||||
|
||||
ClusteringSuggestion {
|
||||
cluster_id,
|
||||
face_count: n_faces as usize,
|
||||
avg_confidence,
|
||||
suggested_name: None,
|
||||
representative_face,
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
let total_unclustered: i64 = sqlx::query_scalar(
|
||||
r#"
|
||||
SELECT COUNT(*) FROM face_detections fd
|
||||
WHERE fd.identity_id IS NULL
|
||||
"#,
|
||||
)
|
||||
.fetch_one(pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
|
||||
|
||||
Ok(Json(SuggestClusteringResponse {
|
||||
success: true,
|
||||
suggestions,
|
||||
total_unclustered: total_unclustered as usize,
|
||||
}))
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct SuggestMergeRequest {
|
||||
pub identity_id: Option<String>,
|
||||
pub similarity_threshold: Option<f64>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct SuggestMergeResponse {
|
||||
pub success: bool,
|
||||
pub suggestions: Vec<IdentityMergeSuggestion>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct IdentityMergeSuggestion {
|
||||
pub source_identity_id: String,
|
||||
pub target_identity_id: String,
|
||||
pub source_name: String,
|
||||
pub target_name: String,
|
||||
pub similarity_score: f64,
|
||||
pub shared_files: usize,
|
||||
pub reason: String,
|
||||
}
|
||||
|
||||
async fn suggest_merge(
|
||||
State(state): State<AppState>,
|
||||
Json(req): Json<SuggestMergeRequest>,
|
||||
) -> Result<Json<SuggestMergeResponse>, (StatusCode, String)> {
|
||||
let similarity_threshold = req.similarity_threshold.unwrap_or(0.8);
|
||||
|
||||
let identity_filter = match &req.identity_id {
|
||||
Some(id) => format!("AND i1.uuid = '{}' OR i2.uuid = '{}'", id, id),
|
||||
None => String::new(),
|
||||
};
|
||||
|
||||
let query = format!(
|
||||
r#"
|
||||
SELECT
|
||||
i1.uuid as source_uuid,
|
||||
i2.uuid as target_uuid,
|
||||
i1.name as source_name,
|
||||
i2.name as target_name,
|
||||
COUNT(DISTINCT fi1.file_uuid) as shared_files
|
||||
FROM identities i1
|
||||
JOIN identities i2 ON i1.id < i2.id
|
||||
LEFT JOIN file_identities fi1 ON fi1.identity_id = i1.id
|
||||
LEFT JOIN file_identities fi2 ON fi2.identity_id = i2.id AND fi1.file_uuid = fi2.file_uuid
|
||||
WHERE i1.identity_type = 'people'
|
||||
AND i2.identity_type = 'people'
|
||||
AND i1.id != i2.id
|
||||
{}
|
||||
GROUP BY i1.uuid, i2.uuid, i1.name, i2.name
|
||||
HAVING COUNT(DISTINCT fi1.file_uuid) > 0
|
||||
ORDER BY shared_files DESC
|
||||
LIMIT 50
|
||||
"#,
|
||||
identity_filter
|
||||
);
|
||||
|
||||
let pool = state.db.pool();
|
||||
let rows = sqlx::query(&query)
|
||||
.fetch_all(pool)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
|
||||
|
||||
let suggestions: Vec<IdentityMergeSuggestion> = rows
|
||||
.into_iter()
|
||||
.filter_map(|row| {
|
||||
let shared_files: i64 = row.get("shared_files");
|
||||
if shared_files > 0 {
|
||||
let similarity = (shared_files as f64 / 10.0).min(1.0);
|
||||
if similarity >= similarity_threshold {
|
||||
Some(IdentityMergeSuggestion {
|
||||
source_identity_id: row.get("source_uuid"),
|
||||
target_identity_id: row.get("target_uuid"),
|
||||
source_name: row.get("source_name"),
|
||||
target_name: row.get("target_name"),
|
||||
similarity_score: similarity,
|
||||
shared_files: shared_files as usize,
|
||||
reason: format!(
|
||||
"Share {} file(s) - similarity: {:.1}%",
|
||||
shared_files,
|
||||
similarity * 100.0
|
||||
),
|
||||
})
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
Ok(Json(SuggestMergeResponse {
|
||||
success: true,
|
||||
suggestions,
|
||||
}))
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct PersonData {
|
||||
person_id: String,
|
||||
frames: Vec<i32>,
|
||||
avg_embedding: Option<Vec<f64>>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct SpeakerData {
|
||||
speaker_id: String,
|
||||
segments: Vec<(f64, f64)>,
|
||||
}
|
||||
@@ -8,17 +8,27 @@ use axum::{
|
||||
use serde::{Deserialize, Serialize};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::core::db::{Database, PostgresDb, ResourceRecord};
|
||||
use crate::core::db::ResourceRecord;
|
||||
|
||||
pub fn identity_routes() -> Router<crate::api::server::AppState> {
|
||||
Router::new()
|
||||
.route("/api/v1/people", get(list_people))
|
||||
.route("/api/v1/people/search", post(search_people))
|
||||
.route("/api/v1/people/candidates", get(list_candidates))
|
||||
.route("/api/v1/people/{identity_id}/confirm-candidate", post(confirm_candidate))
|
||||
.route("/api/v1/people/{identity_id}/reject-candidate", post(reject_candidate))
|
||||
.route(
|
||||
"/api/v1/people/:identity_id/confirm-candidate",
|
||||
post(confirm_candidate),
|
||||
)
|
||||
.route(
|
||||
"/api/v1/people/:identity_id/reject-candidate",
|
||||
post(reject_candidate),
|
||||
)
|
||||
.route("/api/v1/files", get(list_files))
|
||||
.route("/api/v1/files/{uuid}", get(get_file_detail))
|
||||
.route("/api/v1/files/:uuid", get(get_file_detail))
|
||||
.route("/api/v1/files/:uuid/identities", get(get_file_identities))
|
||||
.route("/api/v1/identities/:uuid", get(get_identity_detail))
|
||||
.route("/api/v1/identities/:uuid/files", get(get_identity_files))
|
||||
.route("/api/v1/identities/:uuid/chunks", get(get_identity_chunks))
|
||||
.route("/api/v1/resources/register", post(register_resource))
|
||||
.route("/api/v1/resources/heartbeat", post(heartbeat_resource))
|
||||
.route("/api/v1/resources", get(list_resources))
|
||||
@@ -59,18 +69,24 @@ async fn list_people(
|
||||
let page_size = params.page_size.unwrap_or(20);
|
||||
let offset = ((page - 1) as i64) * (page_size as i64);
|
||||
|
||||
let records = state.db.list_people(page_size as i32, offset).await
|
||||
let records = state
|
||||
.db
|
||||
.list_people(page_size as i32, offset)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
|
||||
|
||||
// TODO: Get total count
|
||||
let total = 100; // Placeholder
|
||||
|
||||
let data = records.into_iter().map(|r| PeopleItem {
|
||||
identity_id: r.uuid,
|
||||
name: r.name,
|
||||
metadata: r.metadata,
|
||||
created_at: r.created_at,
|
||||
}).collect();
|
||||
let data = records
|
||||
.into_iter()
|
||||
.map(|r| PeopleItem {
|
||||
identity_id: r.uuid,
|
||||
name: r.name,
|
||||
metadata: r.metadata,
|
||||
created_at: r.created_at,
|
||||
})
|
||||
.collect();
|
||||
|
||||
Ok(Json(PeopleResponse {
|
||||
success: true,
|
||||
@@ -96,15 +112,21 @@ async fn search_people(
|
||||
let page_size = req.page_size.unwrap_or(20);
|
||||
let offset = ((page - 1) as i64) * (page_size as i64);
|
||||
|
||||
let records = state.db.search_people(&req.query, page_size as i32, offset).await
|
||||
let records = state
|
||||
.db
|
||||
.search_people(&req.query, page_size as i32, offset)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
|
||||
|
||||
let data: Vec<PeopleItem> = records.into_iter().map(|r| PeopleItem {
|
||||
identity_id: r.uuid,
|
||||
name: r.name,
|
||||
metadata: r.metadata,
|
||||
created_at: r.created_at,
|
||||
}).collect();
|
||||
let data: Vec<PeopleItem> = records
|
||||
.into_iter()
|
||||
.map(|r| PeopleItem {
|
||||
identity_id: r.uuid,
|
||||
name: r.name,
|
||||
metadata: r.metadata,
|
||||
created_at: r.created_at,
|
||||
})
|
||||
.collect();
|
||||
|
||||
Ok(Json(PeopleResponse {
|
||||
success: true,
|
||||
@@ -145,14 +167,20 @@ async fn list_candidates(
|
||||
let page_size = params.page_size.unwrap_or(20);
|
||||
let offset = ((page - 1) as i64) * (page_size as i64);
|
||||
|
||||
let records = state.db.get_people_candidates(page_size as i32, offset).await
|
||||
let records = state
|
||||
.db
|
||||
.get_people_candidates(page_size as i32, offset)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
|
||||
|
||||
let data = records.into_iter().map(|r| CandidateItem {
|
||||
pre_chunk_id: r.id,
|
||||
file_uuid: r.file_uuid,
|
||||
data: r.data,
|
||||
}).collect();
|
||||
let data = records
|
||||
.into_iter()
|
||||
.map(|r| CandidateItem {
|
||||
pre_chunk_id: r.id,
|
||||
file_uuid: r.file_uuid,
|
||||
data: r.data,
|
||||
})
|
||||
.collect();
|
||||
|
||||
Ok(Json(CandidatesResponse {
|
||||
success: true,
|
||||
@@ -184,7 +212,10 @@ async fn confirm_candidate(
|
||||
let identity_id = Uuid::parse_str(&identity_id_str)
|
||||
.map_err(|e| (StatusCode::BAD_REQUEST, format!("Invalid UUID: {}", e)))?;
|
||||
|
||||
state.db.confirm_candidate(req.pre_chunk_id, identity_id).await
|
||||
state
|
||||
.db
|
||||
.confirm_candidate(req.pre_chunk_id, identity_id)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
|
||||
|
||||
Ok(Json(ConfirmCandidateResponse {
|
||||
@@ -198,7 +229,10 @@ async fn reject_candidate(
|
||||
Path(_identity_id_str): Path<String>, // Unused, but consistent with route
|
||||
Json(req): Json<ConfirmCandidateRequest>,
|
||||
) -> Result<Json<ConfirmCandidateResponse>, (StatusCode, String)> {
|
||||
state.db.reject_candidate(req.pre_chunk_id).await
|
||||
state
|
||||
.db
|
||||
.reject_candidate(req.pre_chunk_id)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
|
||||
|
||||
Ok(Json(ConfirmCandidateResponse {
|
||||
@@ -240,15 +274,21 @@ async fn list_files(
|
||||
let page_size = params.page_size.unwrap_or(20);
|
||||
let offset = ((page - 1) as i64) * (page_size as i64);
|
||||
|
||||
let records = state.db.list_files(page_size as i32, offset).await
|
||||
let records = state
|
||||
.db
|
||||
.list_files(page_size as i32, offset)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
|
||||
|
||||
let data = records.into_iter().map(|r| FileItem {
|
||||
file_uuid: r.uuid,
|
||||
file_name: r.file_name,
|
||||
file_path: r.file_path,
|
||||
status: "ready".to_string(),
|
||||
}).collect();
|
||||
let data = records
|
||||
.into_iter()
|
||||
.map(|r| FileItem {
|
||||
file_uuid: r.file_uuid,
|
||||
file_name: r.file_name,
|
||||
file_path: r.file_path,
|
||||
status: "ready".to_string(),
|
||||
})
|
||||
.collect();
|
||||
|
||||
Ok(Json(FilesResponse {
|
||||
success: true,
|
||||
@@ -261,23 +301,349 @@ async fn list_files(
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct FileDetailResponse {
|
||||
pub success: bool,
|
||||
pub file_uuid: String,
|
||||
pub file_name: String,
|
||||
pub file_path: String,
|
||||
pub metadata: serde_json::Value,
|
||||
pub metadata: Option<serde_json::Value>,
|
||||
pub created_at: Option<chrono::DateTime<chrono::Utc>>,
|
||||
}
|
||||
|
||||
async fn get_file_detail(
|
||||
State(state): State<crate::api::server::AppState>,
|
||||
Path(uuid): Path<String>,
|
||||
) -> Result<Json<FileDetailResponse>, (StatusCode, String)> {
|
||||
// Need a method to get single file
|
||||
// For now, placeholder
|
||||
Ok(Json(FileDetailResponse {
|
||||
let file = state
|
||||
.db
|
||||
.get_file_by_uuid(&uuid)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
|
||||
|
||||
match file {
|
||||
Some(f) => Ok(Json(FileDetailResponse {
|
||||
success: true,
|
||||
file_uuid: f.file_uuid,
|
||||
file_name: f.file_name,
|
||||
file_path: f.file_path,
|
||||
metadata: f.probe_json,
|
||||
created_at: f.created_at,
|
||||
})),
|
||||
None => Err((StatusCode::NOT_FOUND, format!("File not found: {}", uuid))),
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct FileIdentitiesResponse {
|
||||
pub success: bool,
|
||||
pub file_uuid: String,
|
||||
pub total: i64,
|
||||
pub page: usize,
|
||||
pub page_size: usize,
|
||||
pub data: Vec<FileIdentityItem>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct FileIdentityItem {
|
||||
pub identity_id: i32,
|
||||
pub name: String,
|
||||
pub metadata: serde_json::Value,
|
||||
pub face_count: Option<i32>,
|
||||
pub speaker_count: Option<i32>,
|
||||
pub first_appearance: Option<f64>,
|
||||
pub last_appearance: Option<f64>,
|
||||
pub confidence: Option<f64>,
|
||||
}
|
||||
|
||||
async fn get_file_identities(
|
||||
State(state): State<crate::api::server::AppState>,
|
||||
Path(uuid): Path<String>,
|
||||
Query(params): Query<FilesQuery>,
|
||||
) -> Result<Json<FileIdentitiesResponse>, (StatusCode, String)> {
|
||||
let page = params.page.unwrap_or(1);
|
||||
let page_size = params.page_size.unwrap_or(20);
|
||||
let offset = ((page - 1) as i64) * (page_size as i64);
|
||||
|
||||
let records = state
|
||||
.db
|
||||
.get_file_identities(&uuid, page_size as i32, offset)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
|
||||
|
||||
let data: Vec<FileIdentityItem> = records
|
||||
.into_iter()
|
||||
.map(|r| FileIdentityItem {
|
||||
identity_id: r.identity_id,
|
||||
name: r.name,
|
||||
metadata: r.metadata,
|
||||
face_count: r.face_count,
|
||||
speaker_count: r.speaker_count,
|
||||
first_appearance: r.first_appearance,
|
||||
last_appearance: r.last_appearance,
|
||||
confidence: r.confidence,
|
||||
})
|
||||
.collect();
|
||||
|
||||
Ok(Json(FileIdentitiesResponse {
|
||||
success: true,
|
||||
file_uuid: uuid,
|
||||
file_name: "Unknown".to_string(),
|
||||
file_path: "/path/to/file".to_string(),
|
||||
metadata: serde_json::json!({}),
|
||||
total: data.len() as i64,
|
||||
page,
|
||||
page_size,
|
||||
data,
|
||||
}))
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct IdentityDetailResponse {
|
||||
pub success: bool,
|
||||
pub uuid: Uuid,
|
||||
pub name: String,
|
||||
pub identity_type: Option<String>,
|
||||
pub source: Option<String>,
|
||||
pub status: Option<String>,
|
||||
pub metadata: serde_json::Value,
|
||||
pub reference_data: serde_json::Value,
|
||||
pub tmdb_id: Option<i32>,
|
||||
pub tmdb_profile: Option<String>,
|
||||
pub created_at: Option<chrono::DateTime<chrono::Utc>>,
|
||||
pub updated_at: Option<chrono::DateTime<chrono::Utc>>,
|
||||
}
|
||||
|
||||
async fn get_identity_detail(
|
||||
State(state): State<crate::api::server::AppState>,
|
||||
Path(uuid_str): Path<String>,
|
||||
) -> Result<Json<IdentityDetailResponse>, (StatusCode, String)> {
|
||||
let uuid = Uuid::parse_str(&uuid_str)
|
||||
.map_err(|e| (StatusCode::BAD_REQUEST, format!("Invalid UUID: {}", e)))?;
|
||||
|
||||
let identity = state
|
||||
.db
|
||||
.get_identity_by_uuid(&uuid)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
|
||||
|
||||
match identity {
|
||||
Some(i) => Ok(Json(IdentityDetailResponse {
|
||||
success: true,
|
||||
uuid: i.uuid,
|
||||
name: i.name,
|
||||
identity_type: i.identity_type,
|
||||
source: i.source,
|
||||
status: i.status,
|
||||
metadata: i.metadata,
|
||||
reference_data: i.reference_data,
|
||||
tmdb_id: i.tmdb_id,
|
||||
tmdb_profile: i.tmdb_profile,
|
||||
created_at: i.created_at,
|
||||
updated_at: i.updated_at,
|
||||
})),
|
||||
None => Err((
|
||||
StatusCode::NOT_FOUND,
|
||||
format!("Identity not found: {}", uuid),
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct IdentityFilesResponse {
|
||||
pub success: bool,
|
||||
pub identity_uuid: Uuid,
|
||||
pub total: i64,
|
||||
pub page: usize,
|
||||
pub page_size: usize,
|
||||
pub data: Vec<IdentityFileItem>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct IdentityFileItem {
|
||||
pub file_uuid: String,
|
||||
pub file_name: String,
|
||||
pub file_path: String,
|
||||
pub status: String,
|
||||
pub face_count: Option<i32>,
|
||||
pub speaker_count: Option<i32>,
|
||||
pub first_appearance: Option<f64>,
|
||||
pub last_appearance: Option<f64>,
|
||||
pub confidence: Option<f64>,
|
||||
}
|
||||
|
||||
async fn get_identity_files(
|
||||
State(state): State<crate::api::server::AppState>,
|
||||
Path(uuid_str): Path<String>,
|
||||
Query(params): Query<FilesQuery>,
|
||||
) -> Result<Json<IdentityFilesResponse>, (StatusCode, String)> {
|
||||
let uuid = Uuid::parse_str(&uuid_str)
|
||||
.map_err(|e| (StatusCode::BAD_REQUEST, format!("Invalid UUID: {}", e)))?;
|
||||
|
||||
let page = params.page.unwrap_or(1);
|
||||
let page_size = params.page_size.unwrap_or(20);
|
||||
let offset = ((page - 1) as i64) * (page_size as i64);
|
||||
|
||||
let records = state
|
||||
.db
|
||||
.get_identity_files(&uuid, page_size as i32, offset)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
|
||||
|
||||
let data: Vec<IdentityFileItem> = records
|
||||
.into_iter()
|
||||
.map(|r| IdentityFileItem {
|
||||
file_uuid: r.file_uuid,
|
||||
file_name: r.file_name,
|
||||
file_path: r.file_path,
|
||||
status: r.status,
|
||||
face_count: r.face_count,
|
||||
speaker_count: r.speaker_count,
|
||||
first_appearance: r.first_appearance,
|
||||
last_appearance: r.last_appearance,
|
||||
confidence: r.confidence,
|
||||
})
|
||||
.collect();
|
||||
|
||||
Ok(Json(IdentityFilesResponse {
|
||||
success: true,
|
||||
identity_uuid: uuid,
|
||||
total: data.len() as i64,
|
||||
page,
|
||||
page_size,
|
||||
data,
|
||||
}))
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct IdentityFacesResponse {
|
||||
pub success: bool,
|
||||
pub identity_uuid: Uuid,
|
||||
pub total: i64,
|
||||
pub page: usize,
|
||||
pub page_size: usize,
|
||||
pub data: Vec<IdentityFaceItem>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct IdentityFaceItem {
|
||||
pub id: i64,
|
||||
pub file_uuid: String,
|
||||
pub frame_number: i64,
|
||||
pub timestamp_secs: f64,
|
||||
pub face_id: Option<String>,
|
||||
pub bbox: BBox,
|
||||
pub confidence: f64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct BBox {
|
||||
pub x: f64,
|
||||
pub y: f64,
|
||||
pub width: f64,
|
||||
pub height: f64,
|
||||
}
|
||||
|
||||
async fn get_identity_faces(
|
||||
State(state): State<crate::api::server::AppState>,
|
||||
Path(uuid_str): Path<String>,
|
||||
Query(params): Query<FilesQuery>,
|
||||
) -> Result<Json<IdentityFacesResponse>, (StatusCode, String)> {
|
||||
let uuid = Uuid::parse_str(&uuid_str)
|
||||
.map_err(|e| (StatusCode::BAD_REQUEST, format!("Invalid UUID: {}", e)))?;
|
||||
|
||||
let page = params.page.unwrap_or(1);
|
||||
let page_size = params.page_size.unwrap_or(50);
|
||||
let offset = ((page - 1) as i64) * (page_size as i64);
|
||||
|
||||
let records = state
|
||||
.db
|
||||
.get_identity_faces(&uuid, page_size as i32, offset)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
|
||||
|
||||
let data: Vec<IdentityFaceItem> = records
|
||||
.into_iter()
|
||||
.map(|r| IdentityFaceItem {
|
||||
id: r.id,
|
||||
file_uuid: r.file_uuid,
|
||||
frame_number: r.frame_number,
|
||||
timestamp_secs: r.timestamp_secs,
|
||||
face_id: r.face_id,
|
||||
bbox: BBox {
|
||||
x: r.x,
|
||||
y: r.y,
|
||||
width: r.width,
|
||||
height: r.height,
|
||||
},
|
||||
confidence: r.confidence,
|
||||
})
|
||||
.collect();
|
||||
|
||||
Ok(Json(IdentityFacesResponse {
|
||||
success: true,
|
||||
identity_uuid: uuid,
|
||||
total: data.len() as i64,
|
||||
page,
|
||||
page_size,
|
||||
data,
|
||||
}))
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct IdentityChunksResponse {
|
||||
pub success: bool,
|
||||
pub identity_uuid: Uuid,
|
||||
pub total: i64,
|
||||
pub page: usize,
|
||||
pub page_size: usize,
|
||||
pub data: Vec<IdentityChunkItem>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct IdentityChunkItem {
|
||||
pub id: i64,
|
||||
pub file_uuid: String,
|
||||
pub chunk_id: String,
|
||||
pub chunk_type: String,
|
||||
pub start_time: Option<f64>,
|
||||
pub end_time: Option<f64>,
|
||||
pub text_content: Option<String>,
|
||||
}
|
||||
|
||||
async fn get_identity_chunks(
|
||||
State(state): State<crate::api::server::AppState>,
|
||||
Path(uuid_str): Path<String>,
|
||||
Query(params): Query<FilesQuery>,
|
||||
) -> Result<Json<IdentityChunksResponse>, (StatusCode, String)> {
|
||||
let uuid = Uuid::parse_str(&uuid_str)
|
||||
.map_err(|e| (StatusCode::BAD_REQUEST, format!("Invalid UUID: {}", e)))?;
|
||||
|
||||
let page = params.page.unwrap_or(1);
|
||||
let page_size = params.page_size.unwrap_or(20);
|
||||
let offset = ((page - 1) as i64) * (page_size as i64);
|
||||
|
||||
let records = state
|
||||
.db
|
||||
.get_identity_chunks(&uuid, page_size as i32, offset)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
|
||||
|
||||
let data: Vec<IdentityChunkItem> = records
|
||||
.into_iter()
|
||||
.map(|r| IdentityChunkItem {
|
||||
id: r.id,
|
||||
file_uuid: r.file_uuid,
|
||||
chunk_id: r.chunk_id,
|
||||
chunk_type: r.chunk_type,
|
||||
start_time: r.start_time,
|
||||
end_time: r.end_time,
|
||||
text_content: r.text_content,
|
||||
})
|
||||
.collect();
|
||||
|
||||
Ok(Json(IdentityChunksResponse {
|
||||
success: true,
|
||||
identity_uuid: uuid,
|
||||
total: data.len() as i64,
|
||||
page,
|
||||
page_size,
|
||||
data,
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -326,7 +692,10 @@ async fn register_resource(
|
||||
created_at: None,
|
||||
};
|
||||
|
||||
state.db.register_resource(resource).await
|
||||
state
|
||||
.db
|
||||
.register_resource(resource)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
|
||||
|
||||
Ok(Json(ResourceResponse {
|
||||
@@ -347,7 +716,10 @@ async fn heartbeat_resource(
|
||||
Json(req): Json<HeartbeatRequest>,
|
||||
) -> Result<Json<ResourceResponse>, (StatusCode, String)> {
|
||||
let status = req.status.unwrap_or("online".to_string());
|
||||
state.db.heartbeat_resource(&req.resource_id, &status).await
|
||||
state
|
||||
.db
|
||||
.heartbeat_resource(&req.resource_id, &status)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
|
||||
|
||||
Ok(Json(ResourceResponse {
|
||||
@@ -360,17 +732,23 @@ async fn heartbeat_resource(
|
||||
async fn list_resources(
|
||||
State(state): State<crate::api::server::AppState>,
|
||||
) -> Result<Json<ResourceResponse>, (StatusCode, String)> {
|
||||
let records = state.db.list_resources().await
|
||||
let records = state
|
||||
.db
|
||||
.list_resources()
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
|
||||
|
||||
let data: Vec<ResourceItem> = records.into_iter().map(|r| ResourceItem {
|
||||
resource_id: r.resource_id,
|
||||
resource_type: r.resource_type,
|
||||
category: r.category,
|
||||
capabilities: r.capabilities,
|
||||
status: r.status,
|
||||
last_heartbeat: r.last_heartbeat,
|
||||
}).collect();
|
||||
let data: Vec<ResourceItem> = records
|
||||
.into_iter()
|
||||
.map(|r| ResourceItem {
|
||||
resource_id: r.resource_id,
|
||||
resource_type: r.resource_type,
|
||||
category: r.category,
|
||||
capabilities: r.capabilities,
|
||||
status: r.status,
|
||||
last_heartbeat: r.last_heartbeat,
|
||||
})
|
||||
.collect();
|
||||
|
||||
Ok(Json(ResourceResponse {
|
||||
success: true,
|
||||
|
||||
@@ -211,7 +211,7 @@ pub async fn get_signal_timeline(
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct AVSuggestRequest {
|
||||
pub video_uuid: String,
|
||||
pub file_uuid: String,
|
||||
pub overlap_threshold: Option<f64>, // default 0.6
|
||||
}
|
||||
|
||||
@@ -233,7 +233,7 @@ pub async fn suggest_audio_visual_bindings(
|
||||
|
||||
// 1. Get Face signals and their time ranges
|
||||
let face_signals = db
|
||||
.list_unbound_signals(&req.video_uuid, "face")
|
||||
.list_unbound_signals(&req.file_uuid, "face")
|
||||
.await
|
||||
.map_err(|e| {
|
||||
(
|
||||
@@ -243,7 +243,7 @@ pub async fn suggest_audio_visual_bindings(
|
||||
})?;
|
||||
|
||||
let speaker_signals = db
|
||||
.list_unbound_signals(&req.video_uuid, "speaker")
|
||||
.list_unbound_signals(&req.file_uuid, "speaker")
|
||||
.await
|
||||
.map_err(|e| {
|
||||
(
|
||||
@@ -263,11 +263,11 @@ pub async fn suggest_audio_visual_bindings(
|
||||
|
||||
// Placeholder: Calculate overlap by fetching timelines
|
||||
let face_timeline = db
|
||||
.get_chunks_by_signal(&req.video_uuid, "face", face_id)
|
||||
.get_chunks_by_signal(&req.file_uuid, "face", face_id)
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
let speaker_timeline = db
|
||||
.get_chunks_by_signal(&req.video_uuid, "speaker", speaker_id)
|
||||
.get_chunks_by_signal(&req.file_uuid, "speaker", speaker_id)
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
|
||||
|
||||
@@ -21,13 +21,27 @@ pub struct ApiState {
|
||||
pub db: Arc<PostgresDb>,
|
||||
}
|
||||
|
||||
const PUBLIC_PATHS: &[&str] = &[
|
||||
"/api/v1/faces/", // Thumbnail paths (partial match)
|
||||
];
|
||||
|
||||
fn is_public_path(path: &str) -> bool {
|
||||
PUBLIC_PATHS.iter().any(|prefix| path.starts_with(prefix)) && path.ends_with("/thumbnail")
|
||||
}
|
||||
|
||||
pub async fn api_key_validation(
|
||||
State(state): State<ApiState>,
|
||||
request: Request,
|
||||
next: Next,
|
||||
) -> Response {
|
||||
let path = request.uri().path();
|
||||
tracing::info!("[MIDDLEWARE] Starting API key validation");
|
||||
tracing::info!("[MIDDLEWARE] Path: {:?}", request.uri().path());
|
||||
tracing::info!("[MIDDLEWARE] Path: {:?}", path);
|
||||
|
||||
if is_public_path(path) {
|
||||
tracing::info!("[MIDDLEWARE] Public path, skipping auth: {}", path);
|
||||
return next.run(request).await;
|
||||
}
|
||||
|
||||
let headers = request.headers();
|
||||
tracing::info!("[MIDDLEWARE] All headers: {:?}", headers);
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
pub mod agent_api;
|
||||
pub mod face_recognition;
|
||||
pub mod five_w1h_agent_api;
|
||||
pub mod identities;
|
||||
pub mod identity_agent_api;
|
||||
pub mod identity_api;
|
||||
pub mod identity_binding;
|
||||
pub mod middleware;
|
||||
@@ -8,6 +10,7 @@ pub mod n8n_search;
|
||||
pub mod person_identity;
|
||||
pub mod search;
|
||||
pub mod server;
|
||||
pub mod snapshot_api;
|
||||
pub mod universal_search;
|
||||
pub mod visual_chunk_search;
|
||||
pub mod who;
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
use crate::core::db::{Bm25Result, PostgresDb};
|
||||
use reqwest::Client;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashSet;
|
||||
|
||||
@@ -35,7 +34,7 @@ pub async fn n8n_search_smart(
|
||||
req: SmartSearchRequest,
|
||||
) -> Result<SmartSearchResponse, Box<dyn std::error::Error + Send + Sync>> {
|
||||
let limit = req.limit.unwrap_or(10);
|
||||
let video_uuid = req.uuid.clone();
|
||||
let file_uuid = req.uuid.clone();
|
||||
|
||||
// 1. Call LLM to extract 5W1H (Fallback to keywords if LLM fails)
|
||||
let dimensions = match parse_query_with_llm(&req.query).await {
|
||||
@@ -93,10 +92,7 @@ pub async fn n8n_search_smart(
|
||||
|
||||
// A. Keyword Search (BM25)
|
||||
if !keywords.is_empty() {
|
||||
if let Ok(results) = db
|
||||
.search_bm25(&keywords, video_uuid.as_deref(), limit)
|
||||
.await
|
||||
{
|
||||
if let Ok(results) = db.search_bm25(&keywords, file_uuid.as_deref(), limit).await {
|
||||
for sr in results {
|
||||
add_hit(&mut hits, &mut seen_chunk_ids, sr, 1.0);
|
||||
}
|
||||
@@ -106,7 +102,7 @@ pub async fn n8n_search_smart(
|
||||
// B. Who Search (Person Matching)
|
||||
if let Some(who_query) = &dimensions.who {
|
||||
// 1. Search Person
|
||||
if let Ok(persons) = db.search_person_candidates(who_query, &video_uuid, 5).await {
|
||||
if let Ok(persons) = db.search_person_candidates(who_query, &file_uuid, 5).await {
|
||||
if !persons.is_empty() {
|
||||
let person_id = persons[0]
|
||||
.get("candidate_id")
|
||||
@@ -122,7 +118,7 @@ pub async fn n8n_search_smart(
|
||||
|
||||
// Re-run BM25 with person name to find specific chunks and boost them
|
||||
if let Ok(results) = db
|
||||
.search_bm25(person_name, video_uuid.as_deref(), limit)
|
||||
.search_bm25(person_name, file_uuid.as_deref(), limit)
|
||||
.await
|
||||
{
|
||||
for sr in results {
|
||||
|
||||
@@ -10,7 +10,6 @@ use axum::{
|
||||
use serde::{Deserialize, Serialize};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::core::db::schema;
|
||||
use crate::core::db::{Database, PostgresDb};
|
||||
use crate::core::person_identity::{
|
||||
ChunkPersonInfo, CreatePersonIdentityRequest, PersonIdentity, PersonIdentityResponse,
|
||||
@@ -20,7 +19,7 @@ use crate::core::person_identity::{
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct IdentifyPersonsRequest {
|
||||
pub video_uuid: String,
|
||||
pub file_uuid: String,
|
||||
pub auto_match: Option<bool>,
|
||||
pub match_threshold: Option<f64>,
|
||||
}
|
||||
@@ -39,19 +38,19 @@ pub struct FaceListQuery {
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct VideoUuidQuery {
|
||||
pub video_uuid: String,
|
||||
pub file_uuid: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct PersonTimelineQuery {
|
||||
pub video_uuid: String,
|
||||
pub file_uuid: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct FaceThumbnailQuery {
|
||||
pub video_uuid: String,
|
||||
pub file_uuid: String,
|
||||
#[serde(default)]
|
||||
pub index: Option<usize>, // Which face detection to use (default: 0)
|
||||
pub index: Option<usize>,
|
||||
}
|
||||
|
||||
// Structs for parsing face_clustered.json
|
||||
@@ -85,7 +84,7 @@ pub struct ChunkPersonsResponse {
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct MergePersonsRequest {
|
||||
pub video_uuid: String,
|
||||
pub file_uuid: String,
|
||||
pub target_person_id: String,
|
||||
pub source_person_ids: Vec<String>,
|
||||
}
|
||||
@@ -115,7 +114,7 @@ pub struct MergeHistoryResponse {
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct PersonListQuery {
|
||||
pub video_uuid: String,
|
||||
pub file_uuid: String,
|
||||
pub page: Option<usize>,
|
||||
pub page_size: Option<usize>,
|
||||
pub min_appearances: Option<i32>,
|
||||
@@ -124,13 +123,13 @@ pub struct PersonListQuery {
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct AutoIdentifyRequest {
|
||||
pub video_uuid: String,
|
||||
pub file_uuid: String,
|
||||
pub min_speaker_confidence: Option<f64>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct SimilarPersonsQuery {
|
||||
pub video_uuid: String,
|
||||
pub file_uuid: String,
|
||||
pub threshold: Option<f64>,
|
||||
pub limit: Option<i32>,
|
||||
}
|
||||
@@ -210,7 +209,7 @@ pub struct AutoIdentifyResponse {
|
||||
}
|
||||
|
||||
pub struct AggregateBySpeakerRequest {
|
||||
pub video_uuid: String,
|
||||
pub file_uuid: String,
|
||||
pub auto_merge: bool, // If true, automatically merge duplicates
|
||||
}
|
||||
|
||||
@@ -311,14 +310,14 @@ async fn identify_persons(
|
||||
|
||||
tracing::info!(
|
||||
"[PERSON_IDENTITY] Identifying persons for video: {}",
|
||||
request.video_uuid
|
||||
request.file_uuid
|
||||
);
|
||||
|
||||
let auto_match = request.auto_match.unwrap_or(true);
|
||||
let threshold = request.match_threshold.unwrap_or(0.5);
|
||||
|
||||
if auto_match {
|
||||
let matches = match auto_match_face_speaker(&db, &request.video_uuid, threshold).await {
|
||||
let matches = match auto_match_face_speaker(&db, &request.file_uuid, threshold).await {
|
||||
Ok(m) => m,
|
||||
Err(e) => {
|
||||
return Err((
|
||||
@@ -333,7 +332,7 @@ async fn identify_persons(
|
||||
let person = match create_person_identity(
|
||||
&db,
|
||||
CreatePersonIdentityRequest {
|
||||
video_uuid: request.video_uuid.clone(),
|
||||
file_uuid: request.file_uuid.clone(),
|
||||
face_identity_id: None,
|
||||
speaker_id: Some(match_result.speaker_id.clone()),
|
||||
name: None,
|
||||
@@ -372,7 +371,7 @@ async fn identify_persons(
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct PersonDetailQuery {
|
||||
pub video_uuid: String,
|
||||
pub file_uuid: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
@@ -381,7 +380,7 @@ pub struct SearchIdentitiesRequest {
|
||||
pub speaker_id: Option<String>,
|
||||
pub gender: Option<String>,
|
||||
pub min_appearances: Option<i32>,
|
||||
pub video_uuid: Option<String>, // Optional: search only in specific video
|
||||
pub file_uuid: Option<String>, // Optional: search only in specific video
|
||||
pub limit: Option<usize>,
|
||||
}
|
||||
|
||||
@@ -404,7 +403,7 @@ async fn search_identities(
|
||||
let mut sql = format!(
|
||||
r#"
|
||||
SELECT
|
||||
id, person_id, face_identity_id, speaker_id, video_uuid,
|
||||
id, person_id, face_identity_id, speaker_id, file_uuid,
|
||||
name, original_name, character_name, gender, age,
|
||||
appearance_count, total_appearance_duration,
|
||||
first_appearance_time, last_appearance_time, is_confirmed
|
||||
@@ -435,8 +434,8 @@ async fn search_identities(
|
||||
conditions.push(format!("appearance_count >= {}", min_count));
|
||||
}
|
||||
|
||||
if let Some(vid) = &req.video_uuid {
|
||||
conditions.push(format!("video_uuid = '{}'", vid.replace('\'', "''")));
|
||||
if let Some(vid) = &req.file_uuid {
|
||||
conditions.push(format!("file_uuid = '{}'", vid.replace('\'', "''")));
|
||||
}
|
||||
|
||||
if !conditions.is_empty() {
|
||||
@@ -482,7 +481,7 @@ async fn search_identities(
|
||||
person_id,
|
||||
face_id,
|
||||
speaker_id,
|
||||
video_uuid,
|
||||
file_uuid,
|
||||
name,
|
||||
original_name,
|
||||
character_name,
|
||||
@@ -498,7 +497,7 @@ async fn search_identities(
|
||||
"id": id,
|
||||
"person_id": person_id,
|
||||
"face_identity_id": face_id,
|
||||
"video_uuid": video_uuid,
|
||||
"file_uuid": file_uuid,
|
||||
"profile": {
|
||||
"name": name,
|
||||
"original_name": original_name,
|
||||
@@ -579,12 +578,12 @@ async fn get_identity_videos(
|
||||
// Each video has its own local person_id, speaker_id, and character_name
|
||||
let videos_query = r#"
|
||||
SELECT
|
||||
pi.video_uuid, v.file_name, v.file_path,
|
||||
pi.file_uuid, v.file_name, v.file_path,
|
||||
pi.person_id, pi.speaker_id, pi.character_name,
|
||||
pi.appearance_count,
|
||||
pi.total_appearance_duration, pi.first_appearance_time, pi.last_appearance_time
|
||||
FROM '"{}' pi
|
||||
LEFT JOIN videos v ON pi.video_uuid = v.uuid
|
||||
LEFT JOIN videos v ON pi.file_uuid = v.uuid
|
||||
WHERE pi.face_identity_id = $1
|
||||
ORDER BY pi.last_appearance_time DESC
|
||||
"#;
|
||||
@@ -599,7 +598,7 @@ async fn get_identity_videos(
|
||||
.map(|row| {
|
||||
use sqlx::Row;
|
||||
serde_json::json!({
|
||||
"video_uuid": row.get::<String, _>("video_uuid"),
|
||||
"file_uuid": row.get::<String, _>("file_uuid"),
|
||||
"file_name": row.get::<Option<String>, _>("file_name"),
|
||||
"file_path": row.get::<Option<String>, _>("file_path"),
|
||||
"person_id": row.get::<String, _>("person_id"),
|
||||
@@ -659,12 +658,12 @@ async fn get_identity_faces(
|
||||
// Fetch distinct face detections for this identity
|
||||
let sql = r#"
|
||||
SELECT
|
||||
fd.id as detection_id, fd.video_uuid, fd.frame_number,
|
||||
fd.id as detection_id, fd.file_uuid, fd.frame_number,
|
||||
fd.timestamp_secs, fd.x, fd.y, fd.width, fd.height,
|
||||
fd.cluster_id, v.file_name
|
||||
FROM face_detections fd
|
||||
JOIN person_identities pi ON fd.video_uuid = pi.video_uuid
|
||||
LEFT JOIN videos v ON fd.video_uuid = v.uuid
|
||||
JOIN person_identities pi ON fd.file_uuid = pi.file_uuid
|
||||
LEFT JOIN videos v ON fd.file_uuid = v.uuid
|
||||
WHERE pi.face_identity_id = $1
|
||||
ORDER BY fd.timestamp_secs DESC
|
||||
LIMIT $2
|
||||
@@ -682,7 +681,7 @@ async fn get_identity_faces(
|
||||
use sqlx::Row;
|
||||
serde_json::json!({
|
||||
"detection_id": row.get::<i32, _>("detection_id"),
|
||||
"video_uuid": row.get::<String, _>("video_uuid"),
|
||||
"file_uuid": row.get::<String, _>("file_uuid"),
|
||||
"file_name": row.get::<Option<String>, _>("file_name"),
|
||||
"frame_number": row.get::<i64, _>("frame_number"),
|
||||
"timestamp": row.get::<f64, _>("timestamp_secs"),
|
||||
@@ -715,7 +714,7 @@ async fn get_identity_faces(
|
||||
/// List all faces in a video (both registered and unregistered)
|
||||
async fn get_video_faces(
|
||||
State(_state): State<crate::api::server::AppState>,
|
||||
Path(video_uuid): Path<String>,
|
||||
Path(file_uuid): Path<String>,
|
||||
) -> Result<Json<serde_json::Value>, (StatusCode, String)> {
|
||||
let db = match PostgresDb::init().await {
|
||||
Ok(db) => db,
|
||||
@@ -735,13 +734,13 @@ async fn get_video_faces(
|
||||
pi.person_id, pi.face_identity_id, pi.name, pi.is_confirmed
|
||||
FROM face_clusters fc
|
||||
LEFT JOIN person_identities pi ON fc.cluster_id = pi.person_id
|
||||
AND pi.video_uuid = fc.video_uuid
|
||||
WHERE fc.video_uuid = $1
|
||||
AND pi.file_uuid = fc.file_uuid
|
||||
WHERE fc.file_uuid = $1
|
||||
ORDER BY fc.size DESC
|
||||
"#;
|
||||
|
||||
let clusters: Vec<serde_json::Value> = match sqlx::query(clusters_query)
|
||||
.bind(&video_uuid)
|
||||
.bind(&file_uuid)
|
||||
.fetch_all(db.pool())
|
||||
.await
|
||||
{
|
||||
@@ -785,7 +784,7 @@ async fn get_video_faces(
|
||||
|
||||
Ok(Json(serde_json::json!({
|
||||
"success": true,
|
||||
"video_uuid": video_uuid,
|
||||
"file_uuid": file_uuid,
|
||||
"total_faces": total_faces,
|
||||
"registered_count": registered_count,
|
||||
"unregistered_count": total_faces - registered_count,
|
||||
@@ -799,7 +798,7 @@ async fn register_identity(
|
||||
Path(person_id): Path<String>,
|
||||
Query(query): Query<VideoUuidQuery>,
|
||||
) -> Result<Json<serde_json::Value>, (StatusCode, String)> {
|
||||
let video_uuid = query.video_uuid;
|
||||
let file_uuid = query.file_uuid;
|
||||
let db = match PostgresDb::init().await {
|
||||
Ok(db) => db,
|
||||
Err(e) => {
|
||||
@@ -813,11 +812,11 @@ async fn register_identity(
|
||||
// 1. Fetch person info
|
||||
let person_query = r#"
|
||||
SELECT id, name, face_identity_id FROM '"{}'
|
||||
WHERE person_id = $1 AND video_uuid = $2
|
||||
WHERE person_id = $1 AND file_uuid = $2
|
||||
"#;
|
||||
let person: Option<(i32, Option<String>, Option<i32>)> = match sqlx::query_as(person_query)
|
||||
.bind(&person_id)
|
||||
.bind(&video_uuid)
|
||||
.bind(&file_uuid)
|
||||
.fetch_optional(db.pool())
|
||||
.await
|
||||
{
|
||||
@@ -835,7 +834,7 @@ async fn register_identity(
|
||||
None => {
|
||||
return Err((
|
||||
StatusCode::NOT_FOUND,
|
||||
format!("Person '{}' not found in video '{}'", person_id, video_uuid),
|
||||
format!("Person '{}' not found in video '{}'", person_id, file_uuid),
|
||||
))
|
||||
}
|
||||
};
|
||||
@@ -851,11 +850,11 @@ async fn register_identity(
|
||||
|
||||
// 2. Get cluster centroid or detections embedding
|
||||
let cluster_query = r#"
|
||||
SELECT centroid FROM face_clusters WHERE cluster_id = $1 AND video_uuid = $2
|
||||
SELECT centroid FROM face_clusters WHERE cluster_id = $1 AND file_uuid = $2
|
||||
"#;
|
||||
let centroid: Option<Vec<f32>> = sqlx::query_scalar(cluster_query)
|
||||
.bind(&person_id)
|
||||
.bind(&video_uuid)
|
||||
.bind(&file_uuid)
|
||||
.fetch_optional(db.pool())
|
||||
.await
|
||||
.ok()
|
||||
@@ -923,7 +922,7 @@ async fn get_person_details(
|
||||
Path(person_id): Path<String>,
|
||||
Query(query): Query<PersonDetailQuery>,
|
||||
) -> Result<Json<serde_json::Value>, (StatusCode, String)> {
|
||||
let video_uuid = query.video_uuid;
|
||||
let file_uuid = query.file_uuid;
|
||||
let db = match PostgresDb::init().await {
|
||||
Ok(db) => db,
|
||||
Err(e) => {
|
||||
@@ -941,7 +940,7 @@ async fn get_person_details(
|
||||
first_appearance_time, last_appearance_time,
|
||||
is_confirmed, created_at, updated_at
|
||||
FROM '"{}'
|
||||
WHERE person_id = $1 AND video_uuid = $2
|
||||
WHERE person_id = $1 AND file_uuid = $2
|
||||
"#;
|
||||
|
||||
let person: Option<(
|
||||
@@ -959,7 +958,7 @@ async fn get_person_details(
|
||||
chrono::DateTime<chrono::Utc>,
|
||||
)> = match sqlx::query_as(query)
|
||||
.bind(&person_id)
|
||||
.bind(&video_uuid)
|
||||
.bind(&file_uuid)
|
||||
.fetch_optional(db.pool())
|
||||
.await
|
||||
{
|
||||
@@ -1001,7 +1000,7 @@ async fn get_person_details(
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct UpdatePersonQuery {
|
||||
pub video_uuid: String,
|
||||
pub file_uuid: String,
|
||||
}
|
||||
|
||||
async fn update_person_identity(
|
||||
@@ -1010,7 +1009,7 @@ async fn update_person_identity(
|
||||
Query(query): Query<UpdatePersonQuery>,
|
||||
Json(request): Json<UpdatePersonIdentityRequest>,
|
||||
) -> Result<Json<serde_json::Value>, (StatusCode, String)> {
|
||||
let video_uuid = query.video_uuid;
|
||||
let file_uuid = query.file_uuid;
|
||||
let db = match PostgresDb::init().await {
|
||||
Ok(db) => db,
|
||||
Err(e) => {
|
||||
@@ -1083,10 +1082,10 @@ async fn get_person_timeline(
|
||||
}
|
||||
};
|
||||
|
||||
let name_query = "SELECT name FROM person_identities WHERE person_id = $1 AND video_uuid = $2";
|
||||
let name_query = "SELECT name FROM person_identities WHERE person_id = $1 AND file_uuid = $2";
|
||||
let name: Option<String> = match sqlx::query_scalar(name_query)
|
||||
.bind(&person_id)
|
||||
.bind(&query.video_uuid)
|
||||
.bind(&query.file_uuid)
|
||||
.fetch_optional(db.pool())
|
||||
.await
|
||||
{
|
||||
@@ -1102,13 +1101,13 @@ async fn get_person_timeline(
|
||||
let timeline_query = r#"
|
||||
SELECT start_time, end_time, duration, confidence
|
||||
FROM person_appearances
|
||||
WHERE person_id = $1 AND video_uuid = $2
|
||||
WHERE person_id = $1 AND file_uuid = $2
|
||||
ORDER BY start_time ASC
|
||||
"#;
|
||||
|
||||
let timeline: Vec<(f64, f64, f64, f64)> = match sqlx::query_as(timeline_query)
|
||||
.bind(&person_id)
|
||||
.bind(&query.video_uuid)
|
||||
.bind(&query.file_uuid)
|
||||
.fetch_all(db.pool())
|
||||
.await
|
||||
{
|
||||
@@ -1139,13 +1138,13 @@ async fn get_person_timeline(
|
||||
MAX(end_time) as last_appearance,
|
||||
AVG(confidence) as average_confidence
|
||||
FROM person_appearances
|
||||
WHERE person_id = $1 AND video_uuid = $2
|
||||
WHERE person_id = $1 AND file_uuid = $2
|
||||
"#;
|
||||
|
||||
let stats: (i64, Option<f64>, Option<f64>, Option<f64>, Option<f64>) =
|
||||
match sqlx::query_as(stats_query)
|
||||
.bind(&person_id)
|
||||
.bind(&query.video_uuid)
|
||||
.bind(&query.file_uuid)
|
||||
.fetch_one(db.pool())
|
||||
.await
|
||||
{
|
||||
@@ -1179,7 +1178,7 @@ async fn get_person_appearances(
|
||||
Path(person_id): Path<String>,
|
||||
Query(query): Query<PersonDetailQuery>,
|
||||
) -> Result<Json<serde_json::Value>, (StatusCode, String)> {
|
||||
let video_uuid = query.video_uuid;
|
||||
let file_uuid = query.file_uuid;
|
||||
let db = match PostgresDb::init().await {
|
||||
Ok(db) => db,
|
||||
Err(e) => {
|
||||
@@ -1192,18 +1191,18 @@ async fn get_person_appearances(
|
||||
|
||||
let query = r#"
|
||||
SELECT
|
||||
person_id, video_uuid, start_time, end_time, duration,
|
||||
person_id, file_uuid, start_time, end_time, duration,
|
||||
face_detection_id, asrx_segment_start, asrx_segment_end,
|
||||
confidence, created_at
|
||||
FROM person_appearances
|
||||
WHERE person_id = $1 AND video_uuid = $2
|
||||
WHERE person_id = $1 AND file_uuid = $2
|
||||
ORDER BY start_time DESC
|
||||
LIMIT 100
|
||||
"#;
|
||||
|
||||
let appearances: Vec<serde_json::Value> = match sqlx::query(query)
|
||||
.bind(&person_id)
|
||||
.bind(&video_uuid)
|
||||
.bind(&file_uuid)
|
||||
.fetch_all(db.pool())
|
||||
.await
|
||||
{
|
||||
@@ -1213,7 +1212,7 @@ async fn get_person_appearances(
|
||||
use sqlx::Row;
|
||||
serde_json::json!({
|
||||
"person_id": row.get::<String, _>("person_id"),
|
||||
"video_uuid": row.get::<String, _>("video_uuid"),
|
||||
"file_uuid": row.get::<String, _>("file_uuid"),
|
||||
"start_time": row.get::<f64, _>("start_time"),
|
||||
"end_time": row.get::<f64, _>("end_time"),
|
||||
"duration": row.get::<f64, _>("duration"),
|
||||
@@ -1240,7 +1239,7 @@ async fn get_person_appearances(
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct ChunkPersonsQuery {
|
||||
pub video_uuid: String,
|
||||
pub file_uuid: String,
|
||||
}
|
||||
|
||||
async fn get_chunk_persons(
|
||||
@@ -1248,7 +1247,7 @@ async fn get_chunk_persons(
|
||||
Path(chunk_id): Path<String>,
|
||||
Query(query): Query<ChunkPersonsQuery>,
|
||||
) -> Result<Json<ChunkPersonsResponse>, (StatusCode, String)> {
|
||||
let video_uuid = &query.video_uuid;
|
||||
let file_uuid = &query.file_uuid;
|
||||
let db = match PostgresDb::init().await {
|
||||
Ok(db) => db,
|
||||
Err(e) => {
|
||||
@@ -1290,7 +1289,7 @@ async fn get_chunk_persons(
|
||||
}
|
||||
};
|
||||
|
||||
let (video_uuid, start_time, end_time, _metadata) = chunk;
|
||||
let (file_uuid, start_time, end_time, _metadata) = chunk;
|
||||
|
||||
let persons_query = r#"
|
||||
SELECT
|
||||
@@ -1300,7 +1299,7 @@ async fn get_chunk_persons(
|
||||
LEAST(pa.end_time, $3) - GREATEST(pa.start_time, $2) as overlap_duration
|
||||
FROM person_appearances pa
|
||||
JOIN person_identities pi ON pa.person_id = pi.person_id
|
||||
WHERE pa.video_uuid = $1
|
||||
WHERE pa.file_uuid = $1
|
||||
AND pa.start_time < $3
|
||||
AND pa.end_time > $2
|
||||
ORDER BY overlap_duration DESC
|
||||
@@ -1308,7 +1307,7 @@ async fn get_chunk_persons(
|
||||
|
||||
let persons: Vec<ChunkPersonInfo> =
|
||||
match sqlx::query_as::<_, (String, Option<String>, f64, f64)>(persons_query)
|
||||
.bind(&video_uuid)
|
||||
.bind(&file_uuid)
|
||||
.bind(start_time)
|
||||
.bind(end_time)
|
||||
.fetch_all(db.pool())
|
||||
@@ -1349,15 +1348,15 @@ async fn get_person_thumbnail(
|
||||
// 1. Locate the face_clustered.json file
|
||||
let json_path = format!(
|
||||
"output/{}/{}_face_clustered.json",
|
||||
query.video_uuid, query.video_uuid
|
||||
query.file_uuid, query.file_uuid
|
||||
);
|
||||
let json_path2 = format!(
|
||||
"output/{}/{}.face_clustered.json",
|
||||
query.video_uuid, query.video_uuid
|
||||
query.file_uuid, query.file_uuid
|
||||
);
|
||||
|
||||
// Fallback path if the naming convention is slightly different
|
||||
let fallback_path = format!("output/{}/face_clustered.json", query.video_uuid);
|
||||
let fallback_path = format!("output/{}/face_clustered.json", query.file_uuid);
|
||||
|
||||
let path = if std::path::Path::new(&json_path).exists() {
|
||||
json_path
|
||||
@@ -1370,7 +1369,7 @@ async fn get_person_thumbnail(
|
||||
StatusCode::NOT_FOUND,
|
||||
format!(
|
||||
"Face data not found for video: {}. Tried: {}, {}, {}",
|
||||
query.video_uuid, json_path, json_path2, fallback_path
|
||||
query.file_uuid, json_path, json_path2, fallback_path
|
||||
),
|
||||
));
|
||||
};
|
||||
@@ -1418,7 +1417,7 @@ async fn get_person_thumbnail(
|
||||
let (timestamp, face) = detections[index];
|
||||
|
||||
// 3. Locate the video file
|
||||
let video_path = format!("output/{}/{}.mp4", query.video_uuid, query.video_uuid);
|
||||
let video_path = format!("output/{}/{}.mp4", query.file_uuid, query.file_uuid);
|
||||
if !std::path::Path::new(&video_path).exists() {
|
||||
return Err((
|
||||
StatusCode::NOT_FOUND,
|
||||
@@ -1489,12 +1488,12 @@ async fn create_person_identity(
|
||||
|
||||
let query = r#"
|
||||
INSERT INTO person_identities (
|
||||
person_id, video_uuid, face_identity_id, speaker_id,
|
||||
person_id, file_uuid, face_identity_id, speaker_id,
|
||||
name, metadata, confidence
|
||||
) VALUES ($1, $2, $3, $4, $5, $6, $7)
|
||||
RETURNING
|
||||
id, person_id, face_identity_id, speaker_id,
|
||||
video_uuid, confidence, name, metadata,
|
||||
file_uuid, confidence, name, metadata,
|
||||
first_appearance_time, last_appearance_time,
|
||||
total_appearance_duration, appearance_count,
|
||||
created_at, updated_at, is_confirmed
|
||||
@@ -1502,11 +1501,11 @@ async fn create_person_identity(
|
||||
|
||||
let person: PersonIdentity = sqlx::query_as(query)
|
||||
.bind(&person_id)
|
||||
.bind(&request.video_uuid)
|
||||
.bind(&request.file_uuid)
|
||||
.bind(&request.face_identity_id)
|
||||
.bind(&request.speaker_id)
|
||||
.bind(&request.name)
|
||||
.bind(&request.metadata.unwrap_or(serde_json::json!({})))
|
||||
.bind(serde_json::to_string(&request.metadata.unwrap_or(serde_json::json!({}))).unwrap())
|
||||
.bind(0.0)
|
||||
.fetch_one(db.pool())
|
||||
.await?;
|
||||
@@ -1516,13 +1515,13 @@ async fn create_person_identity(
|
||||
|
||||
async fn auto_match_face_speaker(
|
||||
db: &PostgresDb,
|
||||
video_uuid: &str,
|
||||
file_uuid: &str,
|
||||
threshold: f64,
|
||||
) -> Result<Vec<PersonMatch>, anyhow::Error> {
|
||||
let query = "SELECT * FROM auto_match_face_speaker($1, $2)";
|
||||
|
||||
let matches: Vec<PersonMatch> = sqlx::query_as(query)
|
||||
.bind(video_uuid)
|
||||
.bind(file_uuid)
|
||||
.bind(threshold)
|
||||
.fetch_all(db.pool())
|
||||
.await?;
|
||||
@@ -1548,9 +1547,9 @@ async fn auto_identify_persons(
|
||||
// 1. Load face_clustered.json
|
||||
let clustered_path = format!(
|
||||
"output/{}/{}.face_clustered.json",
|
||||
request.video_uuid, request.video_uuid
|
||||
request.file_uuid, request.file_uuid
|
||||
);
|
||||
let fallback_path = format!("output/{}/face_clustered.json", request.video_uuid);
|
||||
let fallback_path = format!("output/{}/face_clustered.json", request.file_uuid);
|
||||
let path = if std::path::Path::new(&clustered_path).exists() {
|
||||
clustered_path
|
||||
} else if std::path::Path::new(&fallback_path).exists() {
|
||||
@@ -1560,7 +1559,7 @@ async fn auto_identify_persons(
|
||||
StatusCode::NOT_FOUND,
|
||||
format!(
|
||||
"face_clustered.json not found for video: {}",
|
||||
request.video_uuid
|
||||
request.file_uuid
|
||||
),
|
||||
));
|
||||
};
|
||||
@@ -1615,7 +1614,7 @@ async fn auto_identify_persons(
|
||||
// 3. Load ASRX from chunks
|
||||
let asrx_query = "SELECT chunk_id, content::text FROM chunks WHERE uuid = $1 AND chunk_type = 'trace' AND chunk_id LIKE 'trace_asrx_%'";
|
||||
let asrx_chunks: Vec<(String, String)> = match sqlx::query_as(asrx_query)
|
||||
.bind(&request.video_uuid)
|
||||
.bind(&request.file_uuid)
|
||||
.fetch_all(db.pool())
|
||||
.await
|
||||
{
|
||||
@@ -1631,7 +1630,7 @@ async fn auto_identify_persons(
|
||||
// Also check sentence chunks for speaker_id
|
||||
let sentence_query = "SELECT content::text FROM chunks WHERE uuid = $1 AND chunk_type = 'sentence' AND content ? 'speaker_id'";
|
||||
let sentence_chunks: Vec<String> = match sqlx::query_scalar(sentence_query)
|
||||
.bind(&request.video_uuid)
|
||||
.bind(&request.file_uuid)
|
||||
.fetch_all(db.pool())
|
||||
.await
|
||||
{
|
||||
@@ -1801,7 +1800,7 @@ async fn list_persons(
|
||||
}
|
||||
};
|
||||
|
||||
let video_uuid = query.video_uuid.replace("'", "''");
|
||||
let file_uuid = query.file_uuid.replace("'", "''");
|
||||
let page = query.page.unwrap_or(1);
|
||||
let page_size = query.page_size.unwrap_or(20);
|
||||
let offset = ((page - 1) as i64) * (page_size as i64);
|
||||
@@ -1811,25 +1810,25 @@ async fn list_persons(
|
||||
let (sql, count_sql) = if has_speaker {
|
||||
if min_appearances > 0 {
|
||||
(
|
||||
format!("SELECT person_id, name, speaker_id, appearance_count, total_appearance_duration, first_appearance_time, last_appearance_time, is_confirmed, metadata::text FROM person_identities WHERE video_uuid = '{}' AND speaker_id IS NOT NULL AND appearance_count >= $1 ORDER BY appearance_count DESC LIMIT $2 OFFSET $3", video_uuid),
|
||||
format!("SELECT COUNT(*) FROM person_identities WHERE video_uuid = '{}' AND speaker_id IS NOT NULL AND appearance_count >= $1", video_uuid),
|
||||
format!("SELECT person_id, name, speaker_id, appearance_count, total_appearance_duration, first_appearance_time, last_appearance_time, is_confirmed, metadata::text FROM person_identities WHERE file_uuid = '{}' AND speaker_id IS NOT NULL AND appearance_count >= $1 ORDER BY appearance_count DESC LIMIT $2 OFFSET $3", file_uuid),
|
||||
format!("SELECT COUNT(*) FROM person_identities WHERE file_uuid = '{}' AND speaker_id IS NOT NULL AND appearance_count >= $1", file_uuid),
|
||||
)
|
||||
} else {
|
||||
(
|
||||
format!("SELECT person_id, name, speaker_id, appearance_count, total_appearance_duration, first_appearance_time, last_appearance_time, is_confirmed, metadata::text FROM person_identities WHERE video_uuid = '{}' AND speaker_id IS NOT NULL ORDER BY appearance_count DESC LIMIT $1 OFFSET $2", video_uuid),
|
||||
format!("SELECT COUNT(*) FROM person_identities WHERE video_uuid = '{}' AND speaker_id IS NOT NULL", video_uuid),
|
||||
format!("SELECT person_id, name, speaker_id, appearance_count, total_appearance_duration, first_appearance_time, last_appearance_time, is_confirmed, metadata::text FROM person_identities WHERE file_uuid = '{}' AND speaker_id IS NOT NULL ORDER BY appearance_count DESC LIMIT $1 OFFSET $2", file_uuid),
|
||||
format!("SELECT COUNT(*) FROM person_identities WHERE file_uuid = '{}' AND speaker_id IS NOT NULL", file_uuid),
|
||||
)
|
||||
}
|
||||
} else {
|
||||
if min_appearances > 0 {
|
||||
(
|
||||
format!("SELECT person_id, name, speaker_id, appearance_count, total_appearance_duration, first_appearance_time, last_appearance_time, is_confirmed, metadata::text FROM person_identities WHERE video_uuid = '{}' AND appearance_count >= $1 ORDER BY appearance_count DESC LIMIT $2 OFFSET $3", video_uuid),
|
||||
format!("SELECT COUNT(*) FROM person_identities WHERE video_uuid = '{}' AND appearance_count >= $1", video_uuid),
|
||||
format!("SELECT person_id, name, speaker_id, appearance_count, total_appearance_duration, first_appearance_time, last_appearance_time, is_confirmed, metadata::text FROM person_identities WHERE file_uuid = '{}' AND appearance_count >= $1 ORDER BY appearance_count DESC LIMIT $2 OFFSET $3", file_uuid),
|
||||
format!("SELECT COUNT(*) FROM person_identities WHERE file_uuid = '{}' AND appearance_count >= $1", file_uuid),
|
||||
)
|
||||
} else {
|
||||
(
|
||||
format!("SELECT person_id, name, speaker_id, appearance_count, total_appearance_duration, first_appearance_time, last_appearance_time, is_confirmed, metadata::text FROM person_identities WHERE video_uuid = '{}' ORDER BY appearance_count DESC LIMIT $1 OFFSET $2", video_uuid),
|
||||
format!("SELECT COUNT(*) FROM person_identities WHERE video_uuid = '{}'", video_uuid),
|
||||
format!("SELECT person_id, name, speaker_id, appearance_count, total_appearance_duration, first_appearance_time, last_appearance_time, is_confirmed, metadata::text FROM person_identities WHERE file_uuid = '{}' ORDER BY appearance_count DESC LIMIT $1 OFFSET $2", file_uuid),
|
||||
format!("SELECT COUNT(*) FROM person_identities WHERE file_uuid = '{}'", file_uuid),
|
||||
)
|
||||
}
|
||||
};
|
||||
@@ -1925,7 +1924,7 @@ async fn merge_persons(
|
||||
State(_state): State<crate::api::server::AppState>,
|
||||
Json(request): Json<MergePersonsRequest>,
|
||||
) -> Result<Json<MergePersonsResponse>, (StatusCode, String)> {
|
||||
let video_uuid = &request.video_uuid;
|
||||
let file_uuid = &request.file_uuid;
|
||||
let db = match PostgresDb::init().await {
|
||||
Ok(db) => db,
|
||||
Err(e) => {
|
||||
@@ -2150,7 +2149,7 @@ async fn undo_merge(
|
||||
State(_state): State<crate::api::server::AppState>,
|
||||
Json(request): Json<UndoMergeRequest>,
|
||||
) -> Result<Json<serde_json::Value>, (StatusCode, String)> {
|
||||
// video_uuid is validated through merge_history lookup
|
||||
// file_uuid is validated through merge_history lookup
|
||||
let db = match PostgresDb::init().await {
|
||||
Ok(db) => db,
|
||||
Err(e) => {
|
||||
@@ -2285,7 +2284,10 @@ async fn undo_merge(
|
||||
.bind(source_duration)
|
||||
.bind(source_first)
|
||||
.bind(source_last)
|
||||
.bind(&serde_json::json!({"restored_from_merge": _merge_id}))
|
||||
.bind(
|
||||
serde_json::to_string(&serde_json::json!({"restored_from_merge": _merge_id}))
|
||||
.unwrap(),
|
||||
)
|
||||
.execute(&mut *tx)
|
||||
.await
|
||||
{
|
||||
@@ -2333,14 +2335,14 @@ async fn undo_merge(
|
||||
/// Get merge history
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct MergeHistoryQuery {
|
||||
pub video_uuid: String,
|
||||
pub file_uuid: String,
|
||||
}
|
||||
|
||||
async fn get_merge_history(
|
||||
State(_state): State<crate::api::server::AppState>,
|
||||
Query(query): Query<MergeHistoryQuery>,
|
||||
) -> Result<Json<MergeHistoryResponse>, (StatusCode, String)> {
|
||||
let _video_uuid = &query.video_uuid;
|
||||
let _file_uuid = &query.file_uuid;
|
||||
let db = match PostgresDb::init().await {
|
||||
Ok(db) => db,
|
||||
Err(e) => {
|
||||
@@ -2412,16 +2414,16 @@ async fn get_similar_persons(
|
||||
}
|
||||
};
|
||||
|
||||
let video_uuid = query.video_uuid;
|
||||
let file_uuid = query.file_uuid;
|
||||
let threshold = query.threshold.unwrap_or(0.5);
|
||||
let limit = query.limit.unwrap_or(10);
|
||||
|
||||
// Find the speaker_id of the requested person
|
||||
let get_speaker_query =
|
||||
"SELECT speaker_id FROM person_identities WHERE person_id = $1 AND video_uuid = $2";
|
||||
"SELECT speaker_id FROM person_identities WHERE person_id = $1 AND file_uuid = $2";
|
||||
let current_speaker_id: Option<String> = match sqlx::query_scalar(get_speaker_query)
|
||||
.bind(&person_id)
|
||||
.bind(&video_uuid)
|
||||
.bind(&file_uuid)
|
||||
.fetch_optional(db.pool())
|
||||
.await
|
||||
{
|
||||
@@ -2437,7 +2439,7 @@ async fn get_similar_persons(
|
||||
let results = match current_speaker_id {
|
||||
Some(sid) => {
|
||||
// Find others with same speaker_id
|
||||
let similar_query = "SELECT person_id, name, speaker_id, appearance_count, first_appearance_time, last_appearance_time FROM person_identities WHERE speaker_id = $1 AND person_id != $2 AND video_uuid = $3 ORDER BY appearance_count DESC LIMIT $4";
|
||||
let similar_query = "SELECT person_id, name, speaker_id, appearance_count, first_appearance_time, last_appearance_time FROM person_identities WHERE speaker_id = $1 AND person_id != $2 AND file_uuid = $3 ORDER BY appearance_count DESC LIMIT $4";
|
||||
let rows: Vec<(
|
||||
String,
|
||||
Option<String>,
|
||||
@@ -2448,7 +2450,7 @@ async fn get_similar_persons(
|
||||
)> = match sqlx::query_as(similar_query)
|
||||
.bind(&sid)
|
||||
.bind(&person_id)
|
||||
.bind(&video_uuid)
|
||||
.bind(&file_uuid)
|
||||
.bind(limit)
|
||||
.fetch_all(db.pool())
|
||||
.await
|
||||
@@ -2509,13 +2511,13 @@ async fn get_person_suggestions(
|
||||
// 1. Naming suggestions: Persons with NULL name but high appearance count.
|
||||
// 2. Merge suggestions: Persons sharing the same speaker_id.
|
||||
|
||||
let video_uuid = req.video_uuid;
|
||||
let file_uuid = req.file_uuid;
|
||||
|
||||
// Naming suggestions
|
||||
let naming_query = "SELECT person_id, name, speaker_id, appearance_count FROM person_identities WHERE video_uuid = $1 AND (name IS NULL OR name = person_id) AND appearance_count > 50 ORDER BY appearance_count DESC LIMIT 10";
|
||||
let naming_query = "SELECT person_id, name, speaker_id, appearance_count FROM person_identities WHERE file_uuid = $1 AND (name IS NULL OR name = person_id) AND appearance_count > 50 ORDER BY appearance_count DESC LIMIT 10";
|
||||
let naming_rows: Vec<(String, Option<String>, Option<String>, i32)> =
|
||||
match sqlx::query_as(naming_query)
|
||||
.bind(&video_uuid)
|
||||
.bind(&file_uuid)
|
||||
.fetch_all(db.pool())
|
||||
.await
|
||||
{
|
||||
@@ -2538,9 +2540,9 @@ async fn get_person_suggestions(
|
||||
}
|
||||
|
||||
// Merge suggestions (Speaker overlap)
|
||||
let merge_query = "SELECT person_id, speaker_id, appearance_count FROM person_identities WHERE video_uuid = $1 AND speaker_id IS NOT NULL ORDER BY speaker_id, appearance_count DESC";
|
||||
let merge_query = "SELECT person_id, speaker_id, appearance_count FROM person_identities WHERE file_uuid = $1 AND speaker_id IS NOT NULL ORDER BY speaker_id, appearance_count DESC";
|
||||
let merge_rows: Vec<(String, String, i32)> = match sqlx::query_as(merge_query)
|
||||
.bind(&video_uuid)
|
||||
.bind(&file_uuid)
|
||||
.fetch_all(db.pool())
|
||||
.await
|
||||
{
|
||||
@@ -2644,14 +2646,14 @@ async fn confirm_person_suggestion(
|
||||
/// Request to unbind speaker from person
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct UnbindSpeakerRequest {
|
||||
pub video_uuid: String,
|
||||
pub file_uuid: String,
|
||||
pub reason: Option<String>,
|
||||
}
|
||||
|
||||
/// Request to reassign speaker to person
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct ReassignSpeakerRequest {
|
||||
pub video_uuid: String,
|
||||
pub file_uuid: String,
|
||||
pub speaker_id: String,
|
||||
pub reason: Option<String>,
|
||||
}
|
||||
@@ -2659,7 +2661,7 @@ pub struct ReassignSpeakerRequest {
|
||||
/// Request to remove a specific appearance
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct RemoveAppearanceRequest {
|
||||
pub video_uuid: String,
|
||||
pub file_uuid: String,
|
||||
pub appearance_id: i32,
|
||||
pub reason: Option<String>,
|
||||
}
|
||||
@@ -2667,7 +2669,7 @@ pub struct RemoveAppearanceRequest {
|
||||
/// Request to reassign appearance to another person
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct ReassignAppearanceRequest {
|
||||
pub video_uuid: String,
|
||||
pub file_uuid: String,
|
||||
pub appearance_id: i32,
|
||||
pub target_person_id: String,
|
||||
pub reason: Option<String>,
|
||||
@@ -2676,7 +2678,7 @@ pub struct ReassignAppearanceRequest {
|
||||
/// Request to split a person into two
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct SplitPersonRequest {
|
||||
pub video_uuid: String,
|
||||
pub file_uuid: String,
|
||||
pub new_person_id: String,
|
||||
pub appearance_ids_to_move: Vec<i32>,
|
||||
pub new_person_name: Option<String>,
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user