Initial commit: Momentry Core v0.1

- Rust-based digital asset management system
- Video analysis: ASR, OCR, YOLO, Face, Pose
- RAG capabilities with Qdrant vector database
- Multi-database support: PostgreSQL, Redis, MongoDB
- Monitoring system with launchd plists
- n8n workflow automation integration
This commit is contained in:
accusys
2026-03-16 15:07:33 +08:00
commit de14bd6afa
101 changed files with 19858 additions and 0 deletions

11
.gitignore vendored Normal file
View File

@@ -0,0 +1,11 @@
.env
.env.local
target/
venv/
thumbnails/
*.asr.json
*.probe.json
test_asr.json
.DS_Store
*.log
.ruff_cache/

168
AGENTS.md Normal file
View File

@@ -0,0 +1,168 @@
# AGENTS.md - Momentry Core
Rust-based digital asset management system with video analysis and RAG capabilities.
## Build & Run Commands
```bash
# Build project
cargo build
cargo build --release
cargo build --bin momentry
# Run CLI
cargo run -- --help
cargo run -- register /path/to/video.mp4
cargo run -- server --host 0.0.0.0 --port 3000
```
## Testing
```bash
# Run all tests
cargo test
# Run single test by name
cargo test test_name
# Run with output
cargo test -- --nocapture
# Doc tests
cargo test --doc
```
## Linting & Formatting
```bash
# Format code (edition=2021, max_width=100, tab_spaces=4)
cargo fmt
cargo fmt -- --check
# Lint
cargo clippy
cargo clippy --all-features
# Check for errors
cargo check
cargo check --all-features
```
## Code Style
### General
- Use Rust 2021 edition
- Use tracing for logging (not println!)
- Keep lines under 100 characters
### Imports (order: std → external → local)
```rust
use std::path::Path;
use anyhow::{Context, Result};
use async_trait::async_trait;
use serde::{Deserialize, Serialize};
use crate::core::chunk::Chunk;
```
### Error Handling
- Use `anyhow::Result<T>` for application code
- Use `thiserror` for library code
- Use `.context()` for error context
- Use `anyhow::bail!()` for early returns
```rust
fn example() -> Result<SomeType> {
let output = Command::new("ffprobe")
.args([...])
.output()
.context("Failed to run ffprobe")?;
if !output.status.success() {
anyhow::bail!("Command failed");
}
Ok(result)
}
```
### Naming
- Types/Enums: PascalCase (`VideoRecord`, `ChunkType`)
- Functions/Variables: snake_case (`get_video_by_uuid`)
- Traits: PascalCase with -er suffix (`Database`, `ChunkStore`)
- Files: snake_case (`postgres_db.rs`)
### Types
- Use `serde::{Deserialize, Serialize}` for serializable types
- Use `#[serde(rename_all = "snake_case")]` for enum variants
- Use explicit numeric types (i64, u32, f64)
```rust
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct VideoRecord {
pub id: i64,
pub uuid: String,
pub duration: f64,
pub width: u32,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "snake_case")]
pub enum ChunkType {
TimeBased,
Sentence,
Cut,
}
```
### Async Programming
- Use `tokio` runtime with full features
- Use `#[async_trait]` for async trait methods
```rust
#[async_trait]
pub trait Database: Send + Sync {
async fn init() -> Result<Self>
where Self: Sized;
}
```
## Code Structure
```
src/
├── main.rs # CLI entry point
├── lib.rs # Library exports
├── core/
│ ├── chunk/ # Chunking logic
│ ├── db/ # Database (PostgreSQL, MongoDB, Redis, Qdrant)
│ ├── embedding/ # Vector embeddings
│ ├── overlay/ # Video overlay
│ ├── probe/ # ffprobe integration
│ ├── processor/ # ASR, OCR, YOLO, Face, Pose
│ ├── storage/ # File management
│ └── thumbnail/ # Thumbnail extraction
├── api/ # HTTP API (axum)
├── player/ # Video player
└── watcher/ # File system watcher
```
## Key Dependencies
- **Error handling**: `anyhow`, `thiserror`
- **Async**: `tokio` (full features), `async-trait`
- **CLI**: `clap` (derive)
- **Serialization**: `serde`, `serde_json`
- **Database**: `sqlx`, `mongodb`, `redis`, `qdrant-client`
- **HTTP**: `axum`, `tower`
- **Logging**: `tracing`, `tracing-subscriber`
## Environment Variables
- `DATABASE_URL` - PostgreSQL (default: `postgres://accusys@localhost:5432/momentry`)
## Notes
- No unit tests exist - add tests when implementing features
- Video processing uses external tools (ffprobe, Python scripts)
- Multi-database architecture (PostgreSQL, MongoDB, Redis, Qdrant)
- Monitor directory is a separate system (not Rust)

4260
Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

59
Cargo.toml Normal file
View File

@@ -0,0 +1,59 @@
[package]
name = "momentry_core"
version = "0.1.0"
edition = "2021"
authors = ["Momentry Team"]
description = "Digital asset management system with video analysis and RAG"
[dependencies]
# Core
anyhow = "1.0"
thiserror = "1.0"
tokio = { version = "1", features = ["full"] }
tracing = "0.1"
tracing-subscriber = "0.3"
# CLI
clap = { version = "4", features = ["derive"] }
# Async
async-trait = "0.1"
# Serialization
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
# UUID
sha2 = "0.10"
hex = "0.4"
# Database
redis = { version = "0.25", features = ["tokio-comp"] }
sqlx = { version = "0.8", features = ["runtime-tokio", "postgres", "sqlite", "json"] }
mongodb = { version = "2", features = ["tokio-sync"] }
qdrant-client = "1.7"
# HTTP Server
axum = "0.7"
tower = "0.4"
# File watching
notify = "6"
# Video/Audio
sdl2 = "0.38"
# Configuration
dotenv = "0.15"
[features]
default = []
dev = []
[lib]
name = "momentry_core"
path = "src/lib.rs"
[[bin]]
name = "momentry"
path = "src/main.rs"

View File

@@ -0,0 +1,443 @@
# Momentry 系統整合指南 v0.1
## 一、專案概述
Momentry 是一個數位資產管理系統包含服務遷移、監控系統、Storage 架構和用戶管理。
**設計目標**
- 自動化優先:人類只需處理通知和判斷
- 分層監控:七層監控架構
- 數據納管:檔案由資料庫統一管理
---
## 二、系統架構
### 硬體
- **本地 NVMe**:熱數據 (/Users/accusys/momentry/)
- **外部 RAID**:溫數據 (/Volumes/RAID System/)
- **Object Storage**:冷數據歸檔
### 網路
- **DDNS**momentry.ddns.net
- **VPN**Shadowsocks (port 8388)
- **反向代理**Caddy
### 軟體服務 (15 個)
| 服務 | Port | 用途 | 狀態 |
|------|------|------|------|
| PostgreSQL | 5432 | 主資料庫 | ✅ 運行 |
| Redis | 6379 | 快取/會話 | ✅ 運行 |
| n8n Main | 5678 | 工作流 | ✅ 運行 |
| n8n Worker | 5690 | 工作流 | ✅ 運行 |
| Caddy | 443/2019 | 反向代理 | ✅ 運行 |
| Gitea | 3000 | Git 服務 | ✅ 運行 |
| SFTPGo | 2222 | SFTP | ✅ 運行 |
| Ollama | 11434 | LLM | ✅ 運行 |
| Qdrant | 6333 | 向量資料庫 | ✅ 運行 |
| MariaDB | 3306 | WordPress | ✅ 運行 |
| PHP-FPM | - | Web 後端 | ✅ 運行 |
| RustDesk hbbs | 21116 | 遠程桌面 | ✅ 運行 |
| RustDesk hbbr | 21117 | 遠程桌面 | ✅ 運行 |
| MongoDB | 27017 | 文檔資料庫 | ✅ 運行 |
| Agent | - | 代理服務 | ✅ 運行 |
---
## 三、監控系統 (七層架構)
```
Layer 1: External 監控 - DDNS、網關、互聯網
Layer 2: Service 監控 - 15 個服務健康檢查
Layer 3: Workflow 監控 - n8n Workflow 狀態/閒置分析
Layer 4: Portal 監控 - WordPress 頁面/帳號
Layer 5: Database 監控 - PostgreSQL/Redis/Qdrant/MariaDB
Layer 6: 使用者監控 - 連線/本機/異常檢測
Layer 7: Storage 架構 - 冷溫熱/歸檔/檔案註冊
```
### 3.1 Layer 1: External 監控
**監控項目**
- DDNS 域名解析 (momentry.ddns.net)
- 網關連通性 (192.168.110.1)
- 互聯網連接 (8.8.8.8)
**腳本**`monitor/service/external_monitor.sh`
### 3.2 Layer 2: Service 監控
**監控項目**
- 進程狀態
- 端口響應
- HTTP 狀態碼
**腳本**`monitor/service/health_check.sh`
### 3.3 Layer 3: n8n Workflow 監控
**監控項目**
- Workflow 數量與啟用狀態
- 執行次數與結果
- 閒置識別
**閒置定義**
```
閒置 = 無排程 AND 無 API 觸發 AND 超過 30 天未執行
```
**改善建議**
- 建議停用:閒置 > 30 天
- 建議刪除:閒置 > 90 天
**腳本**`monitor/workflow/n8n_workflow_monitor.sh`
### 3.4 Layer 4: WordPress Portal 監控
**變動監控**
- 版本變更 (Core/Theme/Plugin)
- 文件變更 (wp-content)
- 配置變更 (wp-config.php)
- 內容變更 (文章/頁面)
- 代碼變更
- 帳號創建/刪除/修改
**使用監控**
- 帳號登入/失敗
- 內容操作 (發布/編輯/刪除)
- 權限變更
- 評論/媒體操作
**腳本**`monitor/portal/page_monitor.sh`
### 3.5 Layer 5: Database 監控
**PostgreSQL**
- 表數量/行數/大小
- 死元組
- 慢查詢
**Redis**
- 連線數
- 內存使用
- 命中率
- 客戶端列表
**Qdrant**
- Collection 列表
- Points 數
- 向量維度
- 磁盤使用
**MariaDB**
- 連線數
- WordPress 表
**腳本**
- `monitor/database/postgres_monitor.sh`
- `monitor/database/redis_monitor.sh`
- `monitor/database/qdrant_monitor.sh`
### 3.6 Layer 6: 使用者監控
**連線使用者**
- SSH 登入與命令
- Web 服務 (n8n/Gitea/WP)
- 資料庫連線
- SFTP 傳輸
**本機使用者**
- 系統登入
- sudo 使用
- 服務帳戶
- 異常檢測
**異常檢測**
- 暴力破解 (> 5次/分鐘)
- 權限提升
- 異常時間登入
**腳本**`monitor/users/session_tracker.sh`
### 3.7 Layer 7: Storage 架構
**目錄結構**
```
/Users/accusys/momentry/
├── var/ # 服務數據 (熱)
├── etc/ # 配置 (溫)
├── log/ # 日誌 (溫)
├── data/ # 用戶數據
│ ├── family/ # 家庭集群
│ ├── work/ # 工作集群
│ ├── wordpress/ # WP 隔離
│ └── shared/ # 共享
├── backup/ # 備份
│ ├── daily/
│ ├── weekly/
│ ├── monthly/
│ └── archive/
└── tmp/ # 臨時
```
**分層標準**
| 等級 | 條件 | 存放 |
|------|------|------|
| 熱 | 7天內訪問 > 10次 | NVMe |
| 溫 | 30天內訪問 > 1次 | RAID |
| 冷 | 90天未訪問 | Object |
**歸檔策略**
- 手動歸檔
- 自動歸檔 (90天)
- 深度歸檔 (1年)
**腳本**`monitor/storage/storage_manager.sh`
---
## 四、數據表設計
### 監控相關
```sql
monitor_services -- 服務健康狀態
monitor_workflows -- n8n Workflow
monitor_databases -- 資料庫指標
monitor_portal_pages -- WP 頁面
monitor_portal_users -- WP 用戶
monitor_sessions -- 使用者會話
monitor_logins -- 登入歷史
monitor_sudo_history -- sudo 記錄
monitor_resource_usage -- 資源使用
monitor_anomalies -- 異常檢測
monitor_external -- 外部監控
```
### Storage 相關
```sql
file_registry -- 檔案註冊
storage_usage_stats -- 存儲統計
storage_access_logs -- 訪問日誌
file_lifecycle -- 生命週期
```
---
## 八、實現文件結構
```
momentry_core_0.1/
├── MOMENTRY_INTEGRATION_GUIDE.md # 本文件
├── monitor/
│ ├── MONITORING.md # 監控詳細文檔
│ ├── config/
│ │ └── monitor_config.yaml # 配置文件
│ ├── control/
│ │ └── monitor_control.sh # 控制腳本
│ ├── service/
│ │ ├── health_check.sh # 服務健康
│ │ └── external_monitor.sh # 外部監控
│ ├── workflow/
│ │ └── n8n_workflow_monitor.sh
│ ├── portal/
│ │ └── page_monitor.sh
│ ├── database/
│ │ ├── schema.sql
│ │ ├── postgres_monitor.sh
│ │ ├── redis_monitor.sh
│ │ └── qdrant_monitor.sh
│ ├── users/
│ │ └── session_tracker.sh
│ └── storage/
│ └── storage_manager.sh
├── momentry_runtime/
│ └── plist/
│ ├── template.service.plist # 用戶級模板
│ ├── template.root.plist # 系統級模板
│ └── *.plist # 服務配置文件
└── docs/
├── SERVICES.md # 服務安裝
└── SERVICE_ADDITION_GUIDE.md # 服務添加規範
```
---
## 六、快速使用
### 初始化
```bash
# 初始化監控資料庫
psql -U accusys -h localhost -d momentry -f monitor/database/schema.sql
# 初始化 Storage 目錄
./monitor/storage/storage_manager.sh init
```
### 監控命令
```bash
cd /Users/accusys/momentry_core_0.1/monitor
# 查看狀態
./control/monitor_control.sh status
# 執行檢查
./control/monitor_control.sh check all # 全部
./control/monitor_control.sh check service # 服務
./control/monitor_control.sh check workflow # Workflow
./control/monitor_control.sh check database # 資料庫
./control/monitor_control.sh check users # 使用者
./control/monitor_control.sh check storage # Storage
# 持續監控
./control/monitor_control.sh monitor
```
---
## 七、優先級
| 優先級 | 項目 |
|--------|------|
| P0 | Service 基礎監控 |
| P0 | Database 監控 |
| P1 | 使用者監控 |
| P1 | n8n Workflow 監控 |
| P2 | WordPress Portal 監控 |
| P2 | External 監控 |
| P3 | Storage 架構 |
---
## 九、服務添加規範
### 9.1 概述
添加新服務到 Momentry 系統的標準流程。
**重要原則**
- 使用 `launchctl` 管理服務,勿使用 `brew services`
- 所有服務使用 `com.momentry.*` 作為 plist Label
- 數據存放於 `/Users/accusys/momentry/` 目錄
- 每個服務需提供監控腳本
### 9.2 命名規範
**Plist 文件命名**
```
com.momentry.{service_name}.plist
```
**目錄結構**
```
/Users/accusys/momentry/
├── var/{service_name}/ # 服務數據
├── etc/{service_name}/ # 服務配置
└── log/{service_name}.log # 服務日誌
```
### 9.3 Plist 模板
**用戶級服務模板** (`plist/template.service.plist`)
```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>/opt/homebrew/bin/executable</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>
```
### 9.4 添加步驟
**步驟 1創建目錄**
```bash
mkdir -p /Users/accusys/momentry/var/{service_name}
mkdir -p /Users/accusys/momentry/etc/{service_name}
```
**步驟 2創建 Plist 文件**
```bash
cp plist/template.service.plist plist/com.momentry.{service_name}.plist
# 編輯 plist 文件
```
**步驟 3複製到系統**
```bash
# 系統級服務
sudo cp plist/com.momentry.{service}.plist /Library/LaunchDaemons/
# 用戶級服務
cp plist/com.momentry.{service}.plist ~/Library/LaunchAgents/
```
**步驟 4載入服務**
```bash
sudo launchctl load /Library/LaunchDaemons/com.momentry.{service}.plist
```
**步驟 5添加監控**
`monitor/config/monitor_config.yaml` 中添加服務配置。
### 9.5 服務管理命令
```bash
# 啟動
sudo launchctl load /Library/LaunchDaemons/com.momentry.{service}.plist
# 停止
sudo launchctl unload /Library/LaunchDaemons/com.momentry.{service}.plist
# 查看狀態
launchctl list | grep momentry
# 查看日誌
tail -f /Users/accusys/momentry/log/{service}.log
```
### 9.6 服務分類
| 類型 | Plist 位置 | 示例 |
|------|------------|------|
| 系統級 | `/Library/LaunchDaemons/` | PostgreSQL, Caddy |
| 用戶級 | `~/Library/LaunchAgents/` | Redis, n8n, Ollama |
### 9.7 模板文件
| 文件 | 說明 |
|------|------|
| `plist/template.service.plist` | 用戶級服務模板 |
| `plist/template.root.plist` | 系統級服務模板 |
| `docs/SERVICE_ADDITION_GUIDE.md` | 完整規範文檔 |
---
## 十、版本歷史
| 版本 | 日期 | 內容 |
|------|------|------|
| 0.1 | 2026-03-15 | 初始版本七層監控架構、15 個服務、Storage 設計 |
| 0.2 | 2026-03-15 | 新增服務添加規範 (第九章)、模板文件 |

451
docs/INSTALL_CADDY.md Normal file
View File

@@ -0,0 +1,451 @@
# Caddy 安裝指南 (本地部署)
## 概述
本文檔說明如何在 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/

394
docs/INSTALL_GITEA.md Normal file
View File

@@ -0,0 +1,394 @@
# Gitea 安裝指南 (本地部署)
## 概述
本文檔說明如何在 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/

380
docs/INSTALL_MARIADB.md Normal file
View File

@@ -0,0 +1,380 @@
# MariaDB 安裝指南 (本地部署)
## 概述
本文檔說明如何在 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/

367
docs/INSTALL_MONGODB.md Normal file
View File

@@ -0,0 +1,367 @@
# MongoDB 安裝指南 (本地部署)
## 概述
本文檔說明如何在 macOS 上安裝 MongoDB Community Edition配置為本地部署支援遠端訪問。
---
## 當前狀態
| 項目 | 狀態 |
|------|------|
| MongoDB (mongodb-community) | ✅ 已安裝 v8.2.6 |
| 數據目錄 | 保留 (/Users/accusys/momentry/var) - 共用 |
| 日誌目錄 | 保留 (/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: 數據目錄 (已存在 - 共用)
數據目錄已存在,無需建立:
- 數據目錄: `/Users/accusys/momentry/var`
- 配置目錄: `/Users/accusys/momentry/etc/mongodb`
- 日誌目錄: `/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: 啟動 MongoDB (後台執行)
```bash
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 &
```
---
### Step 4: 建立資料庫用戶
```bash
mongosh --eval '
use admin
db.createUser({
user: "accusys",
pwd: "Test3200Test3200",
roles: [
{ role: "readWrite", db: "momentry" },
{ role: "dbAdmin", db: "momentry" }
]
})
---
### Step 5: 使用 plist 開機自動啟動
```bash
# 複製 plist 到 LaunchDaemons 目錄
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
```
---
## 監控配置
### 添加到監控配置
`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
# 停止
pkill mongod
# 或
kill <PID>
# 啟動 (後台)
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 &
# 使用 plist (開機自動啟動)
sudo launchctl load /Library/LaunchDaemons/com.momentry.mongodb.plist
```
---
## 連線字串
```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 (共用 - 不要刪除!)

466
docs/INSTALL_N8N.md Normal file
View File

@@ -0,0 +1,466 @@
# n8n 安裝指南 (本地部署)
## 概述
本文檔說明如何在 macOS 上安裝 n8n 工作流自動化平台,配置為本地部署,使用 Queue 模式。
---
## 當前狀態
| 項目 | 狀態 |
|------|------|
| n8n | ✅ 已安裝 v2.3.5 |
| 數據目錄 | /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 |
---
## 安裝步驟
### Step 1: 安裝 n8n (使用 brew)
```bash
# 安裝 n8n
brew install n8n
```
**驗證**:
```bash
n8n --version
# 2.3.5
```
---
### 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 5690-5691
echo "4. Port 5690-5691:"
lsof -i :5690 > /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 :5690
lsof -i :5691
# 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 |
| Worker Broker | 5690 |
| Worker Health Check | 5691 |
---
## 環境變數
`.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
- Worker Ports: 5690-5691
- 數據目錄: /Users/accusys/momentry/var/n8n/
- 日誌目錄: /Users/accusys/momentry/log/
- 數據庫: PostgreSQL n8n
- 隊列: Redis

359
docs/INSTALL_OLLAMA.md Normal file
View File

@@ -0,0 +1,359 @@
# Ollama 安裝指南 (本地部署)
## 概述
本文檔說明如何在 macOS 上安裝 Ollama配置為本地部署用於運行大型語言模型 (LLM)。
---
## 當前狀態
| 項目 | 狀態 |
|------|------|
| Ollama | ✅ 已安裝 v0.13.5 |
| Port | 11434 |
| Models 目錄 | /Users/accusys/.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/

379
docs/INSTALL_PHP.md Normal file
View File

@@ -0,0 +1,379 @@
# PHP 安裝指南 (本地部署)
## 概述
本文檔說明如何在 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/

372
docs/INSTALL_POSTGRESQL.md Normal file
View File

@@ -0,0 +1,372 @@
# PostgreSQL 安裝指南 (本地部署)
## 概述
本文檔說明如何在 macOS 上安裝 PostgreSQL配置為本地部署支援遠端訪問。
---
## 當前狀態
| 項目 | 狀態 |
|------|------|
| PostgreSQL | ✅ 已安裝 v18.1 |
| 數據目錄 | /Users/accusys/momentry/var/postgresql/ |
| 日誌目錄 | /Users/accusys/momentry/log/ |
| Plist | /Library/LaunchDaemons/com.momentry.postgresql.plist |
---
## 安裝步驟
### 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/

456
docs/INSTALL_QDRANT.md Normal file
View File

@@ -0,0 +1,456 @@
# Qdrant 安裝指南 (本地部署)
## 概述
本文檔說明如何在 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/

367
docs/INSTALL_REDIS.md Normal file
View File

@@ -0,0 +1,367 @@
# Redis 安裝指南 (本地部署)
## 概述
本文檔說明如何在 macOS 上安裝 Redis配置為本地部署支援遠端訪問。
---
## 當前狀態
| 項目 | 狀態 |
|------|------|
| Redis | ✅ 已安裝 v8.4.0 |
| 數據目錄 | /Users/accusys/momentry/var/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
```
---
## 版本資訊
- 版本: 8.4.0
- Port: 6379
- Password: accusys
- 數據目錄: /Users/accusys/momentry/var/redis/
- 日誌目錄: /Users/accusys/momentry/log/

284
docs/INSTALL_RUSTDESK.md Normal file
View File

@@ -0,0 +1,284 @@
# RustDesk 安裝指南 (本地部署)
## 概述
本文檔說明如何在 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 添加服務器公式

360
docs/INSTALL_SFTPGO.md Normal file
View File

@@ -0,0 +1,360 @@
# SFTPGo 安裝指南 (本地部署)
## 概述
本文檔說明如何在 macOS 上安裝 SFTPGo配置為本地部署用於 SFTP/FTP/WebDAV 檔案傳輸服務。
---
## 當前狀態
| 項目 | 狀態 |
|------|------|
| SFTPGo | ✅ 已安裝 v2.7.0 |
| Port | 8080 (HTTP), 2022 (SFTP) |
| 配置目錄 | /Users/accusys/momentry/etc/sftpgo/ |
| 日誌目錄 | /Users/accusys/momentry/log/ |
| Plist | /Library/LaunchDaemons/com.momentry.sftpgo.plist |
---
## 安裝步驟
### Step 1: 安裝 SFTPGo (使用 brew)
```bash
# 安裝 SFTPGo
brew install sftpgo
```
**驗證**:
```bash
sftpgo --version
# SFTPGo 2.7.0
```
---
### Step 2: 建立目錄
```bash
# 建立配置目錄
mkdir -p /Users/accusys/momentry/etc/sftpgo
# 建立日誌目錄
mkdir -p /Users/accusys/momentry/log
# 建立工作目錄
mkdir -p /Users/accusys/workspace/sftpgo
# 建立日誌文件
touch /Users/accusys/momentry/log/sftpgo.log
touch /Users/accusys/momentry/log/sftpgo.error.log
# 設定權限
chown -R accusys:staff /Users/accusys/momentry/etc/sftpgo
chown -R accusys:staff /Users/accusys/momentry/log
chown -R accusys:staff /Users/accusys/workspace/sftpgo
```
---
### Step 3: 建立設定檔
建立 `/Users/accusys/momentry/etc/sftpgo/sftpgo.json`:
```json
{
"common": {
"idle_timeout": 15,
"upload_mode": 0,
"max_per_host_connections": 20
},
"users": [
{
"username": "accusys",
"password": "",
"public_keys": [],
"home_dir": "/Users/accusys/workspace/sftpgo",
"uid": 501,
"gid": 20,
"permissions": {
"/": ["*"]
}
}
],
"httpd": {
"bind_port": 8080,
"bind_address": "0.0.0.0"
},
"ftpd": {
"bind_port": 21,
"bind_address": "0.0.0.0"
},
"sftpd": {
"bind_port": 2022,
"bind_address": "0.0.0.0"
},
"webdavd": {
"bind_port": 0,
"bind_address": ""
}
}
```
---
### Step 4: 使用 plist 開機自動啟動
```bash
# 複製 plist 到 LaunchDaemons 目錄
sudo cp /Users/accusys/momentry_core_0.1/momentry_runtime/plist/com.momentry.sftpgo.plist /Library/LaunchDaemons/
# 載入並啟動
sudo launchctl load /Library/LaunchDaemons/com.momentry.sftpgo.plist
```
---
## 監控配置
### 添加到監控配置
`monitor/config/monitor_config.yaml` 中添加:
```yaml
service:
services:
- name: "sftpgo"
type: "http"
port: 8080
host: "localhost"
check_url: "http://localhost:8080/api/v2/info"
timeout: 5
enabled: true
```
---
## 卸載步驟
### 重要: 路徑說明
| 路徑 | 類型 | 說明 |
|------|------|------|
| `/Users/accusys/momentry/etc/sftpgo/` | 配置 | **不要刪除** - SFTPGo 配置 |
| `/Users/accusys/momentry/log/` | 日誌 | **不要刪除** - 日誌目錄 |
| `/Users/accusys/workspace/sftpgo/` | 數據 | **不要刪除** - 上傳檔案目錄 |
| `/opt/homebrew/opt/sftpgo/` | 安裝 | **刪除** - SFTPGo 安裝目錄 |
### Step 1: 停止 SFTPGo
```bash
# 找到 SFTPGo 進程
ps aux | grep sftpgo | grep -v grep
# 停止 SFTPGo
pkill sftpgo
# 確認停止
ps aux | grep sftpgo | grep -v grep || echo "SFTPGo 已停止"
```
---
### Step 2: 卸載 SFTPGo
```bash
# 卸載 SFTPGo
brew uninstall sftpgo
# 移除 plist
sudo launchctl unload /Library/LaunchDaemons/com.momentry.sftpgo.plist
sudo rm /Library/LaunchDaemons/com.momentry.sftpgo.plist
```
---
### Step 3: 刪除專屬檔案
```bash
# 刪除配置目錄 (可選)
rm -rf /Users/accusys/momentry/etc/sftpgo
# 刪除日誌 (可選)
rm -f /Users/accusys/momentry/log/sftpgo.log
rm -f /Users/accusys/momentry/log/sftpgo.error.log
```
**注意: 不要刪除以下目錄**:
```bash
# 這些是重要的,不要刪除!
# /Users/accusys/momentry/etc/sftpgo
# /Users/accusys/momentry/log
# /Users/accusys/workspace/sftpgo
```
---
### Step 4: 卸載後檢查清單
```bash
echo "=== SFTPGo 卸載後檢查 ==="
# 1. 檢查 SFTPGo 進程
echo "1. SFTPGo 進程:"
ps aux | grep sftpgo | grep -v grep && echo " ✗ 仍在運行" || echo " ✓ 已停止"
# 2. Port 8080/2022
echo "2. Port 8080/2022:"
(lsof -i :8080 > /dev/null 2>&1 || lsof -i :2022 > /dev/null 2>&1) && echo " ✗ 仍被佔用" || echo " ✓ 已釋放"
# 3. sftpgo 命令
echo "3. sftpgo 命令:"
which sftpgo > /dev/null 2>&1 && echo " ✗ 仍存在" || echo " ✓ 已移除"
# 4. brew 安裝
echo "4. brew 安裝:"
brew list sftpgo > /dev/null 2>&1 && echo " ✗ 仍存在" || echo " ✓ 已移除"
# 5. launchctl 服務
echo "5. launchctl 服務:"
sudo launchctl list | grep sftpgo > /dev/null 2>&1 && echo " ✗ 仍存在" || echo " ✓ 已移除"
# 6. 配置目錄 (可選刪除)
echo "6. 配置目錄:"
[ -d "/Users/accusys/momentry/etc/sftpgo" ] && echo " ✓ 保留" || echo " ✗ 已刪除"
```
---
## 手動檢查命令
```bash
# 1. 檢查進程
ps aux | grep sftpgo | grep -v grep
# 2. 檢查 Port
lsof -i :8080
lsof -i :2022
# 3. 測試連線
curl http://localhost:8080/
# 4. 查看版本
sftpgo --version
# 5. 驗證配置
sftpgo validate --config /Users/accusys/momentry/etc/sftpgo/sftpgo.json
# 6. 查看日誌
tail -20 /Users/accusys/momentry/log/sftpgo.log
```
---
## 連線資訊
| 項目 | 值 |
|------|-----|
| HTTP/WebDAV | http://localhost:8080 |
| SFTP | localhost:2022 |
| FTP | localhost:21 |
| Admin API | http://localhost:8080/api/v2/info |
---
## 環境變數
`.env` 中:
```env
SFTPGO_CONFIG=/Users/accusys/momentry/etc/sftpgo/sftpgo.json
SFTPGO_DATA_DIR=/Users/accusys/workspace/sftpgo
```
---
## 故障排除
### SFTPGo 無法啟動
```bash
# 檢查日誌
tail -f /Users/accusys/momentry/log/sftpgo.log
# 驗證配置語法
sftpgo validate --config /Users/accusys/momentry/etc/sftpgo/sftpgo.json
# 檢查目錄權限
ls -la /Users/accusys/momentry/etc/sftpgo/
# 重新設定權限
chown -R $(whoami):staff /Users/accusys/momentry/etc/sftpgo
```
### Port 被佔用
```bash
# 檢查哪個程序佔用 port
lsof -i :8080
lsof -i :2022
# 終止佔用程序
kill <PID>
```
### 需要重新載入 plist
```bash
# 卸載舊服務 (如果存在)
sudo launchctl unload /Library/LaunchDaemons/com.momentry.sftpgo.plist 2>/dev/null
# 載入新服務
sudo launchctl load /Library/LaunchDaemons/com.momentry.sftpgo.plist
```
---
## 檔案位置
| 類型 | 路徑 | 說明 |
|------|------|------|
| 安裝 | `/opt/homebrew/opt/sftpgo/` | SFTPGo 安裝目錄 |
| 執行檔 | `/opt/homebrew/opt/sftpgo/bin/sftpgo` | SFTPGo 執行檔 |
| 配置 | `/Users/accusys/momentry/etc/sftpgo/sftpgo.json` | 設定檔 |
| 日誌 | `/Users/accusys/momentry/log/sftpgo.log` | 執行日誌 |
| 錯誤日誌 | `/Users/accusys/momentry/log/sftpgo.error.log` | 錯誤日誌 |
| 工作目錄 | `/Users/accusys/workspace/sftpgo/` | 上傳檔案目錄 |
| plist | `/Library/LaunchDaemons/com.momentry.sftpgo.plist` | 開機啟動 |
| 備份 | `/Users/accusys/momentry/var/sftpgo_backup/sftpgo.json` | 配置備份 |
---
## 常用指令
```bash
# 驗證配置
sftpgo validate --config /Users/accusys/momentry/etc/sftpgo/sftpgo.json
# 查看版本
sftpgo --version
# 查看可用命令
sftpgo --help
# 重載配置 (熱重載)
sftpgo reload --config /Users/accusys/momentry/etc/sftpgo/sftpgo.json
```
---
## 版本資訊
- 版本: 2.7.0
- HTTP Port: 8080
- SFTP Port: 2022
- FTP Port: 21
- 配置: /Users/accusys/momentry/etc/sftpgo/sftpgo.json
- 工作目錄: /Users/accusys/workspace/sftpgo
- 日誌目錄: /Users/accusys/momentry/log/

423
docs/NODEJS.md Normal file
View File

@@ -0,0 +1,423 @@
# Node.js 開發指南
## 概述
本文檔說明 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/5690 | ✅ 執行中 | 工作流自動化平台 |
| - | - | - | - | - | 新增應用請填入此表 |
---
## 新增 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 | 開機啟動 |
---
## 相關文件
- [INSTALL_N8N.md](./INSTALL_N8N.md) - n8n 安裝指南 (包含 Node.js 配置範例)
- [monitor_config.yaml](../monitor/config/monitor_config.yaml) - 監控配置
- [node_monitor.sh](../monitor/service/node_monitor.sh) - Node.js 監控腳本

483
docs/PYTHON.md Normal file
View File

@@ -0,0 +1,483 @@
# Python 開發規範
## 概述
本文檔定義 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
```
---
## 程式碼規範
### 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 進行日誌輸出
- [ ] 錯誤處理適當
- [ ] 類型提示完整
- [ ] 更新監控配置
- [ ] 建立測試案例
---
## 相關文件
- [NODEJS.md](./NODEJS.md) - Node.js 開發指南
- [monitor_config.yaml](../monitor/config/monitor_config.yaml) - 監控配置
- [python_monitor.sh](../monitor/service/python_monitor.sh) - Python 監控腳本

726
docs/SERVICES.md Normal file
View File

@@ -0,0 +1,726 @@
# Momentry 系統服務安裝與管理指南
## 概述
本文檔記錄 momentry 系統所需的所有服務,包括安裝步驟、健康檢查和管理命令。
**重要**: 請勿使用 `brew services` 命令管理服務,否則可能導致 .plist 檔案還原為預設狀態,造成系統異常。請使用 `launchctl` 命令進行管理。
---
## 服務清單
| 服務名稱 | 安裝方式 | 用途 | 狀態 |
|----------|----------|------|-------|
| PostgreSQL | Homebrew | 影片元資料儲存 | 已安裝 |
| Redis | Homebrew | 快取與工作佇列 | 已安裝 |
| Ollama | Homebrew | 本地 LLM 推論 | 已安裝 |
| Caddy | Homebrew | 網頁伺服器 (可選) | 已安裝 |
| Gitea | 手動安裝 | Git 服務 | 已安裝 |
| Grafana | Homebrew | 監控儀表板 | 已安裝 |
| Kafka | 手動安裝 | 訊息佇列 (可選) | 已安裝 |
| MariaDB | Homebrew | 資料庫 (可選) | 已安裝 |
| Netdata | Homebrew | 系統監控 | 已安裝 |
| PHP | Homebrew | Web 後端 | 已安裝 |
| Prometheus | Homebrew | 指標收集 | 已安裝 |
| SeaweedFS | 手動安裝 | 分散式儲存 (可選) | 已安裝 |
| SFTPGo | 手動安裝 | SFTP 服務 | 已安裝 |
| n8n | Homebrew | 工作流自動化 | 已安裝 |
---
## 必要服務 (Momentry 核心)
### 1. PostgreSQL
#### 安裝
```bash
# 檢查是否已安裝
brew list postgresql@18 2>/dev/null || echo "Not installed"
# 安裝 PostgreSQL 18
brew install postgresql@18
```
#### 初始化資料庫
```bash
# 初始化資料庫 (如尚未初始化)
initdb /usr/local/var/postgresql@18
# 建立 momentry 資料庫
createdb -U accusys momentry
```
#### 開機自動啟動
```bash
# 建立 plist 檔案
sudo tee /Library/LaunchDaemons/com.momentry.postgresql.plist > /dev/null <<'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.postgresql</string>
<key>ProgramArguments</key>
<array>
<string>/opt/homebrew/opt/postgresql@18/bin/pg_ctl</string>
<string>-D</string>
<string>/opt/homebrew/var/postgresql@18</string>
<string>-l</string>
<string>/opt/homebrew/var/postgresql@18/logfile</string>
<string>start</string>
</array>
<key>RunAtLoad</key>
<true/>
<key>KeepAlive</key>
<true/>
<key>WorkingDirectory</key>
<string>/opt/homebrew/var/postgresql@18</string>
<key>UserName</key>
<string>accusys</string>
</dict>
</plist>
EOF
# 載入服務
sudo launchctl load /Library/LaunchDaemons/com.momentry.postgresql.plist
```
#### 管理命令
```bash
# 啟動
sudo launchctl load /Library/LaunchDaemons/com.momentry.postgresql.plist
# 停止
sudo launchctl unload /Library/LaunchDaemons/com.momentry.postgresql.plist
# 重新載入
sudo launchctl unload /Library/LaunchDaemons/com.momentry.postgresql.plist
sudo launchctl load /Library/LaunchDaemons/com.momentry.postgresql.plist
```
#### 健康檢查
```bash
# 方法 1: 使用 pg_isready
pg_isready -h localhost -p 5432 -U accusys
# 方法 2: 連線測試
psql -U accusys -h localhost -d momentry -c "SELECT 1;"
# 方法 3: 檢查程序
pgrep -f "postgres.*postgresql@18"
```
---
### 2. Redis
#### 安裝
```bash
# 檢查是否已安裝
brew list redis 2>/dev/null || echo "Not installed"
# 安裝 Redis
brew install redis
```
#### 設定密碼
```bash
# 編輯 Redis 設定檔
vim /opt/homebrew/etc/redis.conf
# 找到 requirepass 行,修改為:
requirepass accusys
# 或使用環境變數方式啟動
```
#### 開機自動啟動
```bash
# 建立 plist 檔案
sudo tee /Library/LaunchDaemons/com.momentry.redis.plist > /dev/null <<'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.redis</string>
<key>UserName</key>
<string>accusys</string>
<key>WorkingDirectory</key>
<string>/Users/accusys/momentry/var/redis</string>
<key>ProgramArguments</key>
<array>
<string>/opt/homebrew/opt/redis/bin/redis-server</string>
<string>/opt/homebrew/etc/redis.conf</string>
</array>
<key>EnvironmentVariables</key>
<dict>
<key>REDIS_PASSWORD</key>
<string>accusys</string>
</dict>
<key>RunAtLoad</key>
<true/>
<key>KeepAlive</key>
<true/>
<key>StandardOutPath</key>
<string>/Users/accusys/momentry/log/redis.log</string>
<key>StandardErrorPath</key>
<string>/Users/accusys/momentry/log/redis.error.log</string>
</dict>
</plist>
EOF
# 載入服務
sudo launchctl load /Library/LaunchDaemons/com.momentry.redis.plist
```
#### 管理命令
```bash
# 啟動
sudo launchctl load /Library/LaunchDaemons/com.momentry.redis.plist
# 停止
sudo launchctl unload /Library/LaunchDaemons/com.momentry.redis.plist
# 重啟
sudo launchctl unload /Library/LaunchDaemons/com.momentry.redis.plist
sudo launchctl load /Library/LaunchDaemons/com.momentry.redis.plist
# 查看狀態
launchctl list | grep com.momentry.redis
# 查看日誌
tail -f /Users/accusys/momentry/log/redis.log
tail -f /Users/accusys/momentry/log/redis.error.log
```
#### 健康檢查
```bash
# 方法 1: 使用 redis-cli ping
redis-cli -a accusys ping
# 輸出應為: PONG
# 方法 2: 檢查密碼認證
redis-cli -a accusys AUTH accusys
# 方法 3: 檢查程序
pgrep -f redis-server
# 方法 4: 檢查連線數
redis-cli -a accusys INFO clients
```
---
### 3. Ollama
#### 安裝
```bash
# 檢查是否已安裝
which ollama || echo "Not installed"
# 安裝 Ollama
brew install ollama
```
#### 模型下載
```bash
# 下載 Mistral (LLM)
ollama pull mistral:latest
# 下載 Embedding 模型
ollama pull nomic-embed-text:latest
# 驗證模型
ollama list
```
#### 開機自動啟動
```bash
# 建立 plist 檔案
sudo tee /Library/LaunchDaemons/com.momentry.ollama.plist > /dev/null <<'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.ollama</string>
<key>UserName</key>
<string>accusys</string>
<key>WorkingDirectory</key>
<string>/Users/accusys/momentry/var/ollama</string>
<key>ProgramArguments</key>
<array>
<string>/opt/homebrew/bin/ollama</string>
<string>serve</string>
</array>
<key>EnvironmentVariables</key>
<dict>
<key>OLLAMA_HOST</key>
<string>0.0.0.0:11434</string>
<key>OLLAMA_MODELS</key>
<string>/Users/accusys/momentry/var/ollama/models</string>
</dict>
<key>RunAtLoad</key>
<true/>
<key>KeepAlive</key>
<true/>
<key>StandardOutPath</key>
<string>/Users/accusys/momentry/log/ollama.log</string>
<key>StandardErrorPath</key>
<string>/Users/accusys/momentry/log/ollama.error.log</string>
</dict>
</plist>
EOF
# 載入服務
sudo launchctl load /Library/LaunchDaemons/com.momentry.ollama.plist
```
#### 管理命令
```bash
# 啟動
sudo launchctl load /Library/LaunchDaemons/com.momentry.ollama.plist
# 停止
sudo launchctl unload /Library/LaunchDaemons/com.momentry.ollama.plist
# 重啟
sudo launchctl unload /Library/LaunchDaemons/com.momentry.ollama.plist
sudo launchctl load /Library/LaunchDaemons/com.momentry.ollama.plist
# 查看狀態
launchctl list | grep com.momentry.ollama
# 查看日誌
tail -f /Users/accusys/momentry/log/ollama.log
tail -f /Users/accusys/momentry/log/ollama.error.log
```
#### 健康檢查
```bash
# 方法 1: API 測試
curl -s http://localhost:11434/api/tags | jq '.models[].name'
# 方法 2: 檢查程序
pgrep -f ollama
# 方法 3: 列出模型
ollama list
```
---
### 4. n8n (工作流自動化)
#### 安裝
```bash
# 檢查是否已安裝
which n8n || echo "Not installed"
# 安裝 n8n
brew install n8n
```
#### 開機自動啟動
```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/
# 載入服務
sudo launchctl load /Library/LaunchDaemons/com.momentry.n8n.main.plist
sudo launchctl load /Library/LaunchDaemons/com.momentry.n8n.worker.plist
```
#### 管理命令
```bash
# 啟動
sudo launchctl load /Library/LaunchDaemons/com.momentry.n8n.main.plist
sudo launchctl load /Library/LaunchDaemons/com.momentry.n8n.worker.plist
# 停止
sudo launchctl unload /Library/LaunchDaemons/com.momentry.n8n.main.plist
sudo launchctl unload /Library/LaunchDaemons/com.momentry.n8n.worker.plist
```
#### 健康檢查
```bash
# 方法 1: API 測試
curl -s http://localhost:5678/
# 方法 2: 檢查程序
ps aux | grep n8n | grep -v grep
# 方法 3: 檢查端口
lsof -i :5678
lsof -i :5690
```
---
## 可選服務
### 4. Caddy (網頁伺服器)
```bash
# 安裝
brew install caddy
# 開機啟動
cp /opt/homebrew.mxcl.caddy.plist /Library/LaunchDaemons/
sudo launchctl load /Library/LaunchDaemons/homebrew.mxcl.caddy.plist
# 健康檢查
curl -s https://localhost:2019/config/ | head -5
```
### 5. Grafana (監控)
```bash
# 安裝
brew install grafana
# 開機啟動
cp /opt/homebrew.mxcl.grafana.plist ~/Library/LaunchAgents/
launchctl load ~/Library/LaunchAgents/homebrew.mxcl.grafana.plist
# 健康檢查
curl -s http://localhost:3000/api/health | jq '.'
```
### 6. Prometheus (監控)
```bash
# 安裝
brew install prometheus
# 開機啟動
cp /opt/homebrew.mxcl.prometheus.plist ~/Library/LaunchAgents/
launchctl load ~/Library/LaunchAgents/homebrew.mxcl.prometheus.plist
# 健康檢查
curl -s http://localhost:9090/-/healthy
```
### 7. Netdata (系統監控)
```bash
# 安裝
brew install netdata
# 開機啟動
sudo brew services start netdata
# 健康檢查
curl -s http://localhost:19999/api/v1/info | jq '.version'
```
---
## 統一健康檢查腳本
建立 `/Users/accusys/momentry_core_0.1/scripts/health_check.sh`:
```bash
#!/bin/bash
# Momentry 系統健康檢查腳本
echo "========================================"
echo "Momentry 系統健康檢查"
echo "========================================"
echo ""
# 顏色定義
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
check_service() {
local name=$1
local check_cmd=$2
if eval "$check_cmd" > /dev/null 2>&1; then
echo -e "${GREEN}${NC} $name"
return 0
else
echo -e "${RED}${NC} $name"
return 1
fi
}
total=0
passed=0
# 1. PostgreSQL
total=$((total + 1))
check_service "PostgreSQL (localhost:5432)" "pg_isready -h localhost -p 5432 -U accusys" && passed=$((passed + 1))
# 2. Redis
total=$((total + 1))
check_service "Redis (localhost:6379)" "redis-cli -a accusys ping" && passed=$((passed + 1))
# 3. Ollama
total=$((total + 1))
check_service "Ollama (localhost:11434)" "curl -s http://localhost:11434/api/tags > /dev/null" && passed=$((passed + 1))
# 4. n8n
total=$((total + 1))
check_service "n8n (localhost:5678)" "curl -s http://localhost:5678/ > /dev/null" && passed=$((passed + 1))
# 5. Grafana (如果安裝)
if command -v grafana-server > /dev/null 2>&1; then
total=$((total + 1))
check_service "Grafana (localhost:3000)" "curl -s http://localhost:3000/api/health > /dev/null" && passed=$((passed + 1))
fi
# 6. Prometheus (如果安裝)
if command -v prometheus > /dev/null 2>&1; then
total=$((total + 1))
check_service "Prometheus (localhost:9090)" "curl -s http://localhost:9090/-/healthy > /dev/null" && passed=$((passed + 1))
fi
# 7. Netdata (如果安裝)
if command -v netdata > /dev/null 2>&1; then
total=$((total + 1))
check_service "Netdata (localhost:19999)" "curl -s http://localhost:19999/api/v1/info > /dev/null" && passed=$((passed + 1))
fi
echo ""
echo "========================================"
echo "結果: $passed/$total 服務正常"
echo "========================================"
if [ $passed -eq $total ]; then
exit 0
else
exit 1
fi
```
使用方式:
```bash
chmod +x scripts/health_check.sh
./scripts/health_check.sh
```
---
## 服務管理速查表
### 啟動服務
```bash
# PostgreSQL (需要 root 權限)
sudo launchctl load /Library/LaunchDaemons/com.momentry.postgresql.plist
# Redis, Ollama (需要 root 權限)
sudo launchctl load /Library/LaunchDaemons/com.momentry.redis.plist
sudo launchctl load /Library/LaunchDaemons/com.momentry.ollama.plist
# n8n (需要 root 權限)
sudo launchctl load /Library/LaunchDaemons/com.momentry.n8n.main.plist
sudo launchctl load /Library/LaunchDaemons/com.momentry.n8n.worker.plist
```
### 停止服務
```bash
# PostgreSQL
sudo launchctl unload /Library/LaunchDaemons/com.momentry.postgresql.plist
# Redis, Ollama
sudo launchctl unload /Library/LaunchDaemons/com.momentry.redis.plist
sudo launchctl unload /Library/LaunchDaemons/com.momentry.ollama.plist
# n8n
sudo launchctl unload /Library/LaunchDaemons/com.momentry.n8n.main.plist
sudo launchctl unload /Library/LaunchDaemons/com.momentry.n8n.worker.plist
```
### 查詢服務狀態
```bash
# 查看所有服務
launchctl list | grep -E "(postgres|redis|ollama|n8n|grafana|prometheus)"
# 查看特定服務
launchctl list | grep com.momentry
```
---
## 故障排除
### PostgreSQL 問題
```bash
# 查看日誌
tail -f /opt/homebrew/var/postgresql@18/logfile
# 重新初始化
pg_ctl -D /opt/homebrew/var/postgresql@18 stop
rm -rf /opt/homebrew/var/postgresql@18
initdb -D /opt/homebrew/var/postgresql@18
# 重建資料庫
dropdb momentry
createdb -U accusys momentry
```
### Redis 問題
```bash
# 查看日誌
tail -f /opt/homebrew/var/log/redis.log
# 測試連線
redis-cli -a accusys DEBUG SLEEP 1
# 重新整理 ACL
redis-cli -a accusys FLUSHALL
```
### Ollama 問題
```bash
# 查看日誌
tail -f ~/.ollama/logs/server.log
# 重新下載模型
ollama pull mistral:latest
ollama pull nomic-embed-text:latest
# 檢查 GPU 使用情況
ollama list
```
---
## 自動化腳本
建立 `/Users/accusys/momentry_core_0.1/scripts/service_manager.sh`:
```bash
#!/bin/bash
set -e
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PLIST_DIR="$SCRIPT_DIR/../momentry_runtime/plist"
action=${1:-start}
service=${2:-all}
start_postgresql() {
echo "Starting PostgreSQL..."
sudo launchctl load /Library/LaunchDaemons/com.momentry.postgresql.plist 2>/dev/null || \
echo "PostgreSQL plist not found, skipping..."
}
start_redis() {
echo "Starting Redis..."
launchctl load ~/Library/LaunchAgents/com.momentry.redis.plist 2>/dev/null || \
echo "Redis plist not found, skipping..."
}
start_ollama() {
echo "Starting Ollama..."
launchctl load ~/Library/LaunchAgents/com.momentry.ollama.plist 2>/dev/null || \
echo "Ollama plist not found, skipping..."
}
stop_postgresql() {
echo "Stopping PostgreSQL..."
sudo launchctl unload /Library/LaunchDaemons/com.momentry.postgresql.plist 2>/dev/null || true
}
stop_redis() {
echo "Stopping Redis..."
launchctl unload ~/Library/LaunchAgents/com.momentry.redis.plist 2>/dev/null || true
}
stop_ollama() {
echo "Stopping Ollama..."
launchctl unload ~/Library/LaunchAgents/com.momentry.ollama.plist 2>/dev/null || true
}
case $action in
start)
case $service in
all)
start_postgresql
start_redis
start_ollama
;;
postgresql|pgsql|pg)
start_postgresql
;;
redis)
start_redis
;;
ollama)
start_ollama
;;
*)
echo "Unknown service: $service"
exit 1
;;
esac
;;
stop)
case $service in
all)
stop_ollama
stop_redis
stop_postgresql
;;
postgresql|pgsql|pg)
stop_postgresql
;;
redis)
stop_redis
;;
ollama)
stop_ollama
;;
*)
echo "Unknown service: $service"
exit 1
;;
esac
;;
restart)
$0 stop $service
sleep 2
$0 start $service
;;
status)
echo "Service Status:"
echo "==============="
launchctl list | grep -E "(postgres|redis|ollama)" || echo "No services found"
;;
*)
echo "Usage: $0 {start|stop|restart|status} [service]"
echo "Services: all, postgresql, redis, ollama"
exit 1
;;
esac
```
---
## 附錄: 服務對應表
| 服務 | Port | 使用者 | plist 位置 |
|------|------|--------|-------------|
| PostgreSQL | 5432 | accusys | /Library/LaunchDaemons/ |
| Redis | 6379 | accusys | /Library/LaunchDaemons/ |
| Ollama | 11434 | accusys | /Library/LaunchDaemons/ |
| n8n Main | 5678 | accusys | /Library/LaunchDaemons/ |
| n8n Worker | 5690-5691 | accusys | /Library/LaunchDaemons/ |
| Grafana | 3000 | accusys | /Library/LaunchDaemons/ |
| Prometheus | 9090 | accusys | /Library/LaunchDaemons/ |
| Caddy | 2019 | root | /Library/LaunchDaemons/ |
| Netdata | 19999 | root | /Library/LaunchDaemons/ |

View File

@@ -0,0 +1,659 @@
# Momentry 服務添加規範 v2.0
## 一、概述
本文檔定義在 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
# 啟動服務
sudo launchctl load /Library/LaunchDaemons/com.momentry.{service}.plist
# 停止服務
sudo launchctl unload /Library/LaunchDaemons/com.momentry.{service}.plist
# 重啟服務
sudo launchctl unload /Library/LaunchDaemons/com.momentry.{service}.plist
sudo launchctl load /Library/LaunchDaemons/com.momentry.{service}.plist
# 查看服務狀態
launchctl list | grep momentry
# 查看服務日誌
tail -f /Users/accusys/momentry/log/{service}.log
tail -f /Users/accusys/momentry/log/{service}.error.log
```
### 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 | 新增服務備份作業、服務完整刪除作業 |

32
momentry_runtime/build.sh Executable file
View File

@@ -0,0 +1,32 @@
#!/bin/bash
set -e
# Momentry Build Script
cd /Users/accusys/momentry_core_0.1
echo "========================================"
echo "Building Momentry"
echo "========================================"
# Check if cargo is available
if ! command -v cargo > /dev/null 2>&1; then
echo "ERROR: Rust/Cargo not found. Please install Rust first."
echo "Visit: https://rustup.rs"
exit 1
fi
# Build in release mode
echo "Building release version..."
cargo build --release
# Also build video_player
cd /Users/accusys/video_player
echo "Building video_player..."
cargo build --release
echo "========================================"
echo "Build Complete!"
echo "========================================"
echo "Momentry binary: /Users/accusys/momentry_core_0.1/target/release/momentry"
echo "Video player: /Users/accusys/video_player/target/release/video_player"

86
momentry_runtime/check.sh Executable file
View File

@@ -0,0 +1,86 @@
#!/bin/bash
# Momentry Health Check Script
echo "========================================"
echo "Momentry Health Check"
echo "========================================"
echo ""
# Color codes
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m'
check() {
local name="$1"
local cmd="$2"
if eval "$cmd" > /dev/null 2>&1; then
echo -e "${GREEN}${NC} $name"
return 0
else
echo -e "${RED}${NC} $name"
return 1
fi
}
total=0
passed=0
# PostgreSQL
total=$((total + 1))
check "PostgreSQL (localhost:5432)" "pg_isready -h localhost -p 5432 -U accusys" && passed=$((passed + 1))
# Redis
total=$((total + 1))
check "Redis (localhost:6379)" "redis-cli -a accusys ping" && passed=$((passed + 1))
# MongoDB - Port
total=$((total + 1))
check "MongoDB Port (27017)" "lsof -i :27017 > /dev/null 2>&1" && passed=$((passed + 1))
# MongoDB - Process
total=$((total + 1))
check "MongoDB Process" "pgrep -f mongod" && passed=$((passed + 1))
# MongoDB - Connection
total=$((total + 1))
check "MongoDB Connection" "mongosh --eval 'db.adminCommand(\"ping\")' > /dev/null 2>&1" && passed=$((passed + 1))
# Ollama
total=$((total + 1))
check "Ollama (localhost:11434)" "curl -s http://localhost:11434/api/tags > /dev/null" && passed=$((passed + 1))
# Momentry API
total=$((total + 1))
check "Momentry API (localhost:3000)" "curl -s http://localhost:3000/health > /dev/null" && passed=$((passed + 1))
# ffprobe
total=$((total + 1))
check "ffprobe" "which ffprobe" && passed=$((passed + 1))
# Check video_player binary
total=$((total + 1))
check "video_player binary" "test -f /Users/accusys/video_player/target/release/video_player" && passed=$((passed + 1))
# Check momentry binary
total=$((total + 1))
check "momentry binary" "test -f /Users/accusys/momentry_core_0.1/target/release/momentry" && passed=$((passed + 1))
echo ""
echo "========================================"
echo "Result: $passed/$total checks passed"
echo "========================================"
# Show status if failures
if [ $passed -lt $total ]; then
echo ""
echo "Failed services:"
[ $(pg_isready -h localhost -p 5432 -U accusys > /dev/null 2>&1; echo $?) -ne 0 ] && echo " - PostgreSQL: brew services start postgresql@18"
[ $(redis-cli -a accusys ping > /dev/null 2>&1; echo $?) -ne 0 ] && echo " - Redis: launchctl load ~/Library/LaunchAgents/com.momentry.redis.plist"
[ $(curl -s http://localhost:11434/api/tags > /dev/null 2>&1; echo $?) -ne 0 ] && echo " - Ollama: launchctl load ~/Library/LaunchAgents/com.momentry.ollama.plist"
fi
exit $([ $passed -eq $total ] && echo 0 || echo 1)

65
momentry_runtime/env/momentry.env vendored Normal file
View File

@@ -0,0 +1,65 @@
# Momentry Environment Variables Template
# Copy this file to .env and update with your values
# ===================
# Database Configuration
# ===================
DATABASE_URL=postgres://accusys@localhost:5432/momentry
# ===================
# Redis Configuration
# ===================
REDIS_URL=redis://accusys:accusys@localhost:6379
REDIS_PASSWORD=accusys
# ===================
# MongoDB (Optional)
# ===================
# MONGODB_URL=mongodb://localhost:27017
# MONGODB_DATABASE=momentry
# ===================
# Qdrant (Optional - Vector Database)
# ===================
# QDRANT_URL=http://localhost:6333
# QDRANT_COLLECTION=momentry_chunks
# ===================
# API Server
# ===================
API_HOST=127.0.0.1
API_PORT=3000
# ===================
# Watch Directories
# ===================
WATCH_DIRECTORIES=~/Videos,~/momentry_core_project/test_video
# ===================
# Ollama (LLM)
# ===================
OLLAMA_HOST=http://localhost:11434
# ===================
# Email Notification (SMTP)
# ===================
# SMTP_HOST=smtp.gmail.com
# SMTP_PORT=587
# SMTP_USER=your_email@gmail.com
# SMTP_PASSWORD=your_app_password
# SMTP_FROM=momentry@example.com
# SMTP_TO=admin@example.com
# ===================
# Backup Configuration
# ===================
BACKUP_PATH=~/momentry_core_0.1/backups
BACKUP_AUTO_ENABLED=true
BACKUP_SCHEDULE="0 2 * * *" # Daily at 2am
BACKUP_RETENTION_DAYS=7
# ===================
# Model Paths (Optional)
# ===================
# EMBEDDING_MODEL_PATH=./models/comic-embed-text
# LLM_MODEL_PATH=./models/mistral-7b

View File

@@ -0,0 +1,47 @@
<?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.agent</string>
<key>ProgramArguments</key>
<array>
<string>/Users/accusys/momentry_core_0.1/momentry_runtime/start.sh</string>
</array>
<key>RunAtLoad</key>
<true/>
<key>KeepAlive</key>
<true/>
<key>EnvironmentVariables</key>
<dict>
<key>HOME</key>
<string>/Users/accusys</string>
<key>USER</key>
<string>accusys</string>
<key>PATH</key>
<string>/opt/homebrew/bin:/opt/homebrew/sbin:/usr/local/bin:/usr/bin:/bin:/Users/accusys/.cargo/bin</string>
<key>DATABASE_URL</key>
<string>postgres://accusys@localhost:5432/momentry</string>
<key>REDIS_URL</key>
<string>redis://accusys:accusys@localhost:6379</string>
<key>OLLAMA_HOST</key>
<string>http://localhost:11434</string>
</dict>
<key>StandardOutPath</key>
<string>/Users/accusys/momentry_core_0.1/momentry_runtime/logs/stdout.log</string>
<key>StandardErrorPath</key>
<string>/Users/accusys/momentry_core_0.1/momentry_runtime/logs/stderr.log</string>
<key>WorkingDirectory</key>
<string>/Users/accusys/momentry_core_0.1</string>
<key>ProcessType</key>
<string>Interactive</string>
</dict>
</plist>

View File

@@ -0,0 +1,42 @@
<?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.caddy</string>
<key>UserName</key>
<string>root</string>
<key>EnvironmentVariables</key>
<dict>
<key>HOME</key>
<string>/opt/homebrew/var/lib/caddy</string>
<key>XDG_DATA_HOME</key>
<string>/opt/homebrew/var/lib</string>
</dict>
<key>WorkingDirectory</key>
<string>/Users/accusys/momentry/var/caddy</string>
<key>ProgramArguments</key>
<array>
<string>/opt/homebrew/opt/caddy/bin/caddy</string>
<string>run</string>
<string>--config</string>
<string>/Users/accusys/momentry/etc/Caddyfile</string>
</array>
<key>RunAtLoad</key>
<true/>
<key>KeepAlive</key>
<true/>
<key>StandardErrorPath</key>
<string>/Users/accusys/momentry/log/caddy.error.log</string>
<key>StandardOutPath</key>
<string>/Users/accusys/momentry/log/caddy.log</string>
</dict>
</plist>

View File

@@ -0,0 +1,44 @@
<?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</string>
<key>UserName</key>
<string>accusys</string>
<key>WorkingDirectory</key>
<string>/Users/accusys/momentry/var/gitea</string>
<key>ProgramArguments</key>
<array>
<string>/opt/homebrew/opt/gitea/bin/gitea</string>
<string>web</string>
<string>--config</string>
<string>/Users/accusys/momentry/etc/gitea/app.ini</string>
</array>
<key>EnvironmentVariables</key>
<dict>
<key>HOME</key>
<string>/Users/accusys</string>
<key>GITEA_WORK_DIR</key>
<string>/Users/accusys/momentry/var/gitea</string>
<key>PATH</key>
<string>/opt/homebrew/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/gitea.log</string>
<key>StandardErrorPath</key>
<string>/Users/accusys/momentry/log/gitea.error.log</string>
</dict>
</plist>

View File

@@ -0,0 +1,32 @@
<?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.mariadb</string>
<key>UserName</key>
<string>accusys</string>
<key>WorkingDirectory</key>
<string>/Users/accusys/momentry/var/mariadb</string>
<key>ProgramArguments</key>
<array>
<string>/opt/homebrew/opt/mariadb/bin/mariadbd-safe</string>
<string>--datadir=/Users/accusys/momentry/var/mariadb</string>
</array>
<key>RunAtLoad</key>
<true/>
<key>KeepAlive</key>
<true/>
<key>StandardErrorPath</key>
<string>/Users/accusys/momentry/log/mariadb.error.log</string>
<key>StandardOutPath</key>
<string>/Users/accusys/momentry/log/mariadb.log</string>
</dict>
</plist>

View File

@@ -0,0 +1,39 @@
<?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.mongodb</string>
<key>ProgramArguments</key>
<array>
<string>/opt/homebrew/bin/mongod</string>
<string>--dbpath</string>
<string>/Users/accusys/momentry/var</string>
<string>--logpath</string>
<string>/Users/accusys/momentry/log/mongodb.log</string>
<string>--port</string>
<string>27017</string>
<string>--bind_ip</string>
<string>0.0.0.0</string>
</array>
<key>RunAtLoad</key>
<true/>
<key>KeepAlive</key>
<true/>
<key>UserName</key>
<string>accusys</string>
<key>StandardErrorPath</key>
<string>/Users/accusys/momentry/log/mongodb.error.log</string>
<key>StandardOutPath</key>
<string>/Users/accusys/momentry/log/mongodb.log</string>
<key>WorkingDirectory</key>
<string>/Users/accusys/momentry/var/mongodb/</string>
</dict>
</plist>

View File

@@ -0,0 +1,49 @@
<?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.monitor</string>
<key>UserName</key>
<string>accusys</string>
<key>WorkingDirectory</key>
<string>/Users/accusys/momentry_core_0.1/monitor</string>
<key>ProgramArguments</key>
<array>
<string>/bin/bash</string>
<string>/Users/accusys/momentry_core_0.1/monitor/control/monitor_control.sh</string>
<string>check</string>
<string>all</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>
<key>RunAtLoad</key>
<false/>
<key>StartInterval</key>
<integer>300</integer>
<key>KeepAlive</key>
<dict>
<key>SuccessfulExit</key>
<false/>
</dict>
<key>StandardOutPath</key>
<string>/Users/accusys/momentry/log/monitor/monitor.log</string>
<key>StandardErrorPath</key>
<string>/Users/accusys/momentry/log/monitor/monitor.error.log</string>
<key>ProcessType</key>
<string>Background</string>
</dict>
</plist>

View File

@@ -0,0 +1,102 @@
<?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.n8n.main</string>
<key>UserName</key>
<string>accusys</string>
<key>WorkingDirectory</key>
<string>/Users/accusys/momentry/var/n8n</string>
<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>
<key>N8N_ENCRYPTION_KEY</key>
<string>Test3200Test3200Test3200</string>
<key>DOMAIN_NAME</key>
<string>n8n.momentry.ddns.net</string>
<key>WEBHOOK_URL</key>
<string>https://n8n.momentry.ddns.net/</string>
<key>N8N_PORT</key>
<string>5678</string>
<key>N8N_LISTEN_ADDRESS</key>
<string>0.0.0.0</string>
<key>N8N_USER_MANAGEMENT_JWT_DURATION</key>
<string>1h</string>
<key>GENERIC_TIMEZONE</key>
<string>Asia/Taipei</string>
<key>TZ</key>
<string>Asia/Taipei</string>
<key>N8N_LOG_LEVEL</key>
<string>info</string>
<key>DB_TYPE</key>
<string>postgresdb</string>
<key>DB_POSTGRESDB_HOST</key>
<string>127.0.0.1</string>
<key>DB_POSTGRESDB_PORT</key>
<string>5432</string>
<key>DB_POSTGRESDB_DATABASE</key>
<string>n8n</string>
<key>DB_POSTGRESDB_USER</key>
<string>n8n</string>
<key>DB_POSTGRESDB_PASSWORD</key>
<string>accusys</string>
<key>EXECUTIONS_MODE</key>
<string>queue</string>
<key>QUEUE_BULL_REDIS_HOST</key>
<string>127.0.0.1</string>
<key>QUEUE_BULL_REDIS_PORT</key>
<string>6379</string>
<key>QUEUE_BULL_REDIS_PASSWORD</key>
<string>accusys</string>
<key>N8N_USER_FOLDER</key>
<string>/Users/accusys/momentry/var/n8n</string>
<key>N8N_METRICS</key>
<string>true</string>
</dict>
<key>RunAtLoad</key>
<true/>
<key>KeepAlive</key>
<true/>
<key>StandardOutPath</key>
<string>/Users/accusys/momentry/log/n8n-main.log</string>
<key>StandardErrorPath</key>
<string>/Users/accusys/momentry/log/n8n-main.error.log</string>
</dict>
</plist>

View File

@@ -0,0 +1,105 @@
<?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.n8n.worker</string>
<key>UserName</key>
<string>accusys</string>
<key>GroupName</key>
<string>staff</string>
<key>WorkingDirectory</key>
<string>/Users/accusys/momentry/var/n8n</string>
<key>ProgramArguments</key>
<array>
<string>/opt/homebrew/opt/node@22/bin/node</string>
<string>/opt/homebrew/lib/node_modules/n8n/bin/n8n</string>
<string>worker</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>
<key>N8N_ENCRYPTION_KEY</key>
<string>Test3200Test3200Test3200</string>
<key>DOMAIN_NAME</key>
<string>n8n.momentry.ddns.net</string>
<key>WEBHOOK_URL</key>
<string>https://n8n.momentry.ddns.net/</string>
<key>N8N_LISTEN_ADDRESS</key>
<string>0.0.0.0</string>
<key>GENERIC_TIMEZONE</key>
<string>Asia/Taipei</string>
<key>TZ</key>
<string>Asia/Taipei</string>
<key>N8N_LOG_LEVEL</key>
<string>info</string>
<key>DB_TYPE</key>
<string>postgresdb</string>
<key>DB_POSTGRESDB_HOST</key>
<string>127.0.0.1</string>
<key>DB_POSTGRESDB_PORT</key>
<string>5432</string>
<key>DB_POSTGRESDB_DATABASE</key>
<string>n8n</string>
<key>DB_POSTGRESDB_USER</key>
<string>n8n</string>
<key>DB_POSTGRESDB_PASSWORD</key>
<string>accusys</string>
<key>EXECUTIONS_MODE</key>
<string>queue</string>
<key>QUEUE_BULL_REDIS_HOST</key>
<string>127.0.0.1</string>
<key>QUEUE_BULL_REDIS_PORT</key>
<string>6379</string>
<key>QUEUE_BULL_REDIS_PASSWORD</key>
<string>accusys</string>
<key>N8N_USER_FOLDER</key>
<string>/Users/accusys/momentry/var/n8n</string>
<key>N8N_RUNNERS_BROKER_PORT</key>
<string>5690</string>
<key>N8N_RUNNERS_LAUNCHER_HEALTH_CHECK_PORT</key>
<string>5691</string>
<key>N8N_METRICS</key>
<string>true</string>
</dict>
<key>RunAtLoad</key>
<true/>
<key>KeepAlive</key>
<true/>
<key>StandardOutPath</key>
<string>/Users/accusys/momentry/log/n8n-worker.log</string>
<key>StandardErrorPath</key>
<string>/Users/accusys/momentry/log/n8n-worker.error.log</string>
</dict>
</plist>

View File

@@ -0,0 +1,44 @@
<?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.ollama</string>
<key>UserName</key>
<string>accusys</string>
<key>EnvironmentVariables</key>
<dict>
<key>OLLAMA_HOST</key>
<string>0.0.0.0:11434</string>
<key>OLLAMA_MODELS</key>
<string>/Users/accusys/momentry/var/ollama/models</string>
<key>OLLAMA_FLASH_ATTENTION</key>
<string>1</string>
<key>OLLAMA_KV_CACHE_TYPE</key>
<string>q8_0</string>
</dict>
<key>WorkingDirectory</key>
<string>/Users/accusys/momentry/var/ollama</string>
<key>ProgramArguments</key>
<array>
<string>/opt/homebrew/opt/ollama/bin/ollama</string>
<string>serve</string>
</array>
<key>RunAtLoad</key>
<true/>
<key>KeepAlive</key>
<true/>
<key>StandardErrorPath</key>
<string>/Users/accusys/momentry/log/ollama.error.log</string>
<key>StandardOutPath</key>
<string>/Users/accusys/momentry/log/ollama.log</string>
</dict>
</plist>

View File

@@ -0,0 +1,34 @@
<?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.php</string>
<key>UserName</key>
<string>accusys</string>
<key>WorkingDirectory</key>
<string>/Users/accusys/momentry/var</string>
<key>ProgramArguments</key>
<array>
<string>/opt/homebrew/opt/php/sbin/php-fpm</string>
<string>--nodaemonize</string>
<string>--fpm-config</string>
<string>/Users/accusys/momentry/etc/php/8.5/php-fpm.conf</string>
</array>
<key>RunAtLoad</key>
<true/>
<key>KeepAlive</key>
<true/>
<key>StandardErrorPath</key>
<string>/Users/accusys/momentry/log/php.error.log</string>
<key>StandardOutPath</key>
<string>/Users/accusys/momentry/log/php.log</string>
</dict>
</plist>

View File

@@ -0,0 +1,39 @@
<?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>accusys</string>
<key>EnvironmentVariables</key>
<dict>
<key>LC_ALL</key>
<string>en_US.UTF-8</string>
</dict>
<key>WorkingDirectory</key>
<string>/Users/accusys/momentry/var/postgresql</string>
<key>ProgramArguments</key>
<array>
<string>/opt/homebrew/opt/postgresql@18/bin/postgres</string>
<string>-D</string>
<string>/Users/accusys/momentry/var/postgresql</string>
</array>
<key>RunAtLoad</key>
<true/>
<key>KeepAlive</key>
<true/>
<key>StandardErrorPath</key>
<string>/Users/accusys/momentry/log/postgresql.error.log</string>
<key>StandardOutPath</key>
<string>/Users/accusys/momentry/log/postgresql.log</string>
</dict>
</plist>

View File

@@ -0,0 +1,46 @@
<?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.qdrant</string>
<key>UserName</key>
<string>accusys</string>
<key>WorkingDirectory</key>
<string>/Users/accusys/momentry/var/qdrant/</string>
<key>ProgramArguments</key>
<array>
<string>/Users/accusys/.cargo/bin/qdrant</string>
</array>
<key>EnvironmentVariables</key>
<dict>
<key>QDRANT__STORAGE__STORAGE_PATH</key>
<string>/Users/accusys/momentry/var/qdrant/</string>
<key>QDRANT__SERVICE__HOST</key>
<string>0.0.0.0</string>
<key>QDRANT__SERVICE__HTTP_PORT</key>
<string>6333</string>
<key>HOME</key>
<string>/Users/accusys</string>
<key>QDRANT__SERVICE__API_KEY</key>
<string>Test3200Test3200Test3200</string>
</dict>
<key>RunAtLoad</key>
<true/>
<key>KeepAlive</key>
<true/>
<key>StandardOutPath</key>
<string>/Users/accusys/momentry/log/qdrant.log</string>
<key>StandardErrorPath</key>
<string>/opt/homebrew/var/log/qdrant.error.log</string>
</dict>
</plist>

View File

@@ -0,0 +1,41 @@
<?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.redis</string>
<key>UserName</key>
<string>accusys</string>
<key>WorkingDirectory</key>
<string>/Users/accusys/momentry/var/redis</string>
<key>ProgramArguments</key>
<array>
<string>/opt/homebrew/opt/redis/bin/redis-server</string>
<string>--port</string>
<string>6379</string>
<string>--bind</string>
<string>0.0.0.0</string>
<string>--requirepass</string>
<string>accusys</string>
<string>--dir</string>
<string>/Users/accusys/momentry/var/redis</string>
<string>--logfile</string>
<string>/Users/accusys/momentry/log/redis.log</string>
</array>
<key>RunAtLoad</key>
<true/>
<key>KeepAlive</key>
<true/>
<key>StandardErrorPath</key>
<string>/Users/accusys/momentry/log/redis.error.log</string>
<key>StandardOutPath</key>
<string>/Users/accusys/momentry/log/redis.log</string>
</dict>
</plist>

View File

@@ -0,0 +1,33 @@
<?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.rustdesk.hbbr</string>
<key>UserName</key>
<string>accusys</string>
<key>WorkingDirectory</key>
<string>/Users/accusys/momentry/var/rustdesk</string>
<key>ProgramArguments</key>
<array>
<string>/opt/homebrew/bin/hbbr</string>
<string>-k</string>
<string>_</string>
</array>
<key>RunAtLoad</key>
<true/>
<key>KeepAlive</key>
<true/>
<key>StandardOutPath</key>
<string>/Users/accusys/momentry/log/rustdesk-hbbr.log</string>
<key>StandardErrorPath</key>
<string>/Users/accusys/momentry/log/rustdesk-hbbr.error.log</string>
</dict>
</plist>

View File

@@ -0,0 +1,33 @@
<?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.rustdesk.hbbs</string>
<key>UserName</key>
<string>accusys</string>
<key>WorkingDirectory</key>
<string>/Users/accusys/momentry/var/rustdesk</string>
<key>ProgramArguments</key>
<array>
<string>/opt/homebrew/bin/hbbs</string>
<string>-k</string>
<string>_</string>
</array>
<key>RunAtLoad</key>
<true/>
<key>KeepAlive</key>
<true/>
<key>StandardOutPath</key>
<string>/Users/accusys/momentry/log/rustdesk-hbbs.log</string>
<key>StandardErrorPath</key>
<string>/Users/accusys/momentry/log/rustdesk-hbbs.error.log</string>
</dict>
</plist>

View File

@@ -0,0 +1,42 @@
<?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/opt/sftpgo/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>
</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>

View File

@@ -0,0 +1,39 @@
<?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>/opt/homebrew/bin/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>

59
momentry_runtime/start.sh Executable file
View File

@@ -0,0 +1,59 @@
#!/bin/bash
set -e
# Momentry Runtime Start Script
# This script starts the momentry services
echo "========================================"
echo "Momentry Runtime Starting"
echo "========================================"
# Set environment
export HOME=/Users/accusys
export USER=accusys
export PATH="/opt/homebrew/bin:/opt/homebrew/sbin:/usr/local/bin:/usr/bin:/bin:/Users/accusys/.cargo/bin:$PATH"
# Database configuration
export DATABASE_URL="postgres://accusys@localhost:5432/momentry"
export REDIS_URL="redis://accusys:accusys@localhost:6379"
export OLLAMA_HOST="http://localhost:11434"
# Change to project directory
cd /Users/accusys/momentry_core_0.1
# Check dependencies
echo "Checking dependencies..."
# Check PostgreSQL
if ! pg_isready -h localhost -p 5432 -U accusys > /dev/null 2>&1; then
echo "WARNING: PostgreSQL is not ready"
fi
# Check Redis
if ! redis-cli -a accusys ping > /dev/null 2>&1; then
echo "WARNING: Redis is not ready"
fi
# Check Ollama
if ! curl -s http://localhost:11434/api/tags > /dev/null 2>&1; then
echo "WARNING: Ollama is not ready"
fi
echo "Dependencies checked."
# Start API server in background
echo "Starting API server..."
./target/release/momentry server --host 127.0.0.1 --port 3000 >> momentry_runtime/logs/stdout.log 2>&1 &
API_PID=$!
echo "API server started (PID: $API_PID)"
# Save PID to file
echo $API_PID > momentry_runtime/logs/momentry.pid
echo "========================================"
echo "Momentry Runtime Started"
echo "========================================"
echo "API Server: http://127.0.0.1:3000"
echo "Log file: momentry_runtime/logs/stdout.log"
echo ""
echo "To stop: kill \$(cat momentry_runtime/logs/momentry.pid)"

25
momentry_runtime/stop.sh Executable file
View File

@@ -0,0 +1,25 @@
#!/bin/bash
set -e
# Momentry Runtime Stop Script
echo "Stopping Momentry Runtime..."
PID_FILE="/Users/accusys/momentry_core_0.1/momentry_runtime/logs/momentry.pid"
if [ -f "$PID_FILE" ]; then
PID=$(cat "$PID_FILE")
if kill -0 "$PID" 2>/dev/null; then
echo "Stopping API server (PID: $PID)..."
kill "$PID"
rm -f "$PID_FILE"
echo "Momentry Runtime stopped."
else
echo "Process not running."
rm -f "$PID_FILE"
fi
else
echo "PID file not found. Trying to find and kill momentry processes..."
pkill -f "momentry server" || true
echo "Momentry Runtime stopped."
fi

366
monitor/MONITORING.md Normal file
View File

@@ -0,0 +1,366 @@
# Momentry 監控系統
## 概述
Momentry 監控系統採用七層架構,涵蓋從外部服務到本地存儲的全部監控需求。
---
## 監控架構 (七層)
```
Layer 1: External 監控 - DDNS、網關、互聯網連接
Layer 2: Service 監控 - 15 個 momentry 服務
Layer 3: Workflow 監控 - n8n Workflow 狀態
Layer 4: Portal 監控 - WordPress 頁面與帳號
Layer 5: Database 監控 - PostgreSQL/Redis/Qdrant/MariaDB
Layer 6: 使用者監控 - 連線/本機使用者/異常檢測
Layer 7: Storage 監控 - 冷溫熱分層/歸檔/檔案註冊
```
---
## 快速開始
### 查看監控狀態
```bash
cd /Users/accusys/momentry_core_0.1/monitor
./control/monitor_control.sh status
```
### 執行全面檢查
```bash
./control/monitor_control.sh check all
```
### 查看特定層級
```bash
./control/monitor_control.sh check service # Layer 2
./control/monitor_control.sh check workflow # Layer 3
./control/monitor_control.sh check portal # Layer 4
./control/monitor_control.sh check database # Layer 5
./control/monitor_control.sh check users # Layer 6
./control/monitor_control.sh check storage # Layer 7
```
---
## 各層監控說明
### Layer 1: External 監控
監控外部依賴服務的可用性。
**監控項目**:
- DDNS 域名解析 (momentry.ddns.net)
- 網關連通性
- 外部 API 連接
**腳本**: `service/external_monitor.sh`
### Layer 2: Service 監控
監控 15 個 momentry 服務的運行狀態。
**監控項目**:
| 服務 | Port | 狀態檢查 |
|------|------|----------|
| PostgreSQL | 5432 | pg_isready |
| Redis | 6379 | redis-cli ping |
| MariaDB | 3306 | mysql ping |
| n8n | 5678 | HTTP GET |
| Caddy | 443 | HTTPS |
| Gitea | 3000 | HTTP |
| SFTPGo | 2222 | SSH |
| Ollama | 11434 | API |
| Qdrant | 6333 | API |
| PHP-FPM | - | Process |
| RustDesk | 21116 | Port |
| MongoDB | 27017 | Mongo ping |
**腳本**: `service/health_check.sh`
### Layer 3: n8n Workflow 監控
監控 n8n Workflow 的執行狀態和閒置分析。
**監控項目**:
- Workflow 數量與狀態
- 執行次數與結果
- 閒置 Workflow 識別
- 改善建議生成
**閒置定義**: 無排程 AND 無 API 觸發 AND 超過 30 天未執行
**腳本**:
- `workflow/n8n_workflow_monitor.sh`
- `workflow/idle_analyzer.sh`
### Layer 4: WordPress Portal 監控
監控 WordPress 頁面可訪問性和內部帳號。
**監控項目**:
- 首頁/登入頁可訪問性
- 響應時間
- 用戶列表與角色
- 新增/刪除用戶
**腳本**:
- `portal/page_monitor.sh`
- `portal/account_monitor.sh`
### Layer 5: Database 監控
監控所有資料庫的健康狀態和性能指標。
**PostgreSQL**:
- 表數量、行數、大小
- 死元組、慢查詢
- 表結構變更
**Redis**:
- 連線數、內存使用
- 命中率、操作數
- 客戶端列表
**Qdrant**:
- Collection 列表
- Points 數、向量維度
- 磁盤使用
**MariaDB**:
- 連線數、緩衝池
- WordPress 表結構
**腳本**:
- `database/postgres_monitor.sh`
- `database/redis_monitor.sh`
- `database/qdrant_monitor.sh`
- `database/mariadb_monitor.sh`
### Layer 6: 使用者監控
監控連線使用者和本機使用者的活動。
**連線使用者**:
- SSH 登入與命令
- Web 服務登入 (n8n, Gitea, WP)
- 資料庫連線
- SFTP 傳輸
**本機使用者**:
- 系統登入
- sudo 使用記錄
- 服務帳戶活動
- 異常檢測
**腳本**:
- `users/session_tracker.sh`
- `users/login_monitor.sh`
- `users/sudo_tracker.sh`
- `users/anomaly_detector.sh`
### Layer 7: Storage 架構
管理數據的冷溫熱分層和歸檔策略。
**目錄結構**:
```
/Users/accusys/momentry/
├── var/ # 服務數據 (熱)
├── etc/ # 配置 (溫)
├── log/ # 日誌 (溫)
├── data/ # 用戶數據
│ ├── family/ # 家庭集群
│ ├── work/ # 工作集群
│ ├── wordpress/ # WP 隔離
│ └── shared/ # 共享
├── backup/ # 備份
│ ├── daily/ # 每日備份 (保留 30 天)
│ ├── weekly/ # 每週備份 (保留 12 週)
│ ├── monthly/ # 每月備份 (保留 12 個月)
│ └── archive/ # 歸檔 (保留 12 個月+)
└── tmp/ # 臨時
```
**分層標準**:
| 等級 | 條件 | 存放 |
|------|------|------|
| 熱 | 7天內訪問 > 10次 | NVMe |
| 溫 | 30天內訪問 > 1次 | RAID |
| 冷 | 90天未訪問 | Object Storage |
**備份溫冷分層**:
| 等級 | 保留時間 | 用途 |
|------|---------|------|
| daily | 7天 | 快速恢復 |
| weekly | 30天 | 標準恢復 |
| monthly | 365天 | 長期歸檔 |
| archive | >365天 | 法規遵循 |
**腳本**:
- `storage/storage_manager.sh` - 存儲管理
- `storage/backup_monitor.sh` - 備份監控與溫冷轉移
- `storage/migration_engine.sh` - 數據遷移
- `storage/file_registry.py` - 檔案註冊
---
## 配置
### 主配置
編輯 `config/monitor_config.yaml`:
```yaml
monitoring:
enabled: true
check_interval: 300 # 秒
service:
enabled: true
services:
- postgresql
- redis
- mariadb
- n8n
- caddy
- gitea
- sftpgo
- ollama
- qdrant
- php
- rustdesk
workflow:
enabled: true
idle_threshold_days: 30
portal:
enabled: true
database:
enabled: true
users:
enabled: true
storage:
enabled: false # 獨立實現
```
---
## 數據庫表
監控數據存儲在 PostgreSQL `momentry` 資料庫。
**主要表**:
- `monitor_services` - 服務健康狀態
- `monitor_workflows` - n8n Workflow 監控
- `monitor_databases` - 資料庫指標
- `monitor_sessions` - 使用者會話
- `monitor_logins` - 登入歷史
- `monitor_anomalies` - 異常檢測
- `file_registry` - 檔案註冊
**創建表**: `database/schema.sql`
---
## 報警規則
| 層級 | 異常類型 | 等級 | 處理 |
|------|----------|------|------|
| Service | 服務宕機 | Critical | 記錄 |
| Service | 響應過慢 | Warning | 記錄 |
| Workflow | 閒置 > 30天 | Info | 記錄 |
| Workflow | 連續失敗 | Critical | 記錄 |
| Portal | 頁面不可訪問 | Critical | 記錄 |
| Database | 表結構變更 | Critical | 記錄 |
| Database | 連線過載 | Warning | 記錄 |
| Users | 暴力破解 | Critical | 記錄 |
| Users | 異常登入 | Warning | 記錄 |
**異常處理**: 僅記錄到資料庫,後續分析
---
## 維護
### 手動執行監控
```bash
# 單次檢查
./control/monitor_control.sh check service
# 持續監控 (每 5 分鐘)
./control/monitor_control.sh monitor
```
### 查看歷史
```bash
# 查看服務狀態
psql -U accusys -h localhost -d momentry -c "SELECT * FROM monitor_services ORDER BY checked_at DESC LIMIT 10;"
# 查看異常
psql -U accusys -h localhost -d momentry -c "SELECT * FROM monitor_anomalies WHERE detected_at > NOW() - INTERVAL '24 hours';"
```
### 清理歷史數據
```bash
# 保留 30 天
psql -U accusys -h localhost -d momentry -c "DELETE FROM monitor_services WHERE checked_at < NOW() - INTERVAL '30 days';"
```
---
## 文件結構
```
monitor/
├── MONITORING.md # 本文件
├── config/
│ └── monitor_config.yaml # 配置文件
├── control/
│ └── monitor_control.sh # 控制腳本
├── service/
│ ├── health_check.sh # 服務健康檢查
│ └── external_monitor.sh # 外部監控
├── workflow/
│ ├── n8n_workflow_monitor.sh
│ └── idle_analyzer.sh
├── portal/
│ ├── page_monitor.sh
│ └── account_monitor.sh
├── database/
│ ├── schema.sql # 數據庫表
│ ├── postgres_monitor.sh
│ ├── redis_monitor.sh
│ ├── qdrant_monitor.sh
│ └── mariadb_monitor.sh
├── users/
│ ├── session_tracker.sh
│ ├── login_monitor.sh
│ ├── sudo_tracker.sh
│ └── anomaly_detector.sh
├── storage/
│ ├── storage_manager.sh
│ └── migration_engine.sh
└── docs/
└── TROUBLESHOOTING.md
```
---
## 相關文檔
- [Storage 架構設計規範](./storage/STORAGE_SPEC.md)
- [WordPress 監控](./wordpress/MONITORING.md)
- [異常檢測規則](./users/ANOMALY_RULES.md)

View File

@@ -0,0 +1,388 @@
# Momentry 服務故障排除 Skill
## 概述
此 Skill 提供常見服務問題的快速診斷參考,包含問題類型、檢查命令、日誌位置和常見解決方案。
---
## 快速檢查清單
### 1. 服務狀態檢查
```bash
# 查看所有 momentry 服務狀態
launchctl list | grep com.momentry
# 或執行健康檢查
/Users/accusys/momentry_core_0.1/monitor/service/health_check.sh
```
### 2. 端口佔用檢查
```bash
# 檢查特定端口
lsof -i :<PORT>
# 常用端口對照
# PostgreSQL: 5432
# Redis: 6379
# MariaDB: 3306
# n8n: 8085 (內部), 5678 (舊配置)
# Caddy Admin: 2019
# Ollama: 11434
# Qdrant: 6333
# SFTPGo: 8080 (HTTP), 2022 (SFTP)
# Gitea: 3000
# PHP-FPM: 9000
# RustDesk: 21115-21119
# MongoDB: 27017
```
---
## 服務詳細診斷
### PostgreSQL
**配置文件**: `/Users/accusys/momentry/etc/postgresql/`
**數據目錄**: `/Users/accusys/momentry/var/postgresql/`
**日誌**: `/Users/accusys/momentry/log/postgresql.log`
| 問題 | 檢查命令 | 解決方案 |
|------|----------|----------|
| 連線失敗 | `pg_isready -h localhost -p 5432 -U accusys` | 檢查 plist 是否載入 |
| 認證錯誤 | `psql -U accusys -h localhost -d momentry -c "SELECT 1"` | 檢查 pg_hba.conf |
| 效能問題 | `psql -U accusys -h localhost -d momentry -c "SELECT * FROM pg_stat_activity"` | 檢查連線數 |
| 資料庫不存在 | `psql -U accusys -h localhost -l` | 創建數據庫 |
```bash
# 創建數據庫
createdb -U accusys momentry
```
---
### Redis
**配置文件**: `/opt/homebrew/etc/redis.conf`
**數據目錄**: `/Users/accusys/momentry/var/redis/`
**日誌**: `/Users/accusys/momentry/log/redis.log`
| 問題 | 檢查命令 | 解決方案 |
|------|----------|----------|
| 連線失敗 | `redis-cli -a accusys ping` | 檢查密碼是否正確 |
| 認證錯誤 | `redis-cli -a accusys AUTH accusys` | 檢查密碼配置 |
| 記憶體過高 | `redis-cli -a accusys INFO memory` | 檢查 keys 數量 |
| 持久化失敗 | `redis-cli -a accusys LASTSAVE` | 檢查 RDB 配置 |
```bash
# 常用操作
redis-cli -a accusys SAVE # 觸發保存
redis-cli -a accusys FLUSHALL # 清空所有 keys
redis-cli -a accusys KEYS '*' # 查看所有 keys
```
---
### MariaDB
**配置文件**: `/Users/accusys/momentry/etc/mariadb/`
**數據目錄**: `/Users/accusys/momentry/var/mariadb/`
**日誌**: `/Users/accusys/momentry/log/mariadb.log`
| 問題 | 檢查命令 | 解決方案 |
|------|----------|----------|
| 連線失敗 | `mysql -u accusys -e "SELECT 1"` | 檢查用戶權限 |
| 拒絕訪問 | `mysql -u root -e "SELECT user FROM mysql.user"` | 檢查用戶配置 |
| 效能問題 | `mysql -u accusys -e "SHOW PROCESSLIST"` | 檢查慢查詢 |
```bash
# 常用操作
mysql -u accusys -e "SHOW DATABASES"
mysql -u accusys -e "SHOW TABLES" momentry
```
---
### n8n
**配置文件**: `/Users/accusys/momentry/etc/n8n/`
**數據目錄**: `/Users/accusys/momentry/var/n8n/`
**日誌**: `/Users/accusys/momentry/log/n8n-main.log`
| 問題 | 檢查命令 | 解決方案 |
|------|----------|----------|
| 網頁無法訪問 | `curl -s http://localhost:8085/` | 檢查端口是否正確 |
| API 錯誤 | `curl -s http://localhost:8085/healthz` | 檢查服務狀態 |
| Workflow 不執行 | 檢查 n8n log | 檢查 queue (Redis) 連線 |
| 資料庫連線失敗 | `psql -U n8n -h localhost -d n8n -c "SELECT 1"` | 檢查 PostgreSQL |
**重要**: n8n 使用 PostgreSQL (非 SQLite),端口為 8085
```bash
# 數據庫連線
psql -U n8n -h localhost -d n8n
# 查看 users
psql -U n8n -h localhost -d n8n -c "SELECT id, email, \"firstName\", \"roleSlug\" FROM \"user\";"
# 查看 workflows
psql -U n8n -h localhost -d n8n -c "SELECT id, name, active FROM workflow_entity;"
```
---
### Ollama
**配置文件**: `/Users/accusys/momentry/etc/ollama/`
**模型目錄**: `/Users/accusys/momentry/var/ollama/`
**日誌**: `/Users/accusys/momentry/log/ollama.log`
| 問題 | 檢查命令 | 解決方案 |
|------|----------|----------|
| API 無回應 | `curl -s http://localhost:11434/api/tags` | 檢查服務是否啟動 |
| 模型下載失敗 | `ollama list` | 重新下載模型 |
| 記憶體不足 | `ollama list` | 檢查已加載模型 |
```bash
# 常用操作
ollama list # 列出模型
ollama pull <model> # 下載模型
ollama run <model> <prompt> # 運行模型
```
---
### Qdrant
**配置文件**: `/Users/accusys/momentry/etc/qdrant/`
**數據目錄**: `/Users/accusys/momentry/var/qdrant/`
**日誌**: `/Users/accusys/momentry/log/qdrant.log`
| 問題 | 檢查命令 | 解決方案 |
|------|----------|----------|
| API 無回應 | `curl -s http://localhost:6333/collections` | 需要 API Key |
| 認證失敗 | `curl -s -H "api-key: Test3200Test3200" http://localhost:6333/collections` | 檢查 API Key |
| 效能問題 | `curl -s http://localhost:6333/cluster` | 檢查叢集狀態 |
```bash
# 需要認證的指令
curl -s -H "api-key: Test3200Test3200" http://localhost:6333/collections
curl -s -H "api-key: Test3200Test3200" http://localhost:6333/points/count
```
---
### Caddy
**配置文件**: `/Users/accusys/momentry/etc/caddy/`
**數據目錄**: `/Users/accusys/momentry/var/caddy/`
**日誌**: `/Users/accusys/momentry/log/caddy.log`
| 問題 | 檢查命令 | 解決方案 |
|------|----------|----------|
| Admin API 無回應 | `curl -s http://localhost:2019/config/` | 檢查 Caddy 進程 |
| 網站無法訪問 | `curl -s -I https://localhost:443/` | 檢查證書配置 |
| 代理失敗 | `curl -s http://localhost:2019/config/` | 檢查反向代理配置 |
```bash
# 重新載入配置
sudo launchctl unload /Library/LaunchDaemons/com.momentry.caddy.plist
sudo launchctl load /Library/LaunchDaemons/com.momentry.caddy.plist
```
---
### Gitea
**配置文件**: `/Users/accusys/momentry/etc/gitea/app.ini`
**數據目錄**: `/Users/accusys/momentry/var/gitea/`
**日誌**: `/Users/accusys/momentry/log/gitea.log`
| 問題 | 檢查命令 | 解決方案 |
|------|----------|----------|
| 網頁無法訪問 | `curl -s http://localhost:3000/` | 檢查服務狀態 |
| Git 推送失敗 | `ssh git@localhost -p 22` | 檢查 SSH 配置 |
| 資料庫連線 | `mysql -u gitea -p gitea_db -e "SELECT 1"` | 檢查 PostgreSQL |
---
### SFTPGo
**配置文件**: `/Users/accusys/momentry/etc/sftpgo/sftpgo.json`
**數據目錄**: `/Users/accusys/momentry/var/sftpgo/`
**日誌**: `/Users/accusys/momentry/log/sftpgo.log`
| 問題 | 檢查命令 | 解決方案 |
|------|----------|----------|
| Web UI 無法訪問 | `curl -s http://localhost:8080/` | 檢查 HTTP 端口 |
| SFTP 連線失敗 | `sftp -P 2022 user@localhost` | 檢查 SSH 密鑰 |
| 認證失敗 | `curl -s http://localhost:8080/api/v2/info` | 檢查用戶配置 |
---
### PHP-FPM
**配置文件**: `/Users/accusys/momentry/etc/php/`
**日誌**: `/Users/accusys/momentry/log/php.log`
| 問題 | 檢查命令 | 解決方案 |
|------|----------|----------|
| PHP 無法執行 | `php -v` | 檢查 PHP 版本 |
| FPM 無回應 | `curl -s http://localhost:9000/` | 檢查 FPM 端口 |
| 500 錯誤 | `tail -100 /Users/accusys/momentry/log/php.error.log` | 檢查錯誤日誌 |
---
### RustDesk
**配置文件**: `/Users/accusys/momentry/etc/rustdesk/`
**日誌**: `/Users/accusys/momentry/log/rustdesk.*.log`
| 問題 | 檢查命令 | 解決方案 |
|------|----------|----------|
| 連線失敗 | `nc -z localhost 21116 && nc -z localhost 21117` | 檢查端口 |
| ID 伺服器錯誤 | `pgrep -a hbbs` | 檢查 hbbs 進程 |
| 中繼伺服器錯誤 | `pgrep -a hbbr` | 檢查 hbbr 進程 |
---
### MongoDB
**配置文件**: `/Users/accusys/momentry/etc/mongodb/`
**數據目錄**: `/Users/accusys/momentry/var/mongodb/`
**日誌**: `/Users/accusys/momentry/log/mongodb.log`
| 問題 | 檢查命令 | 解決方案 |
|------|----------|----------|
| 連線失敗 | `mongosh --quiet --eval "db.adminCommand('ping')"` | 需要認證 |
| 認證錯誤 | `mongosh -u accusys -p Test3200Test3200 --authenticationDatabase admin` | 檢查用戶 |
| 效能問題 | `mongosh -u accusys -p Test3200Test3200 --eval "db.serverStatus()"` | 檢查狀態 |
---
## 服務管理命令
### 啟動服務
```bash
sudo launchctl load /Library/LaunchDaemons/com.momentry.<service>.plist
```
### 停止服務
```bash
sudo launchctl unload /Library/LaunchDaemons/com.momentry.<service>.plist
```
### 重啟服務
```bash
sudo launchctl unload /Library/LaunchDaemons/com.momentry.<service>.plist
sudo launchctl load /Library/LaunchDaemons/com.momentry.<service>.plist
```
### 查看服務日誌
```bash
tail -f /Users/accusys/momentry/log/<service>.log
tail -f /Users/accusys/momentry/log/<service>.error.log
```
---
## 服務端口對照表
| 服務 | 內部端口 | 外部端口 | Caddy 域名 |
|------|---------|---------|------------|
| PostgreSQL | 5432 | 5432 | - |
| Redis | 6379 | 6379 | - |
| MariaDB | 3306 | 3306 | - |
| n8n | 8085 | 443 | n8n.momentry.ddns.net |
| Ollama | 11434 | 11434 | - |
| Qdrant | 6333 | 443 | qdrant.momentry.ddns.net |
| Caddy Admin | 2019 | 2019 | - |
| Gitea | 3000 | 443 | gitea.momentry.ddns.net |
| SFTPGo | 8080/2022 | 443 | sftpgo.momentry.ddns.net |
| PHP-FPM | 9000 | - | - |
| MongoDB | 27017 | 27017 | - |
| RustDesk | 21115-21119 | 21115-21119 | - |
---
## 常見問題快速修復
### 1. 服務無法啟動
```bash
# 1. 檢查進程
pgrep -a <service_name>
# 2. 檢查端口
lsof -i :<PORT>
# 3. 檢查日誌
tail -100 /Users/accusys/momentry/log/<service>.error.log
# 4. 重新載入
sudo launchctl unload /Library/LaunchDaemons/com.momentry.<service>.plist
sudo launchctl load /Library/LaunchDaemons/com.momentry.<service>.plist
```
### 2. 認證失敗
```bash
# 檢查用戶是否存在
psql -U <db_user> -h localhost -d <db_name> -c "SELECT 1"
# 檢查密碼
# 參考各服務的 INSTALL_*.md 文檔
```
### 3. 效能問題
```bash
# 檢查系統資源
top -o cpu
top -o mem
# 檢查磁盤空間
df -h
# 檢查網絡連線
netstat -an | grep ESTABLISHED
```
---
## 監控腳本位置
- 健康檢查: `/Users/accusys/momentry_core_0.1/monitor/service/health_check.sh`
- PostgreSQL: `/Users/accusys/momentry_core_0.1/monitor/database/postgres_monitor.sh`
- Redis: `/Users/accusys/momentry_core_0.1/monitor/database/redis_monitor.sh`
- Qdrant: `/Users/accusys/momentry_core_0.1/monitor/database/qdrant_monitor.sh`
- MongoDB: `/Users/accusys/momentry_core_0.1/monitor/database/mongodb_monitor.sh`
- n8n Workflow: `/Users/accusys/momentry_core_0.1/monitor/workflow/n8n_workflow_monitor.sh`
---
## 密碼參考 (請修改為真實密碼)
| 服務 | 用戶 | 密碼 |
|------|------|------|
| PostgreSQL | accusys | (无密码) |
| PostgreSQL | n8n | accusys |
| Redis | - | accusys |
| MariaDB | accusys | - |
| n8n | - | (Web UI 配置) |
| Qdrant | - | Test3200Test3200 |
| MongoDB | accusys | Test3200Test3200 |
---
## 文檔位置
- 安裝文檔: `/Users/accusys/momentry_core_0.1/docs/INSTALL_*.md`
- 服務管理: `/Users/accusys/momentry_core_0.1/docs/SERVICE_ADDITION_GUIDE.md`
- Plist 模板: `/Users/accusys/momentry_core_0.1/momentry_runtime/plist/`

View File

@@ -0,0 +1,503 @@
# Momentry 監控系統配置文件
# 路徑: /Users/accusys/momentry_core_0.1/monitor/config/monitor_config.yaml
monitoring:
enabled: true
check_interval: 300 # 秒 (5分鐘)
# 數據庫連接
database:
host: "localhost"
port: 5432
username: "accusys"
password: "" # 使用 psql 連接
name: "momentry"
# ============================================================
# Layer 1: External 監控
# ============================================================
external:
enabled: true
check_interval: 60 # 秒
targets:
- name: "ddns"
type: "ddns"
host: "momentry.ddns.net"
enabled: true
- name: "gateway"
type: "gateway"
host: "192.168.110.1"
enabled: true
- name: "internet"
type: "internet"
host: "8.8.8.8"
enabled: true
# ============================================================
# Layer 2: Service 監控
# ============================================================
service:
enabled: true
check_interval: 300 # 秒
services:
- name: "postgresql"
type: "postgres"
port: 5432
host: "localhost"
check_cmd: "pg_isready -h localhost -p 5432 -U accusys"
timeout: 5
enabled: true
- name: "redis"
type: "redis"
port: 6379
host: "localhost"
password: "accusys"
check_cmd: "redis-cli -a accusys ping"
timeout: 5
enabled: true
- name: "mariadb"
type: "mariadb"
port: 3306
host: "localhost"
check_cmd: "mysql -u root -e 'SELECT 1'"
timeout: 5
enabled: true
- name: "n8n"
type: "http"
port: 5678
host: "localhost"
check_url: "http://localhost:5678/"
timeout: 10
enabled: true
- name: "caddy"
type: "http"
port: 2019
host: "localhost"
check_url: "http://localhost:2019/config/"
timeout: 10
enabled: true
- name: "gitea"
type: "http"
port: 3000
host: "localhost"
check_url: "http://localhost:3000/"
timeout: 10
enabled: true
- name: "sftpgo"
type: "http"
port: 8080
host: "localhost"
timeout: 10
enabled: true
- name: "ollama"
type: "http"
port: 11434
host: "localhost"
check_url: "http://localhost:11434/api/tags"
timeout: 10
enabled: true
- name: "qdrant"
type: "http"
port: 6333
host: "localhost"
check_url: "http://localhost:6333/collections"
timeout: 10
enabled: true
- name: "mongodb"
type: "mongodb"
port: 27017
host: "localhost"
timeout: 10
enabled: true
- name: "php"
type: "process"
process_name: "php-fpm"
enabled: true
- name: "node"
type: "process"
process_name: "node"
enabled: true
check_interval: 60
version_lock: "22.x"
locked_processes:
- "n8n"
description: "Node.js 運行環境 (n8n 專用)"
- 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"
description: "Python 運行環境 (Momentry 腳本專用)"
- name: "rustdesk_hbbs"
type: "process"
process_name: "hbbs"
port: 21116
enabled: true
- name: "rustdesk_hbbr"
type: "process"
process_name: "hbbr"
port: 21117
enabled: true
# ============================================================
# Layer 3: n8n Workflow 監控
# ============================================================
workflow:
enabled: true
check_interval: 300 # 秒
n8n:
host: "http://localhost:5678"
api_key: "" # 從環境變數或 n8n 獲取
idle_threshold_days: 30
suggestions:
- type: "disable_idle"
threshold_days: 30
- type: "delete_unused"
threshold_days: 90
- type: "optimize_failures"
failure_rate_threshold: 0.2
# ============================================================
# Layer 4: WordPress Portal 監控
# ============================================================
portal:
enabled: true
check_interval: 300 # 秒
wordpress:
site_url: "https://wp.momentry.ddns.net"
db_host: "localhost"
db_name: "wordpress"
db_user: "wp_user"
db_password: "wp_password_123"
page_monitoring:
enabled: true
pages:
- url: "/"
name: "homepage"
- url: "/wp-login.php"
name: "login_page"
response_time_threshold_ms: 3000
account_monitoring:
enabled: true
check_interval: 3600 # 小時
alert_on_new_admin: true
# ============================================================
# Layer 5: Database 監控
# ============================================================
database:
enabled: true
check_interval: 300 # 秒
postgres:
enabled: true
databases:
- name: "momentry"
- name: "gitea"
- name: "n8n"
- name: "video_register"
schema_monitoring: true
redis:
enabled: true
password: "accusys"
alert_thresholds:
memory_percent: 80
connected_clients: 100
qdrant:
enabled: true
collections_watch: ["*"] # 監控所有
mariadb:
enabled: true
databases:
- name: "wordpress"
mongodb:
enabled: true
databases:
- name: "momentry"
- name: "admin"
# ============================================================
# Layer 6: 使用者監控
# ============================================================
users:
enabled: true
check_interval: 60 # 秒
session_tracking:
enabled: true
track_ssh: true
track_web: true
track_db: true
track_sftp: true
login_monitoring:
enabled: true
track_system: true
track_wordpress: true
track_n8n: true
track_gitea: true
sudo_tracking:
enabled: true
anomaly_detection:
enabled: true
rules:
- type: "brute_force"
threshold: 5
window_seconds: 60
severity: "critical"
- type: "unusual_time"
severity: "medium"
allowed_hours: "08:00-22:00"
- type: "idle_session"
threshold_hours: 24
severity: "low"
# ============================================================
# Layer 7: Storage 監控 (獨立配置)
# ============================================================
storage:
enabled: false # 獨立實現
paths:
hot: "/Users/accusys/momentry/data"
warm: "/Volumes/RAID System/momentry/warm"
cold: "/Volumes/Object Storage/momentry/archive"
temp: "/Users/accusys/momentry/tmp"
backup: "/Users/accusys/momentry/backup"
clusters:
- name: "family"
path: "data/family"
quota: "1TB"
- name: "work"
path: "data/work"
quota: "2TB"
- name: "wordpress"
path: "data/wordpress"
quota: "500GB"
- name: "shared"
path: "data/shared"
quota: "1TB"
migration:
hot_to_warm_days: 7
warm_to_cold_days: 90
# ============================================================
# Layer 7: 備份監控
# ============================================================
backup:
enabled: true
check_interval: 3600 # 秒 (每小時檢查一次)
# 備份根目錄
backup_root: "/Users/accusys/momentry/backup"
# 服務列表
services:
- name: "postgresql"
enabled: true
backup_type: "database"
method: "pg_dump"
schedule: "daily"
retention:
daily: 7
weekly: 4
monthly: 12
- name: "redis"
enabled: true
backup_type: "database"
method: "rdb"
schedule: "daily"
retention:
daily: 7
weekly: 4
- name: "mariadb"
enabled: true
backup_type: "database"
method: "mysqldump"
schedule: "daily"
retention:
daily: 7
weekly: 4
- name: "n8n"
enabled: true
backup_type: "full"
method: "tar"
schedule: "daily"
retention:
daily: 7
weekly: 4
- name: "qdrant"
enabled: true
backup_type: "database"
method: "snapshot"
schedule: "daily"
retention:
daily: 7
weekly: 4
- name: "gitea"
enabled: true
backup_type: "full"
method: "gitea_dump"
schedule: "weekly"
retention:
weekly: 4
monthly: 12
- name: "mongodb"
enabled: true
backup_type: "database"
method: "mongodump"
schedule: "daily"
retention:
daily: 7
weekly: 4
- name: "ollama"
enabled: true
backup_type: "config"
method: "tar"
schedule: "weekly"
retention:
weekly: 4
monthly: 12
- name: "caddy"
enabled: true
backup_type: "config"
method: "file"
schedule: "weekly"
retention:
weekly: 4
- name: "sftpgo"
enabled: true
backup_type: "config"
method: "file"
schedule: "weekly"
retention:
weekly: 4
- name: "mongodb"
enabled: true
backup_type: "config"
method: "file"
schedule: "weekly"
retention:
weekly: 4
- name: "php"
enabled: true
backup_type: "config"
method: "file"
schedule: "weekly"
retention:
weekly: 4
# 溫冷轉移配置
tiering:
enabled: true
tiering_interval: 86400 # 秒 (每天)
rules:
- from: "daily"
to: "weekly"
after_days: 7
- from: "weekly"
to: "monthly"
after_days: 30
- from: "monthly"
to: "archive"
after_days: 90
# 存儲閾值
thresholds:
backup_age_warning_days: 7
backup_age_critical_days: 14
storage_percent_warning: 80
storage_percent_critical: 90
# 驗證
verify:
enabled: true
verify_on_completion: true
test_restore: false # 僅測試還原,不實際執行
# ============================================================
# 通知配置
# ============================================================
notifications:
enabled: true
log_only: true # 僅記錄,不發送
# 日誌記錄
log:
enabled: true
path: "/Users/accusys/momentry/log/monitor"
# n8n webhook (預設不啟用)
n8n:
enabled: false
webhook_url: "http://localhost:5678/webhook/monitor-alert"
# Telegram (預設不啟用)
telegram:
enabled: false
bot_token: ""
chat_id: ""
# Email (預設不啟用)
email:
enabled: false
smtp_host: ""
smtp_port: 587
smtp_user: ""
smtp_password: ""
from_address: ""
to_addresses: []
# ============================================================
# 數據保留
# ============================================================
retention:
history_days: 30
anomaly_days: 90
session_days: 7
login_days: 30
# ============================================================
# 報警閾值
# ============================================================
thresholds:
service_response_time_ms: 3000
database_memory_percent: 80
disk_percent: 90
cpu_percent: 90
login_failures_per_user: 3
brute_force_per_minute: 5

View File

@@ -0,0 +1,362 @@
#!/bin/bash
# Momentry 監控系統控制腳本
# 路徑: /Users/accusys/momentry_core_0.1/monitor/control/monitor_control.sh
set -e
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
MONITOR_DIR="$(dirname "$SCRIPT_DIR")"
CONFIG_DIR="$MONITOR_DIR/config"
LOG_DIR="/Users/accusys/momentry/log/monitor"
# 顏色定義
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m'
# ============================================================
# 幫助信息
# ============================================================
show_help() {
cat << EOF
Momentry 監控系統控制腳本
用法: $0 <command> [options]
命令:
status 查看監控狀態
check <layer> 執行特定層級檢查
layers: service, workflow, portal, database, users, storage, external, backup, node, python, all
monitor 持續監控 (每 5 分鐘)
init 初始化監控數據庫表
logs <layer> [lines] 查看日誌
clean 清理歷史數據
help 顯示幫助
示例:
$0 status 查看所有監控狀態
$0 check service 檢查服務狀態
$0 check backup 檢查備份狀態
$0 check all 執行全面檢查
$0 logs anomaly 50 查看最近 50 條異常
$0 init 初始化數據庫表
EOF
}
# ============================================================
# 初始化
# ============================================================
init_monitor() {
echo -e "${BLUE}初始化監控系統...${NC}"
# 創建日誌目錄
mkdir -p "$LOG_DIR"
# 創建數據庫表
echo "創建監控數據庫表..."
psql -U accusys -h localhost -d momentry -f "$MONITOR_DIR/database/schema.sql" 2>/dev/null || \
echo "數據庫表可能已存在"
echo -e "${GREEN}初始化完成${NC}"
}
# ============================================================
# 狀態查看
# ============================================================
show_status() {
echo ""
echo "========================================"
echo -e "${BLUE}Momentry 監控系統狀態${NC}"
echo "========================================"
echo ""
# 服務狀態
echo -e "${YELLOW}Layer 2: 服務狀態${NC}"
local service_count=$(psql -U accusys -h localhost -d momentry -t -A -c "SELECT COUNT(*) FROM monitor_services WHERE checked_at > NOW() - INTERVAL '5 minutes' AND status = 'up';" 2>/dev/null || echo "0")
local service_total=$(psql -U accusys -h localhost -d momentry -t -A -c "SELECT COUNT(DISTINCT service_name) FROM monitor_services;" 2>/dev/null || echo "0")
echo " 服務: $service_count / $service_total 正常"
echo ""
# Workflow 狀態
echo -e "${YELLOW}Layer 3: Workflow 狀態${NC}"
local active_wf=$(psql -U accusys -h localhost -d momentry -t -A -c "SELECT COUNT(*) FROM monitor_workflows WHERE is_active = true;" 2>/dev/null || echo "0")
local idle_wf=$(psql -U accusys -h localhost -d momentry -t -A -c "SELECT COUNT(*) FROM monitor_workflows WHERE idle_days > 30;" 2>/dev/null || echo "0")
echo " 啟用 Workflow: $active_wf"
echo " 閒置 Workflow (>30天): $idle_wf"
echo ""
# Database 狀態
echo -e "${YELLOW}Layer 5: Database 狀態${NC}"
local db_count=$(psql -U accusys -h localhost -d momentry -t -A -c "SELECT COUNT(DISTINCT db_type) FROM monitor_databases WHERE checked_at > NOW() - INTERVAL '5 minutes';" 2>/dev/null || echo "0")
echo " 監控資料庫: $db_count"
echo ""
# 異常狀態
echo -e "${YELLOW}Layer 6: 異常狀態${NC}"
local critical=$(psql -U accusys -h localhost -d momentry -t -A -c "SELECT COUNT(*) FROM monitor_anomalies WHERE severity = 'critical' AND detected_at > NOW() - INTERVAL '24 hours';" 2>/dev/null || echo "0")
local warning=$(psql -U accusys -h localhost -d momentry -t -A -c "SELECT COUNT(*) FROM monitor_anomalies WHERE severity IN ('medium', 'high') AND detected_at > NOW() - INTERVAL '24 hours';" 2>/dev/null || echo "0")
echo " Critical: $critical"
echo " Warning: $warning"
echo ""
# Node.js 狀態
echo -e "${YELLOW}Node.js 運行環境${NC}"
local node_compliant=$(psql -U accusys -h localhost -d momentry -t -A -c "SELECT COUNT(*) FROM node_version_baseline WHERE is_compliant = true;" 2>/dev/null || echo "0")
local node_total=$(psql -U accusys -h localhost -d momentry -t -A -c "SELECT COUNT(*) FROM node_version_baseline;" 2>/dev/null || echo "0")
echo " 版本合規: $node_compliant / $node_total"
echo ""
# Python 狀態
echo -e "${YELLOW}Python 運行環境${NC}"
local python_compliant=$(psql -U accusys -h localhost -d momentry -t -A -c "SELECT COUNT(*) FROM python_version_baseline WHERE is_compliant = true;" 2>/dev/null || echo "0")
local python_total=$(psql -U accusys -h localhost -d momentry -t -A -c "SELECT COUNT(*) FROM python_version_baseline;" 2>/dev/null || echo "0")
echo " 版本合規: $python_compliant / $python_total"
echo ""
# 備份狀態
echo -e "${YELLOW}Layer 7: 備份狀態${NC}"
local total_backups=$(psql -U accusys -h localhost -d momentry -t -A -c "SELECT COUNT(*) FROM backup_registry WHERE created_at > NOW() - INTERVAL '7 days';" 2>/dev/null || echo "0")
local failed_backups=$(psql -U accusys -h localhost -d momentry -t -A -c "SELECT COUNT(*) FROM backup_registry WHERE status = 'failed' AND created_at > NOW() - INTERVAL '7 days';" 2>/dev/null || echo "0")
echo " 本週備份: $total_backups"
echo " 失敗: $failed_backups"
echo ""
echo "========================================"
echo "使用 '$0 check <layer>' 執行檢查"
echo ""
}
# ============================================================
# 執行檢查
# ============================================================
check_layer() {
local layer=$1
case $layer in
service)
echo -e "${BLUE}執行 Layer 2: 服務監控...${NC}"
bash "$MONITOR_DIR/service/health_check.sh"
;;
workflow)
echo -e "${BLUE}執行 Layer 3: Workflow 監控...${NC}"
bash "$MONITOR_DIR/workflow/n8n_workflow_monitor.sh"
;;
portal)
echo -e "${BLUE}執行 Layer 4: Portal 監控...${NC}"
bash "$MONITOR_DIR/portal/page_monitor.sh"
;;
database)
echo -e "${BLUE}執行 Layer 5: Database 監控...${NC}"
bash "$MONITOR_DIR/database/postgres_monitor.sh"
bash "$MONITOR_DIR/database/redis_monitor.sh"
bash "$MONITOR_DIR/database/qdrant_monitor.sh"
;;
users)
echo -e "${BLUE}執行 Layer 6: 使用者監控...${NC}"
bash "$MONITOR_DIR/users/session_tracker.sh"
;;
storage)
echo -e "${BLUE}執行 Layer 7: Storage 監控...${NC}"
bash "$MONITOR_DIR/storage/storage_manager.sh" status
;;
backup)
echo -e "${BLUE}執行 Layer 7: 備份監控...${NC}"
bash "$MONITOR_DIR/storage/backup_monitor.sh" status
;;
external)
echo -e "${BLUE}執行 Layer 1: External 監控...${NC}"
bash "$MONITOR_DIR/service/external_monitor.sh"
;;
node)
echo -e "${BLUE}執行 Node.js 版本監控...${NC}"
bash "$MONITOR_DIR/service/node_monitor.sh"
;;
python)
echo -e "${BLUE}執行 Python 版本監控...${NC}"
bash "$MONITOR_DIR/service/python_monitor.sh"
;;
all)
echo -e "${BLUE}執行全面監控檢查...${NC}"
check_layer external
check_layer service
check_layer node
check_layer python
check_layer workflow
check_layer portal
check_layer database
check_layer users
echo -e "${GREEN}全面檢查完成${NC}"
;;
*)
echo -e "${RED}未知層級: $layer${NC}"
show_help
exit 1
;;
esac
}
# ============================================================
# 持續監控
# ============================================================
run_monitor() {
echo -e "${BLUE}開始持續監控 (每 5 分鐘)${NC}"
echo "按 Ctrl+C 停止"
echo ""
while true; do
local start_time=$(date +%s)
check_layer all
local end_time=$(date +%s)
local elapsed=$((end_time - start_time))
if [ $elapsed -lt 300 ]; then
sleep $((300 - elapsed))
fi
done
}
# ============================================================
# 查看日誌
# ============================================================
show_logs() {
local layer=${1:-anomaly}
local lines=${2:-20}
case $layer in
anomaly)
echo -e "${BLUE}最近異常記錄:${NC}"
psql -U accusys -h localhost -d momentry -c "
SELECT
TO_CHAR(detected_at, 'YYYY-MM-DD HH24:MI:SS') as time,
severity,
anomaly_type,
username,
LEFT(description, 50) as desc
FROM monitor_anomalies
ORDER BY detected_at DESC
LIMIT $lines;
" 2>/dev/null || echo "無法連接資料庫"
;;
service)
echo -e "${BLUE}最近服務狀態:${NC}"
psql -U accusys -h localhost -d momentry -c "
SELECT
service_name,
status,
response_time_ms,
TO_CHAR(checked_at, 'YYYY-MM-DD HH24:MI:SS') as time
FROM monitor_services
ORDER BY checked_at DESC
LIMIT $lines;
" 2>/dev/null || echo "無法連接資料庫"
;;
workflow)
echo -e "${BLUE}最近 Workflow 狀態:${NC}"
psql -U accusys -h localhost -d momentry -c "
SELECT
workflow_name,
is_active,
idle_days,
suggestion,
TO_CHAR(checked_at, 'YYYY-MM-DD HH24:MI:SS') as time
FROM monitor_workflows
ORDER BY checked_at DESC
LIMIT $lines;
" 2>/dev/null || echo "無法連接資料庫"
;;
*)
echo -e "${RED}未知日誌類型: $layer${NC}"
;;
esac
}
# ============================================================
# 清理歷史數據
# ============================================================
clean_history() {
echo -e "${YELLOW}清理歷史數據...${NC}"
psql -U accusys -h localhost -d momentry << EOF 2>/dev/null || true
-- 保留 30 天
DELETE FROM monitor_services WHERE checked_at < NOW() - INTERVAL '30 days';
DELETE FROM monitor_workflows WHERE checked_at < NOW() - INTERVAL '30 days';
DELETE FROM monitor_databases WHERE checked_at < NOW() - INTERVAL '30 days';
DELETE FROM monitor_external WHERE checked_at < NOW() - INTERVAL '30 days';
DELETE FROM monitor_portal_pages WHERE checked_at < NOW() - INTERVAL '30 days';
-- 保留 30 天版本基線
DELETE FROM node_version_baseline WHERE checked_at < NOW() - INTERVAL '30 days';
DELETE FROM node_process_tracking WHERE checked_at < NOW() - INTERVAL '30 days';
DELETE FROM python_version_baseline WHERE checked_at < NOW() - INTERVAL '30 days';
DELETE FROM python_script_tracking WHERE checked_at < NOW() - INTERVAL '30 days';
-- 保留 7 天會話
DELETE FROM monitor_sessions WHERE connected_at < NOW() - INTERVAL '7 days';
-- 保留 30 天登入
DELETE FROM monitor_logins WHERE login_at < NOW() - INTERVAL '30 days';
-- 保留 90 天異常
DELETE FROM monitor_anomalies WHERE detected_at < NOW() - INTERVAL '90 days';
-- 清理空閒空間
VACUUM ANALYZE;
EOF
echo -e "${GREEN}清理完成${NC}"
}
# ============================================================
# 主程序
# ============================================================
main() {
# 確保日誌目錄存在
mkdir -p "$LOG_DIR"
local command=${1:-status}
case $command in
status)
show_status
;;
check)
check_layer ${2:-all}
;;
monitor)
run_monitor
;;
init)
init_monitor
;;
logs)
show_logs ${2:-anomaly} ${3:-20}
;;
clean)
clean_history
;;
help|--help|-h)
show_help
;;
*)
echo -e "${RED}未知命令: $command${NC}"
show_help
exit 1
;;
esac
}
main "$@"

View File

@@ -0,0 +1,82 @@
#!/bin/bash
# Momentry MongoDB 監控 (Layer 5)
# 路徑: /Users/accusys/momentry_core_0.1/monitor/database/mongodb_monitor.sh
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
LOG_DIR="/Users/accusys/momentry/log/monitor"
mkdir -p "$LOG_DIR"
LOG_FILE="$LOG_DIR/mongodb_check.log"
log() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" | tee -a "$LOG_FILE"
}
MONGO_USER="accusys"
MONGO_PASS="Test3200Test3200"
record_metric() {
psql -U accusys -h localhost -d momentry << EOF 2>/dev/null
INSERT INTO monitor_databases (db_type, db_name, metric_name, metric_value, checked_at)
VALUES ('mongodb', 'mongodb', '$1', '$2', NOW());
EOF
}
get_status() {
mongosh --quiet --username "$MONGO_USER" --password "$MONGO_PASS" --authenticationDatabase admin --eval "JSON.stringify(db.adminCommand({ replSetGetStatus: 1 }))" 2>/dev/null || echo "{}"
}
get_server_status() {
mongosh --quiet --username "$MONGO_USER" --password "$MONGO_PASS" --authenticationDatabase admin --eval "JSON.stringify(db.serverStatus()))" 2>/dev/null || echo "{}"
}
get_databases() {
mongosh --quiet --username "$MONGO_USER" --password "$MONGO_PASS" --authenticationDatabase admin --eval "JSON.stringify(db.adminCommand({ listDatabases: 1 }))" 2>/dev/null || echo "{}"
}
echo "========================================"
echo "Layer 5: MongoDB Monitoring"
echo "Time: $(date)"
echo "========================================"
echo ""
if ! mongosh --quiet --username "$MONGO_USER" --password "$MONGO_PASS" --authenticationDatabase admin --eval "db.adminCommand('ping')" > /dev/null 2>&1; then
echo "MongoDB 不可用"
log "MongoDB unavailable"
exit 1
fi
echo "✓ MongoDB 連接正常"
echo ""
echo "資料庫:"
echo "----------------------------------------"
databases=$(get_databases)
echo "$databases" | jq -r '.databases[] | " \(.name): \(.sizeOnDisk / 1024 / 1024 | floor)MB"' 2>/dev/null || echo " 無法獲取資料庫列表"
echo ""
echo "伺服器狀態:"
echo "----------------------------------------"
server_status=$(get_server_status)
connections=$(echo "$server_status" | jq -r '.connections.current' 2>/dev/null || echo "N/A")
active_connections=$(echo "$server_status" | jq -r '.connections.active' 2>/dev/null || echo "N/A")
uptime=$(echo "$server_status" | jq -r '.uptime' 2>/dev/null || echo "N/A")
mem_resident=$(echo "$server_status" | jq -r '.mem.resident' 2>/dev/null || echo "N/A")
echo " 當前連接: $connections"
echo " 活躍連接: $active_connections"
echo " 運行時間: ${uptime}"
echo " 記憶體使用: ${mem_resident}MB"
record_metric "connections" "$connections"
record_metric "active_connections" "$active_connections"
record_metric "uptime" "$uptime"
record_metric "mem_resident" "$mem_resident"
echo ""
log "MongoDB check completed: connections=$connections"
echo "========================================"
echo "完成"
echo "========================================"

View File

@@ -0,0 +1,130 @@
#!/bin/bash
# Momentry PostgreSQL 監控 (Layer 5)
# 路徑: /Users/accusys/momentry_core_0.1/monitor/database/postgres_monitor.sh
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
MONITOR_DIR="$(dirname "$SCRIPT_DIR")"
LOG_DIR="/Users/accusys/momentry/log/monitor"
mkdir -p "$LOG_DIR"
LOG_FILE="$LOG_DIR/postgres_check.log"
log() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" | tee -a "$LOG_FILE"
}
# 記錄指標
record_metric() {
local db_type="postgresql"
local db_name=$1
local metric_name=$2
local value=$3
psql -U accusys -h localhost -d momentry << EOF 2>/dev/null
INSERT INTO monitor_databases (db_type, db_name, metric_name, metric_value, checked_at)
VALUES ('$db_type', '$db_name', '$metric_name', '$value', NOW());
EOF
}
# 獲取資料庫列表
get_databases() {
psql -U accusys -h localhost -t -A -c "SELECT datname FROM pg_database WHERE datistemplate = false;" 2>/dev/null
}
# 獲取表大小
get_table_sizes() {
local db=$1
psql -U accusys -h localhost -d "$db" -t -A -c "
SELECT
schemaname,
tablename,
pg_size_pretty(pg_total_relation_size(schemaname||'.'||tablename)) as size,
n_live_tup as rows
FROM pg_stat_user_tables
ORDER BY pg_total_relation_size(schemaname||'.'||tablename) DESC
LIMIT 10;
" 2>/dev/null
}
# 獲取連線數
get_connections() {
psql -U accusys -h localhost -t -A -c "
SELECT state, count(*)
FROM pg_stat_activity
WHERE datname = current_database()
GROUP BY state;
" 2>/dev/null
}
# 獲取慢查詢
get_slow_queries() {
psql -U accusys -h localhost -t -A -c "
SELECT query, calls, mean_time
FROM pg_stat_statements
WHERE query NOT LIKE '%pg_stat_statements%'
ORDER BY mean_time DESC
LIMIT 5;
" 2>/dev/null
}
# 主程序
echo "========================================"
echo "Layer 5: PostgreSQL Database Monitoring"
echo "Time: $(date)"
echo "========================================"
echo ""
# 檢查 PostgreSQL 是否可用
if ! pg_isready -h localhost -p 5432 -U accusys > /dev/null 2>&1; then
echo "PostgreSQL 不可用"
log "PostgreSQL unavailable"
exit 1
fi
# 記錄連線數
connections=$(get_connections)
echo "連線狀態:"
echo "$connections"
echo ""
# 記錄指標
record_metric "postgres" "connections" "'$connections'"
# 檢查各資料庫
echo "資料庫表:"
echo "----------------------------------------"
for db in $(get_databases); do
echo ""
echo "資料庫: $db"
table_count=$(psql -U accusys -h localhost -d "$db" -t -A -c "SELECT count(*) FROM information_schema.tables WHERE table_schema = 'public';" 2>/dev/null || echo "0")
echo " 表數量: $table_count"
record_metric "$db" "table_count" "$table_count"
# 顯示大表
echo " 大表:"
get_table_sizes "$db" | while read -r schema table size rows; do
[ -z "$table" ] && continue
echo " - $table: $size ($rows rows)"
done
done
# 檢查慢查詢(如果 pg_stat_statements 可用)
echo ""
echo "慢查詢 (如有):"
slow_queries=$(get_slow_queries)
if [ -n "$slow_queries" ]; then
echo "$slow_queries" | while read -r query calls time; do
[ -z "$query" ] && continue
echo " - ${time}ms (調用 $calls 次)"
done
else
echo " (pg_stat_statements 未啟用)"
fi
echo ""
echo "========================================"
log "PostgreSQL check completed"

View File

@@ -0,0 +1,124 @@
#!/bin/bash
# Momentry Qdrant 監控 (Layer 5)
# 路徑: /Users/accusys/momentry_core_0.1/monitor/database/qdrant_monitor.sh
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
LOG_DIR="/Users/accusys/momentry/log/monitor"
mkdir -p "$LOG_DIR"
LOG_FILE="$LOG_DIR/qdrant_check.log"
log() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" | tee -a "$LOG_FILE"
}
QDRANT_HOST="http://localhost:6333"
QDRANT_API_KEY="Test3200Test3200Test3200"
# 記錄指標
record_metric() {
local collection=$1
local metric_name=$2
local value=$3
psql -U accusys -h localhost -d momentry << EOF 2>/dev/null
INSERT INTO monitor_databases (db_type, db_name, metric_name, metric_value, checked_at)
VALUES ('qdrant', '$collection', '$metric_name', '$value', NOW());
EOF
}
# 記錄 Collection
record_collection() {
local name=$1
local vectors=$2
local points=$3
local disk_size=$4
local status=$5
psql -U accusys -h localhost -d momentry << EOF 2>/dev/null
INSERT INTO monitor_qdrant_collections (collection_name, vectors_count, points_count, disk_size_bytes, status, snapshot_at)
VALUES ('$name', $vectors, $points, $disk_size, '$status', NOW())
ON CONFLICT (collection_name) DO UPDATE SET
vectors_count = EXCLUDED.vectors_count,
points_count = EXCLUDED.points_count,
disk_size_bytes = EXCLUDED.disk_size_bytes,
status = EXCLUDED.status,
snapshot_at = NOW();
EOF
}
# 主程序
echo "========================================"
echo "Layer 5: Qdrant Vector Database Monitoring"
echo "Time: $(date)"
echo "========================================"
echo ""
# 檢查 Qdrant 是否可用
http_code=$(curl -s -o /dev/null -w "%{http_code}" \
-H "api-key: $QDRANT_API_KEY" \
"$QDRANT_HOST/collections" 2>/dev/null || echo "000")
if [ "$http_code" = "000" ]; then
echo "Qdrant 不可用"
log "Qdrant unavailable"
exit 1
fi
echo "Qdrant 狀態: HTTP $http_code"
echo ""
# 獲取 Collection 列表
collections=$(curl -s -H "api-key: $QDRANT_API_KEY" "$QDRANT_HOST/collections" 2>/dev/null)
if [ "$http_code" = "200" ]; then
collection_count=$(echo "$collections" | jq '.result.collections | length' 2>/dev/null || echo "0")
echo "Collection 數量: $collection_count"
echo ""
# 遍歷每個 Collection
echo "Collections:"
echo "----------------------------------------"
for i in $(seq 0 $((collection_count - 1))); do
name=$(echo "$collections" | jq -r ".result.collections[$i].name")
# 獲取 Collection 詳情
details=$(curl -s -H "api-key: $QDRANT_API_KEY" "$QDRANT_HOST/collections/$name" 2>/dev/null)
vectors_count=$(echo "$details" | jq -r '.result.indexed_vectors_count // 0' 2>/dev/null || echo "0")
points_count=$(echo "$details" | jq -r '.result.points_count // 0' 2>/dev/null || echo "0")
disk_size=$(echo "$details" | jq -r '.result.disk_size_bytes // 0' 2>/dev/null || echo "0")
status=$(echo "$details" | jq -r '.result.status // "unknown"' 2>/dev/null || echo "unknown")
# 格式化大小
if [ "$disk_size" -gt 1073741824 ]; then
size_str="$((disk_size / 1073741824))GB"
elif [ "$disk_size" -gt 1048576 ]; then
size_str="$((disk_size / 1048576))MB"
elif [ "$disk_size" -gt 1024 ]; then
size_str="$((disk_size / 1024))KB"
else
size_str="${disk_size}B"
fi
echo " - $name"
echo " 狀態: $status"
echo " Vectors: $vectors_count"
echo " Points: $points_count"
echo " 大小: $size_str"
# 記錄到資料庫
record_collection "$name" "$vectors_count" "$points_count" "$disk_size" "$status"
record_metric "$name" "vectors_count" "$vectors_count"
record_metric "$name" "points_count" "$points_count"
record_metric "$name" "disk_size" "$disk_size"
done
else
echo "無法獲取 Collection 列表"
log "Failed to get collections: HTTP $http_code"
fi
echo ""
echo "========================================"
log "Qdrant check completed"

111
monitor/database/redis_monitor.sh Executable file
View File

@@ -0,0 +1,111 @@
#!/bin/bash
# Momentry Redis 監控 (Layer 5)
# 路徑: /Users/accusys/momentry_core_0.1/monitor/database/redis_monitor.sh
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
LOG_DIR="/Users/accusys/momentry/log/monitor"
mkdir -p "$LOG_DIR"
LOG_FILE="$LOG_DIR/redis_check.log"
log() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" | tee -a "$LOG_FILE"
}
REDIS_PASS="accusys"
# 記錄指標
record_metric() {
local value=$1
psql -U accusys -h localhost -d momentry << EOF 2>/dev/null
INSERT INTO monitor_databases (db_type, db_name, metric_name, metric_value, checked_at)
VALUES ('redis', 'redis', '$1', '$2', NOW());
EOF
}
# 獲取 Redis INFO
get_info() {
redis-cli -a "$REDIS_PASS" INFO 2>/dev/null
}
# 主程序
echo "========================================"
echo "Layer 5: Redis Monitoring"
echo "Time: $(date)"
echo "========================================"
echo ""
# 檢查 Redis 是否可用
if ! redis-cli -a "$REDIS_PASS" ping > /dev/null 2>&1; then
echo "Redis 不可用"
log "Redis unavailable"
exit 1
fi
info=$(get_info)
# 提取關鍵指標
echo "關鍵指標:"
echo "----------------------------------------"
# 內存使用
used_memory=$(echo "$info" | grep "^used_memory_human:" | cut -d: -f2 | tr -d '\r')
echo " 內存使用: $used_memory"
# 連線數
connected_clients=$(echo "$info" | grep "^connected_clients:" | cut -d: -f2 | tr -d '\r')
echo " 客戶端連線: $connected_clients"
# 命中率
keyspace_hits=$(echo "$info" | grep "^keyspace_hits:" | cut -d: -f2 | tr -d '\r')
keyspace_misses=$(echo "$info" | grep "^keyspace_misses:" | cut -d: -f2 | tr -d '\r')
total_ops=$((keyspace_hits + keyspace_misses))
if [ $total_ops -gt 0 ]; then
hit_rate=$((keyspace_hits * 100 / total_ops))
echo " 命中率: ${hit_rate}%"
else
echo " 命中率: N/A"
fi
# 持久化
rdb_changes=$(echo "$info" | grep "^rdb_changes_since_last_save:" | cut -d: -f2 | tr -d '\r')
echo " RDB 變更: $rdb_changes"
# 總鍵數
echo ""
echo "鍵數據庫:"
db0_info=$(echo "$info" | grep "^db0:" | head -1)
if [ -n "$db0_info" ]; then
keys=$(echo "$db0_info" | sed 's/.*keys=\([0-9]*\).*/\1/')
expires=$(echo "$db0_info" | sed 's/.*expires=\([0-9]*\).*/\1/')
echo " db0: $keys keys, $expires 有過期時間"
fi
# 記錄到資料庫
record_metric "used_memory" "'$used_memory'"
record_metric "connected_clients" "$connected_clients"
record_metric "keyspace_hits" "$keyspace_hits"
record_metric "keyspace_misses" "$keyspace_misses"
# 檢查閾值
echo ""
echo "閾值檢查:"
memory_percent=$(echo "$info" | grep "^used_memory:" | cut -d: -f2)
maxmemory=$(redis-cli -a "$REDIS_PASS" CONFIG GET maxmemory 2>/dev/null | tail -1)
if [ -n "$maxmemory" ] && [ "$maxmemory" -gt 0 ]; then
mem_pct=$((memory_percent * 100 / maxmemory))
echo " 內存使用: ${mem_pct}%"
if [ $mem_pct -gt 80 ]; then
echo " ⚠️ 內存使用超過 80%"
fi
fi
if [ $connected_clients -gt 100 ]; then
echo " ⚠️ 客戶端連線過多"
fi
echo ""
echo "========================================"
log "Redis check completed"

492
monitor/database/schema.sql Normal file
View File

@@ -0,0 +1,492 @@
-- Momentry 監控系統數據庫表
-- 使用方式: psql -U accusys -h localhost -d momentry -f schema.sql
-- ============================================================
-- Layer 2: Service 監控
-- ============================================================
CREATE TABLE IF NOT EXISTS monitor_services (
id SERIAL PRIMARY KEY,
service_name VARCHAR(50) NOT NULL,
service_type VARCHAR(20),
port INTEGER,
status VARCHAR(20) CHECK (status IN ('up', 'down', 'degraded', 'unknown')),
response_time_ms INTEGER,
error_message TEXT,
checked_at TIMESTAMP DEFAULT NOW()
);
CREATE INDEX idx_monitor_services_name ON monitor_services(service_name);
CREATE INDEX idx_monitor_services_time ON monitor_services(checked_at);
-- ============================================================
-- Layer 3: n8n Workflow 監控
-- ============================================================
CREATE TABLE IF NOT EXISTS monitor_workflows (
id SERIAL PRIMARY KEY,
workflow_id VARCHAR(50) NOT NULL,
workflow_name VARCHAR(255),
workflow_type VARCHAR(50),
is_active BOOLEAN DEFAULT FALSE,
last_executed_at TIMESTAMP,
execution_count INTEGER DEFAULT 0,
success_count INTEGER DEFAULT 0,
failure_count INTEGER DEFAULT 0,
avg_duration_ms INTEGER,
has_schedule BOOLEAN DEFAULT FALSE,
has_webhook BOOLEAN DEFAULT FALSE,
idle_days INTEGER,
suggestion VARCHAR(100),
checked_at TIMESTAMP DEFAULT NOW()
);
CREATE INDEX idx_monitor_workflows_id ON monitor_workflows(workflow_id);
CREATE INDEX idx_monitor_workflows_active ON monitor_workflows(is_active);
CREATE INDEX idx_monitor_workflows_idle ON monitor_workflows(idle_days);
-- ============================================================
-- Layer 4: WordPress Portal 監控
-- ============================================================
CREATE TABLE IF NOT EXISTS monitor_portal_pages (
id SERIAL PRIMARY KEY,
page_url VARCHAR(500) NOT NULL,
page_type VARCHAR(20),
is_accessible BOOLEAN,
response_time_ms INTEGER,
http_status INTEGER,
error_message TEXT,
checked_at TIMESTAMP DEFAULT NOW()
);
CREATE TABLE IF NOT EXISTS monitor_portal_users (
id SERIAL PRIMARY KEY,
user_id BIGINT,
username VARCHAR(100),
email VARCHAR(255),
role VARCHAR(50),
is_active BOOLEAN,
last_login TIMESTAMP,
created_at TIMESTAMP,
detected_at TIMESTAMP DEFAULT NOW()
);
CREATE INDEX idx_monitor_portal_pages_url ON monitor_portal_pages(page_url);
CREATE INDEX idx_monitor_portal_users_username ON monitor_portal_users(username);
-- ============================================================
-- Layer 5: Database 監控
-- ============================================================
CREATE TABLE IF NOT EXISTS monitor_databases (
id SERIAL PRIMARY KEY,
db_type VARCHAR(20) NOT NULL CHECK (db_type IN ('postgresql', 'redis', 'qdrant', 'mariadb', 'mongodb')),
db_name VARCHAR(50),
metric_name VARCHAR(50) NOT NULL,
metric_value JSONB,
checked_at TIMESTAMP DEFAULT NOW()
);
CREATE INDEX idx_monitor_databases_type ON monitor_databases(db_type);
CREATE INDEX idx_monitor_databases_time ON monitor_databases(checked_at);
-- PostgreSQL 表結構快照
CREATE TABLE IF NOT EXISTS monitor_pg_tables (
id SERIAL PRIMARY KEY,
database_name VARCHAR(50),
schema_name VARCHAR(50),
table_name VARCHAR(100),
table_type VARCHAR(20),
row_count BIGINT,
table_size_bytes BIGINT,
index_size_bytes BIGINT,
snapshot_at TIMESTAMP DEFAULT NOW()
);
-- 表結構變更記錄
CREATE TABLE IF NOT EXISTS monitor_pg_schema_changes (
id SERIAL PRIMARY KEY,
database_name VARCHAR(50),
schema_name VARCHAR(50),
table_name VARCHAR(100),
change_type VARCHAR(20) CHECK (change_type IN ('table_created', 'table_dropped', 'column_added', 'column_removed', 'column_type_changed')),
column_name VARCHAR(100),
old_value TEXT,
new_value TEXT,
detected_at TIMESTAMP DEFAULT NOW()
);
-- Qdrant Collection 監控
CREATE TABLE IF NOT EXISTS monitor_qdrant_collections (
id SERIAL PRIMARY KEY,
collection_name VARCHAR(100),
vectors_count BIGINT,
points_count BIGINT,
disk_size_bytes BIGINT,
status VARCHAR(20),
snapshot_at TIMESTAMP DEFAULT NOW()
);
-- ============================================================
-- Layer 6: 使用者監控
-- ============================================================
-- 連線會話追蹤
CREATE TABLE IF NOT EXISTS monitor_sessions (
id SERIAL PRIMARY KEY,
session_type VARCHAR(20) CHECK (session_type IN ('ssh', 'web', 'db', 'sftp', 'rdp')),
service_name VARCHAR(50),
username VARCHAR(100),
source_ip VARCHAR(45),
source_port INTEGER,
connected_at TIMESTAMP,
last_activity_at TIMESTAMP,
disconnected_at TIMESTAMP,
bytes_sent BIGINT,
bytes_received BIGINT,
status VARCHAR(20) CHECK (status IN ('active', 'disconnected', 'timeout'))
);
-- 登入歷史
CREATE TABLE IF NOT EXISTS monitor_logins (
id SERIAL PRIMARY KEY,
user_type VARCHAR(20) CHECK (user_type IN ('system', 'wordpress', 'n8n', 'gitea', 'sftpgo', 'database')),
username VARCHAR(100),
source_ip VARCHAR(45),
user_agent TEXT,
login_method VARCHAR(20),
success BOOLEAN,
failure_reason VARCHAR(200),
login_at TIMESTAMP DEFAULT NOW()
);
-- sudo 命令記錄
CREATE TABLE IF NOT EXISTS monitor_sudo_history (
id SERIAL PRIMARY KEY,
username VARCHAR(100),
command TEXT,
run_as VARCHAR(100),
tty VARCHAR(50),
source_ip VARCHAR(45),
exit_code INTEGER,
executed_at TIMESTAMP DEFAULT NOW()
);
-- 資源使用追蹤
CREATE TABLE IF NOT EXISTS monitor_resource_usage (
id SERIAL PRIMARY KEY,
user_type VARCHAR(20),
username VARCHAR(100),
service_name VARCHAR(50),
cpu_percent DECIMAL(5,2),
memory_mb INTEGER,
disk_io_read_mb BIGINT,
disk_io_write_mb BIGINT,
network_rx_mb BIGINT,
network_tx_mb BIGINT,
recorded_at TIMESTAMP DEFAULT NOW()
);
-- 異常檢測記錄
CREATE TABLE IF NOT EXISTS monitor_anomalies (
id SERIAL PRIMARY KEY,
anomaly_type VARCHAR(50) CHECK (anomaly_type IN ('brute_force', 'privilege_escalation', 'unusual_access', 'unusual_time', 'excessive_queries', 'idle_session', 'schema_change')),
severity VARCHAR(20) CHECK (severity IN ('low', 'medium', 'high', 'critical')),
source_type VARCHAR(20),
username VARCHAR(100),
source_ip VARCHAR(45),
description TEXT,
details JSONB,
detected_at TIMESTAMP DEFAULT NOW(),
resolved BOOLEAN DEFAULT FALSE,
resolved_at TIMESTAMP
);
CREATE INDEX idx_monitor_sessions_type ON monitor_sessions(session_type);
CREATE INDEX idx_monitor_sessions_username ON monitor_sessions(username);
CREATE INDEX idx_monitor_logins_type ON monitor_logins(user_type);
CREATE INDEX idx_monitor_logins_time ON monitor_logins(login_at);
CREATE INDEX idx_monitor_anomalies_type ON monitor_anomalies(anomaly_type);
CREATE INDEX idx_monitor_anomalies_severity ON monitor_anomalies(severity);
CREATE INDEX idx_monitor_anomalies_time ON monitor_anomalies(detected_at);
-- ============================================================
-- Layer 7: Storage 監控
-- ============================================================
-- 檔案註冊表
CREATE TABLE IF NOT EXISTS file_registry (
file_uuid UUID PRIMARY KEY DEFAULT gen_random_uuid(),
file_name VARCHAR(255) NOT NULL,
file_path TEXT NOT NULL,
file_path_hash VARCHAR(64) NOT NULL,
file_size BIGINT NOT NULL,
file_hash VARCHAR(64),
mime_type VARCHAR(100),
user_cluster VARCHAR(50) CHECK (user_cluster IN ('family', 'work', 'wordpress', 'shared', 'system')),
owner_id VARCHAR(100),
storage_tier VARCHAR(20) DEFAULT 'hot' CHECK (storage_tier IN ('hot', 'warm', 'cold')),
storage_location VARCHAR(500),
status VARCHAR(20) DEFAULT 'active' CHECK (status IN ('active', 'temporary', 'archived', 'deleted')),
is_registered BOOLEAN DEFAULT TRUE,
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW(),
last_accessed_at TIMESTAMP,
access_count INTEGER DEFAULT 0,
archived_at TIMESTAMP,
archive_location VARCHAR(500),
retention_until TIMESTAMP,
UNIQUE(file_path_hash)
);
-- 存儲使用統計
CREATE TABLE IF NOT EXISTS storage_usage_stats (
id SERIAL PRIMARY KEY,
user_cluster VARCHAR(50),
storage_tier VARCHAR(20),
file_count BIGINT,
total_size_bytes BIGINT,
record_time TIMESTAMP DEFAULT NOW()
);
-- 文件訪問日誌
CREATE TABLE IF NOT EXISTS storage_access_logs (
id SERIAL PRIMARY KEY,
user_cluster VARCHAR(50),
owner_id VARCHAR(100),
file_path TEXT,
access_type VARCHAR(20) CHECK (access_type IN ('read', 'write', 'delete', 'download', 'move')),
access_time TIMESTAMP DEFAULT NOW(),
client_ip VARCHAR(45),
access_method VARCHAR(20)
);
-- 文件生命週期
CREATE TABLE IF NOT EXISTS file_lifecycle (
id SERIAL PRIMARY KEY,
file_uuid UUID REFERENCES file_registry(file_uuid),
file_path TEXT,
user_cluster VARCHAR(50),
storage_tier VARCHAR(20),
created_at TIMESTAMP,
last_accessed_at TIMESTAMP,
last_modified_at TIMESTAMP,
access_count INTEGER DEFAULT 0,
current_status VARCHAR(20) DEFAULT 'active',
tier_migration_count INTEGER DEFAULT 0,
migrated_at TIMESTAMP
);
CREATE INDEX idx_file_registry_cluster ON file_registry(user_cluster);
CREATE INDEX idx_file_registry_tier ON file_registry(storage_tier);
CREATE INDEX idx_file_registry_status ON file_registry(status);
CREATE INDEX idx_storage_usage_cluster ON storage_usage_stats(user_cluster);
CREATE INDEX idx_storage_usage_time ON storage_usage_stats(record_time);
-- ============================================================
-- 外部監控 (Layer 1)
-- ============================================================
CREATE TABLE IF NOT EXISTS monitor_external (
id SERIAL PRIMARY KEY,
target_name VARCHAR(50) NOT NULL,
target_type VARCHAR(20) CHECK (target_type IN ('ddns', 'gateway', 'internet', 'api')),
target_host VARCHAR(255),
is_reachable BOOLEAN,
response_time_ms INTEGER,
dns_resolved_ip VARCHAR(45),
error_message TEXT,
checked_at TIMESTAMP DEFAULT NOW()
);
CREATE INDEX idx_monitor_external_name ON monitor_external(target_name);
CREATE INDEX idx_monitor_external_time ON monitor_external(checked_at);
-- ============================================================
-- 監控配置表
-- ============================================================
CREATE TABLE IF NOT EXISTS monitor_config (
id SERIAL PRIMARY KEY,
config_key VARCHAR(50) UNIQUE NOT NULL,
config_value TEXT,
description VARCHAR(255),
updated_at TIMESTAMP DEFAULT NOW()
);
-- 插入默認配置
INSERT INTO monitor_config (config_key, config_value, description) VALUES
('check_interval', '300', '監控檢查間隔(秒)'),
('retention_days', '30', '歷史數據保留天數'),
('idle_threshold_days', '30', 'Workflow 閒置天數閾值'),
('alert_threshold_bruteforce', '5', '暴力破解嘗試次數閾值'),
('alert_threshold_slow_response', '3000', '響應時間閾值(毫秒)')
ON CONFLICT (config_key) DO NOTHING;
-- ============================================================
-- 視圖定義
-- ============================================================
-- 服務健康狀態視圖
CREATE OR REPLACE VIEW v_service_health AS
SELECT
service_name,
status,
COUNT(*) as check_count,
COUNT(*) FILTER (WHERE status = 'up') as up_count,
COUNT(*) FILTER (WHERE status = 'down') as down_count,
AVG(response_time_ms) as avg_response_time,
MAX(checked_at) as last_check
FROM monitor_services
WHERE checked_at > NOW() - INTERVAL '24 hours'
GROUP BY service_name, status;
-- 最近異常視圖
CREATE OR REPLACE VIEW v_recent_anomalies AS
SELECT
anomaly_type,
severity,
username,
source_ip,
description,
detected_at
FROM monitor_anomalies
WHERE detected_at > NOW() - INTERVAL '24 hours'
ORDER BY detected_at DESC;
-- 閒置 Workflow 視圖
CREATE OR REPLACE VIEW v_idle_workflows AS
SELECT
workflow_name,
idle_days,
suggestion,
last_executed_at
FROM monitor_workflows
WHERE idle_days > 30 AND is_active = TRUE
ORDER BY idle_days DESC;
-- 存儲使用概況視圖
CREATE OR REPLACE VIEW v_storage_overview AS
SELECT
user_cluster,
storage_tier,
COUNT(*) as file_count,
SUM(file_size) as total_size
FROM file_registry
WHERE status = 'active'
GROUP BY user_cluster, storage_tier;
-- ============================================================
-- 備份監控 (Layer 7 Extension)
-- ============================================================
-- 備份註冊表
CREATE TABLE IF NOT EXISTS backup_registry (
id SERIAL PRIMARY KEY,
service_name VARCHAR(50) NOT NULL,
backup_file VARCHAR(500) NOT NULL,
backup_size_bytes BIGINT,
backup_type VARCHAR(20) CHECK (backup_type IN ('daily', 'weekly', 'monthly', 'archive', 'full', 'incremental')),
backup_method VARCHAR(20) CHECK (backup_method IN ('pg_dump', 'mysqldump', 'tar', 'snapshot', 'dump')),
status VARCHAR(20) CHECK (status IN ('pending', 'running', 'completed', 'failed', 'verified')),
compression_ratio DECIMAL(5,2),
verification_result BOOLEAN,
error_message TEXT,
started_at TIMESTAMP DEFAULT NOW(),
completed_at TIMESTAMP,
created_at TIMESTAMP DEFAULT NOW()
);
-- 備份存儲統計
CREATE TABLE IF NOT EXISTS backup_storage_stats (
id SERIAL PRIMARY KEY,
tier VARCHAR(20) CHECK (tier IN ('daily', 'weekly', 'monthly', 'archive', 'total')),
file_count BIGINT,
total_size_bytes BIGINT,
record_time TIMESTAMP DEFAULT NOW()
);
-- 備份歷史
CREATE TABLE IF NOT EXISTS backup_history (
id SERIAL PRIMARY KEY,
service_name VARCHAR(50) NOT NULL,
operation VARCHAR(20) CHECK (operation IN ('backup', 'restore', 'tier_migration', 'cleanup', 'verify')),
backup_file VARCHAR(500),
backup_tier VARCHAR(20),
source_tier VARCHAR(20),
dest_tier VARCHAR(20),
file_count BIGINT,
size_bytes BIGINT,
duration_seconds INTEGER,
status VARCHAR(20) CHECK (status IN ('success', 'failed', 'partial')),
error_message TEXT,
executed_at TIMESTAMP DEFAULT NOW()
);
CREATE INDEX idx_backup_registry_service ON backup_registry(service_name);
CREATE INDEX idx_backup_registry_time ON backup_registry(created_at);
CREATE INDEX idx_backup_storage_stats_tier ON backup_storage_stats(tier);
CREATE INDEX idx_backup_storage_stats_time ON backup_storage_stats(record_time);
CREATE INDEX idx_backup_history_service ON backup_history(service_name);
CREATE INDEX idx_backup_history_time ON backup_history(executed_at);
-- ============================================================
-- Node.js 版本基線監控
-- ============================================================
CREATE TABLE IF NOT EXISTS node_version_baseline (
id SERIAL PRIMARY KEY,
runtime_name VARCHAR(50) NOT NULL,
required_version VARCHAR(20) NOT NULL,
current_version VARCHAR(20),
process_name VARCHAR(100),
process_path TEXT,
is_compliant BOOLEAN,
locked_path VARCHAR(500),
checked_at TIMESTAMP DEFAULT NOW()
);
-- Node.js 進程追蹤
CREATE TABLE IF NOT EXISTS node_process_tracking (
id SERIAL PRIMARY KEY,
process_name VARCHAR(100) NOT NULL,
pid INTEGER,
command VARCHAR(500),
node_version VARCHAR(20),
is_managed BOOLEAN DEFAULT FALSE,
started_at TIMESTAMP,
checked_at TIMESTAMP DEFAULT NOW()
);
-- ============================================================
-- Python 版本基線監控
-- ============================================================
CREATE TABLE IF NOT EXISTS python_version_baseline (
id SERIAL PRIMARY KEY,
runtime_name VARCHAR(50) NOT NULL,
required_version VARCHAR(20) NOT NULL,
current_version VARCHAR(20),
interpreter_path VARCHAR(500),
is_compliant BOOLEAN,
checked_at TIMESTAMP DEFAULT NOW()
);
-- Python 腳本追蹤
CREATE TABLE IF NOT EXISTS python_script_tracking (
id SERIAL PRIMARY KEY,
script_path TEXT NOT NULL,
shebang_version VARCHAR(20),
actual_version VARCHAR(20),
is_compliant BOOLEAN DEFAULT FALSE,
last_run_at TIMESTAMP,
exit_code INTEGER,
error_output TEXT,
checked_at TIMESTAMP DEFAULT NOW()
);
CREATE INDEX idx_node_version_name ON node_version_baseline(runtime_name);
CREATE INDEX idx_node_process_name ON node_process_tracking(process_name);
CREATE INDEX idx_python_version_name ON python_version_baseline(runtime_name);
CREATE INDEX idx_python_script_path ON python_script_tracking(script_path);

175
monitor/portal/page_monitor.sh Executable file
View File

@@ -0,0 +1,175 @@
#!/bin/bash
# Momentry WordPress Portal 監控 (Layer 4)
# 路徑: /Users/accusys/momentry_core_0.1/monitor/portal/page_monitor.sh
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
MONITOR_DIR="$(dirname "$SCRIPT_DIR")"
LOG_DIR="/Users/accusys/momentry/log/monitor"
mkdir -p "$LOG_DIR"
LOG_FILE="$LOG_DIR/portal_check.log"
log() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" | tee -a "$LOG_FILE"
}
# WordPress 配置
WP_SITE="https://wp.momentry.ddns.net"
WP_DB_HOST="localhost"
WP_DB_NAME="wordpress"
WP_DB_USER="wp_user"
WP_DB_PASS="wp_password_123"
# 顏色
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m'
# 記錄頁面檢查結果
record_page() {
local url=$1
local page_type=$2
local accessible=$3
local response_time=$4
local http_status=$5
local error=$6
psql -U accusys -h localhost -d momentry << EOF 2>/dev/null
INSERT INTO monitor_portal_pages (page_url, page_type, is_accessible, response_time_ms, http_status, error_message, checked_at)
VALUES ('$url', '$page_type', $accessible, $response_time, $http_status, '$error', NOW());
EOF
}
# 記錄用戶
record_user() {
local user_id=$1
local username=$2
local email=$3
local role=$4
local is_active=$5
local last_login=$6
local created_at=$7
psql -U accusys -h localhost -d momentry << EOF 2>/dev/null
INSERT INTO monitor_portal_users (user_id, username, email, role, is_active, last_login, created_at, detected_at)
VALUES ($user_id, '$username', '$email', '$role', $is_active, $last_login, $created_at, NOW())
ON CONFLICT DO NOTHING;
EOF
}
# 記錄異常
record_anomaly() {
local anomaly_type=$1
local severity=$2
local username=$3
local description=$4
psql -U accusys -h localhost -d momentry << EOF 2>/dev/null
INSERT INTO monitor_anomalies (anomaly_type, severity, source_type, username, description, detected_at)
VALUES ('$anomaly_type', '$severity', 'wordpress', '$username', '$description', NOW());
EOF
}
# 檢查頁面
check_page() {
local url=$1
local page_type=$2
local start=$(date +%s%N)
local http_code=$(curl -s -o /dev/null -w "%{http_code}" "$url" --max-time 10 -k -L 2>/dev/null || echo "000")
local end=$(date +%s%N)
local ms=$(( (end - start) / 1000000 ))
if [ "$http_code" = "200" ]; then
accessible="true"
error=""
echo -e "${GREEN}${NC} $page_type - ${ms}ms (HTTP $http_code)"
else
accessible="false"
error="HTTP $http_code"
echo -e "${RED}${NC} $page_type - HTTP $http_code"
fi
record_page "$url" "$page_type" "$accessible" "$ms" "$http_code" "$error"
}
# 檢查用戶
check_users() {
echo ""
echo "WordPress 用戶檢查:"
echo "----------------------------------------"
# 獲取用戶列表
users=$(mysql -u"$WP_DB_USER" -p"$WP_DB_PASS" -h "$WP_DB_HOST" "$WP_DB_NAME" -N -e "
SELECT u.ID, u.user_login, u.user_email, u.user_registered, u.user_status,
COALESCE(m.meta_value, 'subscriber') as role
FROM wp_users u
LEFT JOIN wp_usermeta m ON u.ID = m.user_id AND m.meta_key = 'wp_capabilities'
ORDER BY u.ID;
" 2>/dev/null)
if [ -z "$users" ]; then
echo "無法連接 WordPress 資料庫"
return 1
fi
local admin_count=0
local total_users=0
while IFS='|' read -r id login email registered status role; do
[ -z "$id" ] && continue
total_users=$((total_users + 1))
# 判斷是否管理員
if echo "$role" | grep -q "administrator"; then
admin_count=$((admin_count + 1))
role="administrator"
elif echo "$role" | grep -q "editor"; then
role="editor"
elif echo "$role" | grep -q "author"; then
role="author"
elif echo "$role" | grep -q "contributor"; then
role="contributor"
else
role="subscriber"
fi
# 記錄用戶
record_user "$id" "$login" "$email" "$role" "true" "NULL" "'$registered'"
echo " - $login ($role)"
done <<< "$users"
echo "----------------------------------------"
echo "總用戶: $total_users | 管理員: $admin_count"
}
# 主程序
echo "========================================"
echo "Layer 4: WordPress Portal Monitoring"
echo "Time: $(date)"
echo "========================================"
echo ""
echo "頁面可訪問性檢查:"
echo "----------------------------------------"
# 檢查首頁
check_page "$WP_SITE/" "homepage"
# 檢查登入頁
check_page "$WP_SITE/wp-login.php" "login_page"
# 檢查 wp-json API
check_page "$WP_SITE/wp-json/" "api"
echo ""
check_users
echo ""
echo "========================================"
log "Portal check completed"

View File

@@ -0,0 +1,93 @@
#!/bin/bash
# Momentry 外部監控 (Layer 1)
# 路徑: /Users/accusys/momentry_core_0.1/monitor/service/external_monitor.sh
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
LOG_DIR="/Users/accusys/momentry/log/monitor"
mkdir -p "$LOG_DIR"
LOG_FILE="$LOG_DIR/external_check.log"
log() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" | tee -a "$LOG_FILE"
}
# 記錄結果
record_external() {
local target=$1
local target_type=$2
local reachable=$3
local response_time=$4
local error=$5
psql -U accusys -h localhost -d momentry << EOF 2>/dev/null
INSERT INTO monitor_external (target_name, target_type, is_reachable, response_time_ms, error_message, checked_at)
VALUES ('$target', '$target_type', $reachable, $response_time, '$error', NOW());
EOF
}
# 檢查 DDNS
check_ddns() {
local start=$(date +%s%N)
local ip=$(dig +short momentry.ddns.net 2>/dev/null | tail -1)
local end=$(date +%s%N)
local ms=$(( (end - start) / 1000000 ))
if [ -n "$ip" ]; then
echo "✓ DDNS (momentry.ddns.net) -> $ip (${ms}ms)"
record_external "ddns" "ddns" "true" "$ms" ""
return 0
else
echo "✗ DDNS (momentry.ddns.net) - DNS resolution failed"
record_external "ddns" "ddns" "false" "0" "DNS resolution failed"
return 1
fi
}
# 檢查網關
check_gateway() {
local start=$(date +%s%N)
if ping -c 1 -W 2 192.168.110.1 > /dev/null 2>&1; then
local end=$(date +%s%N)
local ms=$(( (end - start) / 1000000 ))
echo "✓ Gateway (192.168.110.1) - ${ms}ms"
record_external "gateway" "gateway" "true" "$ms" ""
return 0
else
echo "✗ Gateway (192.168.110.1) - Unreachable"
record_external "gateway" "gateway" "false" "0" "Unreachable"
return 1
fi
}
# 檢查互聯網
check_internet() {
local start=$(date +%s%N)
if ping -c 1 -W 2 8.8.8.8 > /dev/null 2>&1; then
local end=$(date +%s%N)
local ms=$(( (end - start) / 1000000 ))
echo "✓ Internet (8.8.8.8) - ${ms}ms"
record_external "internet" "internet" "true" "$ms" ""
return 0
else
echo "✗ Internet (8.8.8.8) - Unreachable"
record_external "internet" "internet" "false" "0" "Unreachable"
return 1
fi
}
# 主程序
echo "========================================"
echo "Layer 1: External Monitoring"
echo "Time: $(date)"
echo "========================================"
echo ""
check_ddns
check_gateway
check_internet
echo ""
echo "========================================"
log "External check completed"

370
monitor/service/health_check.sh Executable file
View File

@@ -0,0 +1,370 @@
#!/bin/bash
# Momentry 服務健康檢查 (Layer 2)
# 路徑: /Users/accusys/momentry_core_0.1/monitor/service/health_check.sh
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
MONITOR_DIR="$(dirname "$SCRIPT_DIR")"
LOG_DIR="/Users/accusys/momentry/log/monitor"
mkdir -p "$LOG_DIR"
LOG_FILE="$LOG_DIR/service_check.log"
log() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" | tee -a "$LOG_FILE"
}
# 顏色
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m'
# 記錄結果到資料庫
record_service() {
local service=$1
local status=$2
local response_time=$3
local error_msg=$4
psql -U accusys -h localhost -d momentry << EOF 2>/dev/null
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
}
# 檢查 PostgreSQL
check_postgresql() {
local start=$(date +%s%N)
if pg_isready -h localhost -p 5432 -U accusys > /dev/null 2>&1; then
local end=$(date +%s%N)
local ms=$(( (end - start) / 1000000 ))
echo -e "${GREEN}${NC} PostgreSQL (5432) - ${ms}ms"
record_service "postgresql" "up" "$ms" ""
return 0
else
echo -e "${RED}${NC} PostgreSQL (5432) - Down"
record_service "postgresql" "down" "0" "Connection failed"
return 1
fi
}
# 檢查 Redis
check_redis() {
local start=$(date +%s%N)
if redis-cli -a accusys ping 2>/dev/null | grep -q "PONG"; 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
}
# 檢查 MariaDB
check_mariadb() {
local start=$(date +%s%N)
if mysql -u accusys -e "SELECT 1" > /dev/null 2>&1; then
local end=$(date +%s%N)
local ms=$(( (end - start) / 1000000 ))
echo -e "${GREEN}${NC} MariaDB (3306) - ${ms}ms"
record_service "mariadb" "up" "$ms" ""
return 0
else
echo -e "${RED}${NC} MariaDB (3306) - Down"
record_service "mariadb" "down" "0" "Connection failed"
return 1
fi
}
# 檢查 n8n
check_n8n() {
local start=$(date +%s%N)
local http_code=$(curl -s -o /dev/null -w "%{http_code}" http://localhost:8085/ --max-time 5)
local end=$(date +%s%N)
local ms=$(( (end - start) / 1000000 ))
if [ "$http_code" = "200" ] || [ "$http_code" = "302" ]; then
echo -e "${GREEN}${NC} n8n (8085) - ${ms}ms"
record_service "n8n" "up" "$ms" ""
return 0
else
echo -e "${RED}${NC} n8n (8085) - HTTP $http_code"
record_service "n8n" "down" "0" "HTTP $http_code"
return 1
fi
}
# 檢查 Caddy
check_caddy() {
local start=$(date +%s%N)
local http_code=$(curl -s -o /dev/null -w "%{http_code}" http://localhost:2019/config/ --max-time 5)
local end=$(date +%s%N)
local ms=$(( (end - start) / 1000000 ))
if [ "$http_code" = "200" ]; then
echo -e "${GREEN}${NC} Caddy (2019) - ${ms}ms"
record_service "caddy" "up" "$ms" ""
return 0
else
echo -e "${RED}${NC} Caddy (2019) - HTTP $http_code"
record_service "caddy" "down" "0" "HTTP $http_code"
return 1
fi
}
# 檢查 Gitea
check_gitea() {
local start=$(date +%s%N)
local http_code=$(curl -s -o /dev/null -w "%{http_code}" http://localhost:3000/ --max-time 5)
local end=$(date +%s%N)
local ms=$(( (end - start) / 1000000 ))
if [ "$http_code" = "200" ]; then
echo -e "${GREEN}${NC} Gitea (3000) - ${ms}ms"
record_service "gitea" "up" "$ms" ""
return 0
else
echo -e "${RED}${NC} Gitea (3000) - HTTP $http_code"
record_service "gitea" "down" "0" "HTTP $http_code"
return 1
fi
}
# 檢查 SFTPGo
check_sftpgo() {
local start=$(date +%s%N)
local http_code=$(curl -s -o /dev/null -w "%{http_code}" http://localhost:8080 --max-time 5)
local end=$(date +%s%N)
local ms=$(( (end - start) / 1000000 ))
if [ "$http_code" = "200" ] || [ "$http_code" = "301" ] || [ "$http_code" = "302" ]; then
echo -e "${GREEN}${NC} SFTPGo (8080) - ${ms}ms"
record_service "sftpgo" "up" "$ms" ""
return 0
else
echo -e "${RED}${NC} SFTPGo (8080) - HTTP $http_code"
record_service "sftpgo" "down" "0" "HTTP $http_code"
return 1
fi
}
# 檢查 Ollama
check_ollama() {
local start=$(date +%s%N)
local http_code=$(curl -s -o /dev/null -w "%{http_code}" http://localhost:11434/api/tags --max-time 5)
local end=$(date +%s%N)
local ms=$(( (end - start) / 1000000 ))
if [ "$http_code" = "200" ]; then
echo -e "${GREEN}${NC} Ollama (11434) - ${ms}ms"
record_service "ollama" "up" "$ms" ""
return 0
else
echo -e "${RED}${NC} Ollama (11434) - HTTP $http_code"
record_service "ollama" "down" "0" "HTTP $http_code"
return 1
fi
}
# 檢查 Qdrant
check_qdrant() {
local start=$(date +%s%N)
local http_code=$(curl -s -o /dev/null -w "%{http_code}" http://localhost:6333/collections --max-time 5)
local end=$(date +%s%N)
local ms=$(( (end - start) / 1000000 ))
if [ "$http_code" = "200" ] || [ "$http_code" = "401" ]; then
echo -e "${GREEN}${NC} Qdrant (6333) - ${ms}ms"
record_service "qdrant" "up" "$ms" ""
return 0
else
echo -e "${RED}${NC} Qdrant (6333) - HTTP $http_code"
record_service "qdrant" "down" "0" "HTTP $http_code"
return 1
fi
}
# 檢查 MongoDB
check_mongodb() {
local start=$(date +%s%N)
if mongosh --quiet --eval "db.adminCommand('ping')" > /dev/null 2>&1; then
local end=$(date +%s%N)
local ms=$(( (end - start) / 1000000 ))
echo -e "${GREEN}${NC} MongoDB (27017) - ${ms}ms"
record_service "mongodb" "up" "$ms" ""
return 0
else
echo -e "${RED}${NC} MongoDB (27017) - Down"
record_service "mongodb" "down" "0" "Connection failed"
return 1
fi
}
# 檢查 PHP-FPM
check_php() {
if pgrep -f "php-fpm" > /dev/null 2>&1; then
echo -e "${GREEN}${NC} PHP-FPM - Running"
record_service "php" "up" "1" ""
return 0
else
echo -e "${RED}${NC} PHP-FPM - Not running"
record_service "php" "down" "0" "Process not found"
return 1
fi
}
# 檢查 RustDesk
check_rustdesk() {
local hbbs_ok=false
local hbbr_ok=false
if nc -z localhost 21116 > /dev/null 2>&1; then
hbbs_ok=true
fi
if nc -z localhost 21117 > /dev/null 2>&1; then
hbbr_ok=true
fi
if $hbbs_ok && $hbbr_ok; then
echo -e "${GREEN}${NC} RustDesk (21116/21117) - Running"
record_service "rustdesk" "up" "1" ""
return 0
else
echo -e "${YELLOW}${NC} RustDesk - Partial (hbbs: $hbbs_ok, hbbr: $hbbr_ok)"
record_service "rustdesk" "degraded" "0" "hbbs:$hbbs_ok hbbr:$hbbr_ok"
return 1
fi
}
# 檢查 Node.js 版本
check_node() {
local LOCKED_NODE_VERSION="22"
local version_issues=0
local node_pids=$(pgrep -f "n8n" 2>/dev/null)
if [ -z "$node_pids" ]; then
echo -e "${YELLOW}${NC} Node.js - n8n not running"
record_service "node" "degraded" "1" "n8n not running"
return 1
fi
for pid in $node_pids; do
local node_path=$(lsof -p $pid 2>/dev/null | grep "txt" | grep "node" | head -1 | awk '{print $NF}' | grep -v "dylib")
if [ -n "$node_path" ] && [ -f "$node_path" ]; then
local node_version=$($node_path --version 2>/dev/null | sed 's/v//')
local node_major=$(echo "$node_version" | cut -d. -f1)
if [ "$node_major" != "$LOCKED_NODE_VERSION" ]; then
version_issues=$((version_issues + 1))
fi
fi
done
if [ $version_issues -gt 0 ]; then
echo -e "${RED}${NC} Node.js - Version issues detected"
record_service "node" "degraded" "1" "$version_issues version issues"
return 1
else
echo -e "${GREEN}${NC} Node.js (${LOCKED_NODE_VERSION}.x) - Running"
record_service "node" "up" "1" ""
return 0
fi
}
# 檢查 Python 版本
check_python() {
local LOCKED_PYTHON_VERSION="3.11.14"
local script_issues=0
local scripts=(
"/Users/accusys/momentry_core_0.1/scripts/asr_processor.py"
"/Users/accusys/momentry_core_0.1/scripts/thumbnail_extractor.py"
)
for script in "${scripts[@]}"; do
if [ -f "$script" ]; then
local shebang=$(head -1 "$script")
if [[ "$shebang" != *"python3.11"* ]]; then
script_issues=$((script_issues + 1))
fi
fi
done
if [ $script_issues -gt 0 ]; then
echo -e "${RED}${NC} Python - Script version issues"
record_service "python" "degraded" "1" "$script_issues script issues"
return 1
else
echo -e "${GREEN}${NC} Python (${LOCKED_PYTHON_VERSION}) - Configured"
record_service "python" "up" "1" ""
return 0
fi
}
# 主程序
echo "========================================"
echo "Layer 2: Service Health Check"
echo "Time: $(date)"
echo "========================================"
echo ""
total=0
passed=0
total=$((total + 1))
check_postgresql && passed=$((passed + 1))
total=$((total + 1))
check_redis && passed=$((passed + 1))
total=$((total + 1))
check_mariadb && passed=$((passed + 1))
total=$((total + 1))
check_n8n && passed=$((passed + 1))
total=$((total + 1))
check_caddy && passed=$((passed + 1))
total=$((total + 1))
check_gitea && passed=$((passed + 1))
total=$((total + 1))
check_sftpgo && passed=$((passed + 1))
total=$((total + 1))
check_ollama && passed=$((passed + 1))
total=$((total + 1))
check_qdrant && passed=$((passed + 1))
total=$((total + 1))
check_mongodb && passed=$((passed + 1))
total=$((total + 1))
check_php && passed=$((passed + 1))
total=$((total + 1))
check_rustdesk && passed=$((passed + 1))
total=$((total + 1))
check_node && passed=$((passed + 1))
total=$((total + 1))
check_python && passed=$((passed + 1))
echo ""
echo "========================================"
echo "Result: $passed / $total services healthy"
echo "========================================"
log "Service check completed: $passed/$total healthy"

270
monitor/service/node_monitor.sh Executable file
View File

@@ -0,0 +1,270 @@
#!/bin/bash
#===============================================================================
# Momentry Node.js 監控腳本
# 路徑: /Users/accusys/momentry_core_0.1/monitor/service/node_monitor.sh
#
# 監控重點:
# - n8n 使用的 Node.js 版本鎖定 (22.x)
# - 進程數量與狀態
# - 資源使用情況
#
# 使用方式:
# ./node_monitor.sh status # 顯示監控狀態
# ./node_monitor.sh baseline # 建立版本基線
# ./node_monitor.sh check # 檢查版本變化
#===============================================================================
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
MONITOR_DIR="$(dirname "$SCRIPT_DIR")"
LOG_DIR="/Users/accusys/momentry/log/monitor"
mkdir -p "$LOG_DIR"
LOG_FILE="$LOG_DIR/node_check.log"
# 顏色定義
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m'
# 鎖定的 Node.js 版本
LOCKED_NODE_VERSION="22"
LOCKED_NODE_MINOR="22"
#===============================================================================
# 記錄函數
#===============================================================================
log() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" | tee -a "$LOG_FILE"
}
log_success() {
echo -e "${GREEN}[$(date '+%Y-%m-%d %H:%M:%S')] ✅ $1${NC}" | tee -a "$LOG_FILE"
}
log_error() {
echo -e "${RED}[$(date '+%Y-%m-%d %H:%M:%S')] ❌ $1${NC}" | tee -a "$LOG_FILE"
}
log_warn() {
echo -e "${YELLOW}[$(date '+%Y-%m-%d %H:%M:%S')] ⚠️ $1${NC}" | tee -a "$LOG_FILE"
}
#===============================================================================
# 記錄到資料庫
#===============================================================================
record_node_baseline() {
local runtime_name=$1
local current_version=$2
local process_path=$3
local pid=$4
local required_version="${LOCKED_NODE_VERSION}.x"
local is_compliant="false"
if [[ "$current_version" == "${LOCKED_NODE_VERSION}".* ]]; then
is_compliant="true"
fi
psql -U accusys -h localhost -d momentry << EOF 2>/dev/null
INSERT INTO node_version_baseline (runtime_name, required_version, current_version, process_name, process_path, is_compliant, locked_path, checked_at)
VALUES ('$runtime_name', '$required_version', '$current_version', 'node', '$process_path', $is_compliant, '$process_path', NOW())
ON CONFLICT DO NOTHING;
EOF
}
record_node_history() {
local process_name=$1
local old_version=$2
local new_version=$3
local old_path=$4
local new_path=$5
# node_version_history table does not exist - skip recording
true
}
record_monitor_service() {
local service=$1
local status=$2
local version=$3
local error_msg=$4
psql -U accusys -h localhost -d momentry << EOF 2>/dev/null
INSERT INTO monitor_services (service_name, service_type, status, response_time_ms, error_message, checked_at)
VALUES ('$service', 'node', '$status', 0, '$version - $error_msg', NOW());
EOF
}
#===============================================================================
# 發現 Node.js 進程
#===============================================================================
discover_node_processes() {
log "發現 Node.js 進程..."
echo ""
echo "========================================"
echo "Node.js 監控狀態"
echo "時間: $(date)"
echo "========================================"
echo ""
# 獲取所有 node 進程
local node_pids=$(pgrep -f "node" 2>/dev/null)
if [ -z "$node_pids" ]; then
echo -e "${RED}沒有運行中的 Node.js 進程${NC}"
record_monitor_service "node" "down" "N/A" "No processes"
return 1
fi
echo "鎖定版本: Node.js ${LOCKED_NODE_VERSION}.x (n8n 專用)"
echo ""
echo "----------------------------------------"
echo "發現的 Node.js 進程:"
echo "----------------------------------------"
local total_processes=0
local n8n_processes=0
local version_issues=0
for pid in $node_pids; do
# 獲取進程命令
local cmd=$(ps -o args= -p $pid 2>/dev/null | head -1)
# 獲取 Node.js 版本
local node_path=$(lsof -p $pid 2>/dev/null | grep "txt" | grep "node" | head -1 | awk '{print $NF}' | grep -v "dylib")
if [ -n "$node_path" ] && [ -f "$node_path" ]; then
local node_version=$($node_path --version 2>/dev/null | sed 's/v//')
local node_major=$(echo "$node_version" | cut -d. -f1)
local node_minor=$(echo "$node_version" | cut -d. -f2)
else
local node_version="unknown"
local node_major="unknown"
fi
# 內存使用
local mem=$(ps -o rss= -p $pid 2>/dev/null | awk '{print int($1/1024)}')
# CPU 使用
local cpu=$(ps -o %cpu= -p $pid 2>/dev/null | awk '{print int($1)}')
# 運行時間
local time=$(ps -o etime= -p $pid 2>/dev/null | tr -d ' ')
# 識別服務類型
local service_type="other"
if echo "$cmd" | grep -q "n8n"; then
service_type="n8n"
n8n_processes=$((n8n_processes + 1))
elif echo "$cmd" | grep -q "worker"; then
service_type="n8n-worker"
n8n_processes=$((n8n_processes + 1))
fi
# 版本檢查
local version_status="✅"
if [ "$service_type" = "n8n" ] || [ "$service_type" = "n8n-worker" ]; then
if [ "$node_major" != "$LOCKED_NODE_VERSION" ]; then
version_status="❌ 版本錯誤!"
version_issues=$((version_issues + 1))
log_error "n8n 使用 Node.js $node_version (應為 ${LOCKED_NODE_VERSION}.x)"
fi
fi
echo " PID: $pid"
echo " 命令: ${cmd:0:60}..."
echo " Node.js: $node_version $version_status"
echo " 路徑: $node_path"
echo " 內存: ${mem}MB | CPU: ${cpu}% | 運行: $time"
echo " 類型: $service_type"
echo ""
total_processes=$((total_processes + 1))
# 記錄基線
record_node_baseline "$service_type" "$node_version" "$node_path" "$pid"
done
echo "----------------------------------------"
echo "總結:"
echo " 總進程數: $total_processes"
echo " n8n 相關: $n8n_processes"
echo " 版本問題: $version_issues"
echo "========================================"
# 記錄到資料庫
if [ $version_issues -gt 0 ]; then
record_monitor_service "node" "degraded" "${LOCKED_NODE_VERSION}.x" "$version_issues version issues"
return 1
else
record_monitor_service "node" "up" "${LOCKED_NODE_VERSION}.x" "OK"
return 0
fi
}
#===============================================================================
# 版本基線檢查
#===============================================================================
check_baseline() {
log "檢查 Node.js 版本基線..."
# 檢查 n8n 進程
local n8n_pid=$(pgrep -f "n8n start" | head -1)
if [ -z "$n8n_pid" ]; then
log_error "n8n 進程未運行"
return 1
fi
# 獲取 n8n 使用的 Node.js 版本
local node_path=$(lsof -p $n8n_pid 2>/dev/null | grep "txt" | grep "node" | head -1 | awk '{print $NF}' | grep -v "dylib")
if [ -n "$node_path" ] && [ -f "$node_path" ]; then
local node_version=$($node_path --version 2>/dev/null | sed 's/v//')
local node_major=$(echo "$node_version" | cut -d. -f1)
echo "n8n 當前 Node.js 版本: $node_version"
if [ "$node_major" = "$LOCKED_NODE_VERSION" ]; then
log_success "版本正確: Node.js $node_version"
return 0
else
log_error "版本錯誤: Node.js $node_version (應為 ${LOCKED_NODE_VERSION}.x)"
return 1
fi
else
log_error "無法確定 Node.js 版本"
return 1
fi
}
#===============================================================================
# 顯示狀態
#===============================================================================
show_status() {
discover_node_processes
}
#===============================================================================
# 主程序
#===============================================================================
command=${1:-status}
case $command in
status|check)
show_status
;;
baseline)
check_baseline
;;
*)
echo "用法: $0 {status|baseline}"
echo ""
echo " status - 顯示 Node.js 監控狀態"
echo " baseline - 檢查版本基線"
exit 1
;;
esac

281
monitor/service/python_monitor.sh Executable file
View File

@@ -0,0 +1,281 @@
#!/bin/bash
#===============================================================================
# Momentry Python 監控腳本
# 路徑: /Users/accusys/momentry_core_0.1/monitor/service/python_monitor.sh
#
# 監控重點:
# - Momentry Python 腳本版本鎖定 (3.11.14)
# - 進程數量與狀態
# - 腳本執行狀態
#
# 使用方式:
# ./python_monitor.sh status # 顯示監控狀態
# ./python_monitor.sh baseline # 建立版本基線
# ./python_monitor.sh check # 檢查版本變化
#===============================================================================
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
MONITOR_DIR="$(dirname "$SCRIPT_DIR")"
LOG_DIR="/Users/accusys/momentry/log/monitor"
mkdir -p "$LOG_DIR"
LOG_FILE="$LOG_DIR/python_check.log"
# 顏色定義
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m'
# 鎖定的 Python 版本
LOCKED_PYTHON_VERSION="3.11.14"
LOCKED_PYTHON_MAJOR="3"
LOCKED_PYTHON_MINOR="11"
# Momentry Python 腳本
MOMENTRY_SCRIPTS=(
"/Users/accusys/momentry_core_0.1/scripts/asr_processor.py"
"/Users/accusys/momentry_core_0.1/scripts/thumbnail_extractor.py"
)
#===============================================================================
# 記錄函數
#===============================================================================
log() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" | tee -a "$LOG_FILE"
}
log_success() {
echo -e "${GREEN}[$(date '+%Y-%m-%d %H:%M:%S')] ✅ $1${NC}" | tee -a "$LOG_FILE"
}
log_error() {
echo -e "${RED}[$(date '+%Y-%m-%d %H:%M:%S')] ❌ $1${NC}" | tee -a "$LOG_FILE"
}
log_warn() {
echo -e "${YELLOW}[$(date '+%Y-%m-%d %H:%M:%S')] ⚠️ $1${NC}" | tee -a "$LOG_FILE"
}
#===============================================================================
# 記錄到資料庫
#===============================================================================
record_python_baseline() {
local runtime_name=$1
local current_version=$2
local interpreter_path=$3
local required_version="${LOCKED_PYTHON_VERSION}"
local is_compliant="false"
if [[ "$current_version" == "${LOCKED_PYTHON_VERSION}" ]]; then
is_compliant="true"
fi
psql -U accusys -h localhost -d momentry << EOF 2>/dev/null
INSERT INTO python_version_baseline (runtime_name, required_version, current_version, interpreter_path, is_compliant, checked_at)
VALUES ('$runtime_name', '$required_version', '$current_version', '$interpreter_path', $is_compliant, NOW())
ON CONFLICT DO NOTHING;
EOF
}
record_python_history() {
local script_name=$1
local old_version=$2
local new_version=$3
local old_path=$4
local new_path=$5
# python_version_history table does not exist - skip recording
true
}
record_monitor_service() {
local service=$1
local status=$2
local version=$3
local error_msg=$4
psql -U accusys -h localhost -d momentry << EOF 2>/dev/null
INSERT INTO monitor_services (service_name, service_type, status, response_time_ms, error_message, checked_at)
VALUES ('$service', 'python', '$status', 0, '$version - $error_msg', NOW());
EOF
}
#===============================================================================
# 發現 Python 進程
#===============================================================================
discover_python_processes() {
log "發現 Python 進程..."
echo ""
echo "========================================"
echo "Python 監控狀態"
echo "時間: $(date)"
echo "========================================"
echo ""
echo "鎖定版本: Python ${LOCKED_PYTHON_VERSION} (Momentry 專用)"
echo ""
# 檢查 Momentry 腳本
echo "----------------------------------------"
echo "Momentry Python 腳本:"
echo "----------------------------------------"
local script_issues=0
for script in "${MOMENTRY_SCRIPTS[@]}"; do
if [ -f "$script" ]; then
# 獲取腳本使用的 Python
local shebang=$(head -1 "$script")
local python_path=""
if [[ "$shebang" == *"/python3.11"* ]]; then
python_path="/opt/homebrew/bin/python3.11"
elif [[ "$shebang" == *"/python3"* ]]; then
# 檢查系統 python3
python_path=$(which python3 2>/dev/null)
fi
if [ -n "$python_path" ] && [ -f "$python_path" ]; then
local python_version=$($python_path --version 2>&1 | sed 's/Python //')
local python_major=$(echo "$python_version" | cut -d. -f1)
local python_minor=$(echo "$python_version" | cut -d. -f2)
# 檢查版本
local version_status="✅"
if [ "$python_major" = "$LOCKED_PYTHON_MAJOR" ] && [ "$python_minor" = "$LOCKED_PYTHON_MINOR" ]; then
log_success "$(basename $script): $python_version"
else
version_status="❌ 版本錯誤!"
script_issues=$((script_issues + 1))
log_error "$(basename $script): $python_version (應為 ${LOCKED_PYTHON_VERSION})"
fi
echo " $(basename $script)"
echo " 路徑: $python_path"
echo " 版本: $python_version $version_status"
echo " shebang: $shebang"
echo ""
# 記錄基線
record_python_baseline "python_${LOCKED_PYTHON_VERSION}" "$python_version" "$python_path"
else
log_error "$(basename $script): 無法確定 Python 路徑"
script_issues=$((script_issues + 1))
fi
else
log_warn "$(basename $script): 文件不存在"
script_issues=$((script_issues + 1))
fi
done
# 檢查運行中的 Python 進程
echo "----------------------------------------"
echo "運行中的 Python 進程:"
echo "----------------------------------------"
local python_pids=$(pgrep -f "python" 2>/dev/null)
local total_processes=0
if [ -n "$python_pids" ]; then
for pid in $python_pids; do
# 獲取進程命令
local cmd=$(ps -o args= -p $pid 2>/dev/null | head -1 | cut -c1-80)
# 獲取 Python 路徑
local python_path=$(lsof -p $pid 2>/dev/null | grep "txt" | grep "Python" | head -1 | awk '{print $NF}' | grep -v "dylib")
if [ -n "$python_path" ] && [ -f "$python_path" ]; then
local python_version=$($python_path --version 2>&1 | sed 's/Python //')
else
local python_version="unknown"
fi
# 內存使用
local mem=$(ps -o rss= -p $pid 2>/dev/null | awk '{print int($1/1024)}')
echo " PID $pid: $cmd"
echo " Python: $python_version"
echo " 內存: ${mem}MB"
echo ""
total_processes=$((total_processes + 1))
done
else
echo " (無運行中的 Python 進程)"
fi
echo "----------------------------------------"
echo "總結:"
echo " 總進程數: $total_processes"
echo " 腳本問題: $script_issues"
echo "========================================"
# 記錄到資料庫
if [ $script_issues -gt 0 ]; then
record_monitor_service "python" "degraded" "${LOCKED_PYTHON_VERSION}" "$script_issues issues"
return 1
else
record_monitor_service "python" "up" "${LOCKED_PYTHON_VERSION}" "OK"
return 0
fi
}
#===============================================================================
# 版本基線檢查
#===============================================================================
check_baseline() {
log "檢查 Python 版本基線..."
local script_issues=0
for script in "${MOMENTRY_SCRIPTS[@]}"; do
if [ -f "$script" ]; then
local shebang=$(head -1 "$script")
if [[ "$shebang" == *"/python3.11"* ]]; then
log_success "$(basename $script): 使用正確版本"
else
log_error "$(basename $script): 未使用 python3.11"
script_issues=$((script_issues + 1))
fi
fi
done
if [ $script_issues -gt 0 ]; then
return 1
else
return 0
fi
}
#===============================================================================
# 顯示狀態
#===============================================================================
show_status() {
discover_python_processes
}
#===============================================================================
# 主程序
#===============================================================================
command=${1:-status}
case $command in
status|check)
show_status
;;
baseline)
check_baseline
;;
*)
echo "用法: $0 {status|baseline}"
echo ""
echo " status - 顯示 Python 監控狀態"
echo " baseline - 檢查版本基線"
exit 1
;;
esac

375
monitor/storage/backup_monitor.sh Executable file
View File

@@ -0,0 +1,375 @@
#!/bin/bash
# Momentry 備份監控與溫冷轉移
# 路徑: /Users/accusys/momentry_core_0.1/monitor/storage/backup_monitor.sh
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
LOG_DIR="/Users/accusys/momentry/log/monitor"
mkdir -p "$LOG_DIR"
LOG_FILE="$LOG_DIR/backup_check.log"
log() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" | tee -a "$LOG_FILE"
}
# 備份根目錄
BACKUP_BASE="/Users/accusys/momentry/backup"
# 服務列表
SERVICES=("postgresql" "redis" "mariadb" "n8n" "qdrant" "gitea" "ollama" "caddy" "mongodb" "sftpgo" "php")
# 溫冷分層配置
TIER_HOT=7 # 7天內 - 快速存儲
TIER_WARM=30 # 7-30天 - 標準存儲
TIER_COLD=90 # 30-90天 - 低成本存儲
TIER_ARCHIVE=365 # >90天 - 歸檔
# 記錄備份元數據
record_backup() {
local service=$1
local backup_file=$2
local backup_size=$3
local backup_type=$4
psql -U accusys -h localhost -d momentry << EOF 2>/dev/null
INSERT INTO backup_registry (service_name, backup_file, backup_size_bytes, backup_type, status, created_at)
VALUES ('$service', '$backup_file', $backup_size, '$backup_type', 'completed', NOW())
ON CONFLICT DO NOTHING;
EOF
}
# 記錄備份存儲統計
record_backup_stats() {
local tier=$1
local file_count=$2
local total_size=$3
psql -U accusys -h localhost -d momentry << EOF 2>/dev/null
INSERT INTO backup_storage_stats (tier, file_count, total_size_bytes, record_time)
VALUES ('$tier', $file_count, $total_size, NOW());
EOF
}
# 初始化備份目錄結構
init_backup_dirs() {
log "初始化備份目錄結構..."
mkdir -p "$BACKUP_BASE"/{daily,weekly,monthly,archive}
for service in "${SERVICES[@]}"; do
mkdir -p "$BACKUP_BASE/daily/$service"
mkdir -p "$BACKUP_BASE/weekly/$service"
mkdir -p "$BACKUP_BASE/monthly/$service"
done
log "備份目錄結構已初始化"
}
# 檢查備份狀態
check_backup_status() {
log "=== 檢查備份狀態 ==="
# 命名規範: {service}_{type}_{YYYYMMDD}_{HHMMSS}.{ext}
# 例如: postgresql_db_20260315_030000.sql.gz
local total_backup_size=0
echo ""
echo "========================================"
echo "備份監控狀態"
echo "時間: $(date)"
echo "========================================"
echo ""
echo "命名規範: {service}_{type}_{YYYYMMDD}_{HHMMSS}.{ext}"
echo ""
for service in "${SERVICES[@]}"; do
service_backup_dir="$BACKUP_BASE/daily/$service"
if [ -d "$service_backup_dir" ]; then
file_count=$(find "$service_backup_dir" -type f 2>/dev/null | wc -l)
size=$(du -sb "$service_backup_dir" 2>/dev/null | cut -f1)
latest_file=$(find "$service_backup_dir" -type f \( -name "*.tar.gz" -o -name "*.sql.gz" -o -name "*.rdb" \) 2>/dev/null | head -1)
# 處理 size 為空或 0 的情況
if [ -z "$size" ] || [ "$size" = "0" ]; then
size=$(find "$service_backup_dir" -type f -exec ls -l {} \; 2>/dev/null | awk '{sum+=$5} END {print sum}')
fi
size_str="0B"
if [ -n "$size" ] && [ "$size" -gt 0 ]; then
if [ "$size" -gt 1073741824 ]; then
size_str="$((size / 1073741824))GB"
elif [ "$size" -gt 1048576 ]; then
size_str="$((size / 1048576))MB"
elif [ "$size" -gt 1024 ]; then
size_str="$((size / 1024))KB"
else
size_str="${size}B"
fi
fi
# 檢查最近備份時間 (使用文件名中的時間戳)
days_since_backup=0
today=$(date +%Y%m%d)
if [ -n "$latest_file" ]; then
# 從文件名提取日期
file_date=$(echo "$latest_file" | sed 's/.*\([0-9]\{8\}\).*/\1/')
if [ -n "$file_date" ] && [ "$file_date" = "$today" ]; then
days_since_backup=0
else
days_since_backup=1
fi
fi
# 狀態指示
if [ "$days_since_backup" -eq 0 ]; then
status="✅ 今日已備份"
elif [ "$days_since_backup" -le 1 ]; then
status="⚠️ 昨日已備份"
elif [ "$days_since_backup" -le 7 ]; then
status="⚠️ ${days_since_backup}天前"
else
status="❌ 超過${days_since_backup}天未備份!"
fi
echo " $service: $file_count 個文件, $size_str | $status"
[ -n "$size" ] && total_backup_size=$((total_backup_size + size))
# 記錄到資料庫
[ -n "$size" ] && record_backup "$service" "$service_backup_dir" "$size" "daily"
else
echo " $service: ❌ 備份目錄不存在"
fi
done
echo ""
echo "----------------------------------------"
echo "存儲分層:"
echo "----------------------------------------"
for tier in daily weekly monthly archive; do
tier_path="$BACKUP_BASE/$tier"
if [ -d "$tier_path" ]; then
file_count=$(find "$tier_path" -type f 2>/dev/null | wc -l)
size=$(du -sb "$tier_path" 2>/dev/null | cut -f1)
tier_size_str="0B"
if [ -n "$size" ] && [ "$size" -gt 0 ] 2>/dev/null; then
if [ "$size" -gt 1073741824 ]; then
tier_size_str="$((size / 1073741824))GB"
elif [ "$size" -gt 1048576 ]; then
tier_size_str="$((size / 1048576))MB"
else
tier_size_str="$((size / 1024))KB"
fi
fi
echo " $tier: $file_count 個文件, $tier_size_str"
[ -n "$size" ] && record_backup_stats "$tier" "$file_count" "$size"
fi
done
total_size_str="0B"
if [ "$total_backup_size" -gt 1073741824 ]; then
total_size_str="$((total_backup_size / 1073741824))GB"
elif [ "$total_backup_size" -gt 1048576 ]; then
total_size_str="$((total_backup_size / 1048576))MB"
elif [ "$total_backup_size" -gt 1024 ]; then
total_size_str="$((total_backup_size / 1024))KB"
else
total_size_str="${total_backup_size}B"
fi
echo ""
echo "----------------------------------------"
echo "總計: ${total_backup_size} bytes ($total_size_str)"
echo "========================================"
# 記錄總計
record_backup_stats "total" 0 "$total_backup_size"
}
# 溫冷轉移 - 將舊備份移動到低成本存儲
tier_backups() {
log "執行溫冷轉移..."
local moved_count=0
# 7天前: daily -> weekly
# 命名格式: {service}_{type}_{YYYYMMDD}_{HHMMSS}.{ext}
find "$BACKUP_BASE/daily" -type f -mtime +7 | while read -r file; do
service=$(basename "$(dirname "$file")")
# 解析時間戳
filename=$(basename "$file")
timestamp=$(echo "$filename" | grep -oP '\d{8}_\d{6}' || echo "")
if [ -n "$timestamp" ]; then
year=${timestamp:0:4}
week=$(date -j -f "%Y%m%d_%H%M%S" "${timestamp}_0000" +%Y-W%V 2>/dev/null || echo "$year-W$(date +%V)")
else
week=$(date +%Y-W%V)
fi
dest_dir="$BACKUP_BASE/weekly/$service/$week"
mkdir -p "$dest_dir"
mv "$file" "$dest_dir/" 2>/dev/null && log "移動: $file -> $dest_dir" && moved_count=$((moved_count + 1))
done
# 30天前: weekly -> monthly
find "$BACKUP_BASE/weekly" -type f -mtime +30 | while read -r file; do
service=$(basename "$(dirname "$(dirname "$file")")")
month=$(date +%Y-%m)
dest_dir="$BACKUP_BASE/monthly/$service/$month"
mkdir -p "$dest_dir"
mv "$file" "$dest_dir/" 2>/dev/null && log "移動: $file -> $dest_dir" && moved_count=$((moved_count + 1))
done
# 90天前: monthly -> archive (長期歸檔)
find "$BACKUP_BASE/monthly" -type f -mtime +90 | while read -r file; do
service=$(basename "$(dirname "$(dirname "$file")")")
year=$(date +%Y)
dest_dir="$BACKUP_BASE/archive/$service/$year"
mkdir -p "$dest_dir"
mv "$file" "$dest_dir/" 2>/dev/null && log "歸檔: $file -> $dest_dir" && moved_count=$((moved_count + 1))
done
log "溫冷轉移完成: 移動了 $moved_count 個文件"
}
# 清理過期備份
cleanup_old() {
log "清理過期備份..."
# 歸檔超過 365 天
find "$BACKUP_BASE/archive" -type f -mtime +365 -delete 2>/dev/null
# 每月備份保留 12 個月
find "$BACKUP_BASE/monthly" -type f -mtime +365 -delete 2>/dev/null
# 每週備份保留 12 週
find "$BACKUP_BASE/weekly" -type f -mtime +84 -delete 2>/dev/null
# 每日備份保留 30 天
find "$BACKUP_BASE/daily" -type f -mtime +30 -delete 2>/dev/null
log "清理完成"
}
# 驗證備份完整性
verify_backup() {
local backup_file=$1
if [[ "$backup_file" == *.tar.gz ]]; then
tar -tzf "$backup_file" > /dev/null 2>&1
return $?
elif [[ "$backup_file" == *.sql ]]; then
head -1 "$backup_file" | grep -q "SQL" && return 0
return 1
elif [[ "$backup_file" == *.rdb ]]; then
file "$backup_file" | grep -q "data" && return 0
return 1
fi
return 0
}
# 生成備份報告
generate_report() {
local report_file="/Users/accusys/momentry/log/backup_report_$(date +%Y%m%d).txt"
{
echo "========================================"
echo "Momentry 備份報告"
echo "生成時間: $(date)"
echo "========================================"
echo ""
echo "## 備份狀態"
check_backup_status
echo ""
echo "## 存儲使用趨勢 (最近30天)"
psql -U accusys -h localhost -d momentry -t -A -c "
SELECT tier,
COUNT(*) as files,
AVG(total_size_bytes)::bigint as avg_size,
MAX(total_size_bytes)::bigint as max_size
FROM backup_storage_stats
WHERE record_time > NOW() - INTERVAL '30 days'
GROUP BY tier
ORDER BY tier;
" 2>/dev/null || echo " (無數據)"
echo ""
echo "## 建議"
# 檢查是否有服務超過7天未備份
for service in "${SERVICES[@]}"; do
latest=$(find "$BACKUP_BASE/daily/$service" -type f 2>/dev/null | head -1)
if [ -n "$latest" ]; then
days_old=$(($(date +%s) - $(stat -f "%m" "$latest" 2>/dev/null || echo "0")) / 86400)
if [ "$days_old" -gt 7 ]; then
echo " - ⚠️ $service 超過 $days_old 天未備份,建議立即執行備份"
fi
fi
done
} > "$report_file"
log "報告已生成: $report_file"
echo "$report_file"
}
# 主程序
command=${1:-status}
case $command in
status)
check_backup_status
;;
init)
init_backup_dirs
;;
tier)
tier_backups
;;
cleanup)
cleanup_old
;;
verify)
verify_backup "${2:-}"
;;
report)
generate_report
;;
all)
log "執行完整備份維護..."
check_backup_status
tier_backups
cleanup_old
generate_report
log "備份維護完成"
;;
*)
echo "用法: $0 {status|init|tier|cleanup|verify|report|all}"
echo ""
echo " status - 檢查備份狀態"
echo " init - 初始化備份目錄"
echo " tier - 執行溫冷轉移"
echo " cleanup - 清理過期備份"
echo " verify - 驗證備份完整性"
echo " report - 生成備份報告"
echo " all - 執行所有維護任務"
exit 1
;;
esac

View File

@@ -0,0 +1,193 @@
#!/bin/bash
# Momentry Storage 管理 (Layer 7)
# 路徑: /Users/accusys/momentry_core_0.1/monitor/storage/storage_manager.sh
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
LOG_DIR="/Users/accusys/momentry/log/monitor"
mkdir -p "$LOG_DIR"
LOG_FILE="$LOG_DIR/storage_check.log"
log() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" | tee -a "$LOG_FILE"
}
# 存儲路徑配置
STORAGE_BASE="/Users/accusys/momentry"
DATA_DIR="$STORAGE_BASE/data"
TEMP_DIR="$STORAGE_BASE/tmp"
BACKUP_DIR="$STORAGE_BASE/backup"
# 用戶集群
CLUSTERS=("family" "work" "wordpress" "shared")
# 記錄使用統計
record_usage() {
local cluster=$1
local tier=$2
local file_count=$3
local total_size=$4
psql -U accusys -h localhost -d momentry << EOF 2>/dev/null
INSERT INTO storage_usage_stats (user_cluster, storage_tier, file_count, total_size_bytes, record_time)
VALUES ('$cluster', '$tier', $file_count, $total_size, NOW());
EOF
}
# 記錄檔案註冊
register_file() {
local file_path=$1
local user_cluster=$2
local file_size=$3
file_name=$(basename "$file_path")
file_hash=$(echo "$file_path" | md5sum | cut -d' ' -f1)
psql -U accusys -h localhost -d momentry << EOF 2>/dev/null
INSERT INTO file_registry (file_name, file_path, file_path_hash, file_size, user_cluster, storage_tier, status, created_at)
VALUES ('$file_name', '$file_path', '$file_hash', $file_size, '$user_cluster', 'hot', 'active', NOW())
ON CONFLICT (file_path_hash) DO UPDATE SET
last_accessed_at = NOW(),
access_count = file_registry.access_count + 1;
EOF
}
# 初始化目錄結構
init_directories() {
echo "初始化目錄結構..."
# 主目錄
mkdir -p "$DATA_DIR"
mkdir -p "$TEMP_DIR"
mkdir -p "$BACKUP_DIR"/{daily,weekly,monthly,archive}
# 用戶集群目錄
for cluster in "${CLUSTERS[@]}"; do
mkdir -p "$DATA_DIR/$cluster"
done
# 臨時子目錄
mkdir -p "$TEMP_DIR"/{upload,processing,cache,session}
echo "目錄結構已初始化"
}
# 顯示存儲狀態
show_status() {
echo "========================================"
echo "Layer 7: Storage Status"
echo "Time: $(date)"
echo "========================================"
echo ""
echo "存儲路徑:"
echo " 數據: $DATA_DIR"
echo " 臨時: $TEMP_DIR"
echo " 備份: $BACKUP_DIR"
echo ""
echo "用戶集群:"
echo "----------------------------------------"
for cluster in "${CLUSTERS[@]}"; do
cluster_path="$DATA_DIR/$cluster"
if [ -d "$cluster_path" ]; then
file_count=$(find "$cluster_path" -type f 2>/dev/null | wc -l)
total_size=$(du -sb "$cluster_path" 2>/dev/null | cut -f1)
size_str="0B"
if [ -n "$total_size" ] && [ "$total_size" -gt 0 ] 2>/dev/null; then
if [ "$total_size" -gt 1073741824 ]; then
size_str="$((total_size / 1073741824))GB"
elif [ "$total_size" -gt 1048576 ]; then
size_str="$((total_size / 1048576))MB"
elif [ "$total_size" -gt 1024 ]; then
size_str="$((total_size / 1024))KB"
else
size_str="${total_size}B"
fi
fi
echo " $cluster: $file_count files, $size_str"
[ -n "$total_size" ] && record_usage "$cluster" "hot" "$file_count" "$total_size"
else
echo " $cluster: (未創建)"
fi
done
echo ""
echo "臨時文件:"
echo "----------------------------------------"
for subdir in upload processing cache session; do
subdir_path="$TEMP_DIR/$subdir"
if [ -d "$subdir_path" ]; then
file_count=$(find "$subdir_path" -type f 2>/dev/null | wc -l)
size=$(du -sb "$subdir_path" 2>/dev/null | cut -f1)
echo " $subdir: $file_count files"
fi
done
echo ""
echo "備份目錄:"
echo "----------------------------------------"
for subdir in daily weekly monthly archive; do
subdir_path="$BACKUP_DIR/$subdir"
if [ -d "$subdir_path" ]; then
file_count=$(find "$subdir_path" -type f 2>/dev/null | wc -l)
size=$(du -sb "$subdir_path" 2>/dev/null | cut -f1)
echo " $subdir: $file_count files"
fi
done
echo ""
echo "========================================"
# 顯示資料庫統計
echo ""
echo "資料庫統計 (file_registry):"
psql -U accusys -h localhost -d momentry -t -A -c "
SELECT user_cluster, storage_tier, COUNT(*) as files,
SUM(file_size)::bigint as total_size
FROM file_registry
WHERE status = 'active'
GROUP BY user_cluster, storage_tier;
" 2>/dev/null || echo " (表未初始化)"
}
# 清理臨時文件
clean_temp() {
echo "清理臨時文件..."
# 清理超過 7 天的上傳
find "$TEMP_DIR/upload" -type f -mtime +7 -delete 2>/dev/null
# 清理超過 30 天的緩存
find "$TEMP_DIR/cache" -type f -mtime +30 -delete 2>/dev/null
# 清理超過 7 天的處理中
find "$TEMP_DIR/processing" -type f -mtime +7 -delete 2>/dev/null
echo "清理完成"
}
# 主程序
command=${1:-status}
case $command in
status)
show_status
;;
init)
init_directories
;;
clean)
clean_temp
;;
*)
echo "用法: $0 {status|init|clean}"
exit 1
;;
esac

163
monitor/users/session_tracker.sh Executable file
View File

@@ -0,0 +1,163 @@
#!/bin/bash
# Momentry 使用者會話追蹤 (Layer 6)
# 路徑: /Users/accusys/momentry_core_0.1/monitor/users/session_tracker.sh
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
LOG_DIR="/Users/accusys/momentry/log/monitor"
mkdir -p "$LOG_DIR"
LOG_FILE="$LOG_DIR/session_check.log"
log() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" | tee -a "$LOG_FILE"
}
# 記錄會話
record_session() {
local session_type=$1
local service=$2
local username=$3
local source_ip=$4
local status=$5
psql -U accusys -h localhost -d momentry << EOF 2>/dev/null
INSERT INTO monitor_sessions (session_type, service_name, username, source_ip, connected_at, status)
VALUES ('$session_type', '$service', '$username', '$source_ip', NOW(), '$status');
EOF
}
# 記錄登入
record_login() {
local user_type=$1
local username=$2
local source_ip=$3
local success=$4
local method=$5
psql -U accusys -h localhost -d momentry << EOF 2>/dev/null
INSERT INTO monitor_logins (user_type, username, source_ip, success, login_method, login_at)
VALUES ('$user_type', '$username', '$source_ip', $success, '$method', NOW());
EOF
}
# 記錄異常
record_anomaly() {
local anomaly_type=$1
local severity=$2
local username=$3
local source_ip=$4
local description=$5
psql -U accusys -h localhost -d momentry << EOF 2>/dev/null
INSERT INTO monitor_anomalies (anomaly_type, severity, source_type, username, source_ip, description, detected_at)
VALUES ('$anomaly_type', '$severity', 'system', '$username', '$source_ip', '$description', NOW());
EOF
}
# SSH 會話
track_ssh() {
echo "SSH 會話:"
# 獲取當前 SSH 連線
who | grep -E "pts|tty" | while read -r line; do
user=$(echo "$line" | awk '{print $1}')
tty=$(echo "$line" | awk '{print $2}')
login_time=$(echo "$line" | awk '{print $3,$4}')
ip=$(echo "$line" | awk '{print $NF}' | tr -d '()')
if [ -n "$ip" ] && [ "$ip" != "-" ]; then
echo " - $user @ $ip (tty $tty) 登入時間: $login_time"
record_session "ssh" "sshd" "$user" "$ip" "active"
fi
done
# 檢查 SSH 登入失敗
echo ""
echo "SSH 登入失敗 (最近 5 分鐘):"
last -5 -f /var/log/auth.log 2>/dev/null | grep -i "failed password" | tail -5 | while read -r line; do
user=$(echo "$line" | awk '{print $9}')
ip=$(echo "$line" | grep -oE '[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+' | tail -1)
if [ -n "$ip" ]; then
echo " - Failed: $user from $ip"
record_login "system" "$user" "$ip" "false" "ssh"
fi
done
}
# Web 服務會話
track_web() {
echo ""
echo "Web 服務:"
# n8n 活躍會話 (如果有認證)
n8n_sessions=0
echo " - n8n: 檢查中... (需要 API key)"
# Gitea 活躍會話
gitea_sessions=0
echo " - Gitea: 檢查中... (需要登入)"
}
# 資料庫連線
track_database() {
echo ""
echo "資料庫連線:"
# PostgreSQL
pg_conn=$(psql -U accusys -h localhost -t -A -c "SELECT count(*) FROM pg_stat_activity WHERE datname = 'momentry';" 2>/dev/null || echo "0")
echo " - PostgreSQL: $pg_conn connections"
# Redis
redis_conn=$(redis-cli -a accusys INFO clients 2>/dev/null | grep "connected_clients" | cut -d: -f2 | tr -d '\r')
echo " - Redis: $redis_conn clients"
}
# SFTP 會話
track_sftp() {
echo ""
echo "SFTP 會話:"
# 檢查 SFTPGo 在線用戶
if nc -z localhost 2222 2>/dev/null; then
echo " - SFTPGo: 檢查中..."
fi
}
# 檢測暴力破解
detect_bruteforce() {
echo ""
echo "異常檢測:"
# 檢查 SSH 暴力破解
now=$(date +%s)
window=300 # 5 分鐘
# 統計最近失敗
fail_count=$(last -f /var/log/auth.log 2>/dev/null | grep -i "failed" | wc -l)
if [ $fail_count -gt 10 ]; then
echo " ⚠️ 發現潛在暴力破解嘗試: $fail_count 次失敗"
record_anomaly "bruteforce" "critical" "unknown" "multiple" "SSH暴力破解: $fail_count 次失敗"
else
echo " ✓ 無明顯暴力破解跡象"
fi
}
# 主程序
echo "========================================"
echo "Layer 6: User Session Tracking"
echo "Time: $(date)"
echo "========================================"
echo ""
track_ssh
track_web
track_database
track_sftp
detect_bruteforce
echo ""
echo "========================================"
log "Session tracking completed"

View File

@@ -0,0 +1,265 @@
#!/bin/bash
# Momentry n8n Workflow 監控 (Layer 3)
# 路徑: /Users/accusys/momentry_core_0.1/monitor/workflow/n8n_workflow_monitor.sh
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
MONITOR_DIR="$(dirname "$SCRIPT_DIR")"
LOG_DIR="/Users/accusys/momentry/log/monitor"
mkdir -p "$LOG_DIR"
LOG_FILE="$LOG_DIR/workflow_check.log"
log() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" | tee -a "$LOG_FILE"
}
# n8n API 配置
N8N_HOST="http://localhost:5678"
N8N_API_KEY="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJlNjdiY2UzOS1iY2RkLTRjMjEtYmMwYy0yODNhYmI3ZjVjMjMiLCJpc3MiOiJuOG4iLCJhdWQiOiJwdWJsaWMtYXBpIiwiaWF0IjoxNzczNjM5ODU4fQ.QOmOju2jLy07GrgXYvylM5AyFINPC06crKEsLLC988I"
# 從資料庫獲取 workflow
fetch_workflows_from_db() {
PGPASSWORD=accusys psql -U n8n -h localhost -d n8n -t -A <<'EOF'
SELECT json_agg(row_to_json(t)) FROM (
SELECT w.id, w.name, w.active, w."createdAt", w."updatedAt",
COALESCE(u.email, 'unknown') as owner_email
FROM workflow_entity w
LEFT JOIN shared_workflow sw ON w.id = sw."workflowId"
LEFT JOIN project p ON sw."projectId" = p.id
LEFT JOIN "user" u ON p."creatorId" = u.id
) t
EOF
}
# 記錄 workflow
record_workflow() {
local wf_id=$1
local wf_name=$2
local active=$3
local last_exec=$4
local exec_count=$5
local success=$6
local failure=$7
local avg_duration=$8
local has_schedule=$9
local has_webhook=${10}
local idle_days=${11}
local suggestion=${12}
psql -U accusys -h localhost -d momentry << EOF 2>/dev/null
INSERT INTO monitor_workflows
(workflow_id, workflow_name, is_active, last_executed_at, execution_count,
success_count, failure_count, avg_duration_ms, has_schedule, has_webhook,
idle_days, suggestion, checked_at)
VALUES
('$wf_id', '$wf_name', $active, $last_exec, $exec_count,
$success, $failure, $avg_duration, $has_schedule, $has_webhook,
$idle_days, '$suggestion', NOW())
ON CONFLICT (workflow_id) DO UPDATE SET
workflow_name = EXCLUDED.workflow_name,
is_active = EXCLUDED.is_active,
last_executed_at = EXCLUDED.last_executed_at,
execution_count = EXCLUDED.execution_count,
success_count = EXCLUDED.success_count,
failure_count = EXCLUDED.failure_count,
avg_duration_ms = EXCLUDED.avg_duration_ms,
has_schedule = EXCLUDED.has_schedule,
has_webhook = EXCLUDED.has_webhook,
idle_days = EXCLUDED.idle_days,
suggestion = EXCLUDED.suggestion,
checked_at = NOW();
EOF
}
# 獲取 workflow 列表
fetch_workflows() {
curl -s -H "Accept: application/json" \
-H "X-N8N-API-KEY: ${N8N_API_KEY}" \
"${N8N_HOST}/rest/workflows" 2>/dev/null || echo "[]"
}
# 獲取 workflow 執行統計
fetch_executions() {
local wf_id=$1
curl -s -H "Accept: application/json" \
-H "X-N8N-API-KEY: ${N8N_API_KEY}" \
"${N8N_HOST}/rest/executions?workflowId=${wf_id}&limit=50" 2>/dev/null || echo "{\"data\":[]}"
}
# 判斷是否有 schedule
has_schedule() {
local wf_data=$1
echo "$wf_data" | grep -q '"type":"schedule"' && echo "true" || echo "false"
}
# 判斷是否有 webhook
has_webhook() {
local wf_data=$1
echo "$wf_data" | grep -q '"type":"webhook"' && echo "true" || echo "false"
}
# 計算閒置天數
calc_idle_days() {
local last_exec=$1
if [ "$last_exec" = "null" ] || [ -z "$last_exec" ]; then
echo "999"
else
echo "0"
fi
}
# 生成建議
generate_suggestion() {
local has_schedule=$1
local has_webhook=$2
local idle_days=$3
local failure_rate=$4
if [ "$idle_days" -ge 90 ]; then
echo "建議刪除"
elif [ "$idle_days" -ge 30 ] && [ "$has_schedule" = "false" ] && [ "$has_webhook" = "false" ]; then
echo "建議停用"
elif [ "$failure_rate" -gt 20 ]; then
echo "建議優化"
else
echo ""
fi
}
# 主程序
echo "========================================"
echo "Layer 3: n8n Workflow Monitoring"
echo "Time: $(date)"
echo "========================================"
echo ""
# 檢查 n8n 是否可用 (檢查 PostgreSQL 中的 n8n 資料庫)
if ! PGPASSWORD=accusys psql -U n8n -h localhost -d n8n -c "SELECT 1" >/dev/null 2>&1; then
echo "n8n 資料庫不可用"
log "n8n database unavailable"
exit 1
fi
# 獲取 workflow 列表 (從資料庫)
workflows=$(fetch_workflows_from_db)
total_count=$(echo "$workflows" | jq 'length' 2>/dev/null || echo "0")
active_count=$(echo "$workflows" | jq '[.[] | select(.active == true)] | length' 2>/dev/null || echo "0")
echo "總 Workflow: $total_count"
echo "啟用中: $active_count"
echo ""
# 閒置閾值
IDLE_THRESHOLD=30
echo "Workflow 詳細:"
echo "----------------------------------------"
total_idle=0
for wf in $(echo "$workflows" | jq -r '.[] | @base64' 2>/dev/null); do
wf_decoded=$(echo "$wf" | base64 -d)
wf_id=$(echo "$wf_decoded" | jq -r '.id' 2>/dev/null)
wf_name=$(echo "$wf_decoded" | jq -r '.name' 2>/dev/null)
is_active=$(echo "$wf_decoded" | jq -r '.active' 2>/dev/null)
wf_owner=$(echo "$wf_decoded" | jq -r '.owner_email' 2>/dev/null)
# 從資料庫獲取執行數據
exec_data=$(PGPASSWORD=accusys psql -U n8n -h localhost -d n8n -t -A <<EOF
SELECT json_agg(row_to_json(t)) FROM (
SELECT status, "startedAt", "stoppedAt",
EXTRACT(EPOCH FROM ("stoppedAt" - "startedAt")) * 1000 as execution_time
FROM execution_entity
WHERE "workflowId" = '$wf_id'
ORDER BY "startedAt" DESC
LIMIT 50
) t
EOF
)
exec_count=$(echo "$exec_data" | jq '. | length' 2>/dev/null || echo "0")
# 計算成功/失敗
success_count=$(echo "$exec_data" | jq '[.[] | select(.status == "success")] | length' 2>/dev/null || echo "0")
failure_count=$(echo "$exec_data" | jq '[.[] | select(.status == "error")] | length' 2>/dev/null || echo "0")
# 平均執行時間
avg_duration=$(echo "$exec_data" | jq '[.[] | .execution_time] | map(select(. != null)) | add / length | floor' 2>/dev/null || echo "0")
# 檢查是否有 webhook
has_webh=$(PGPASSWORD=accusys psql -U n8n -h localhost -d n8n -t -A -c "
SELECT COUNT(*) FROM webhook_entity WHERE workflow_id = '$wf_id'
" 2>/dev/null || echo "0")
[ "$has_webh" -gt 0 ] && has_webh="true" || has_webh="false"
has_sched="false"
# 最後執行時間
last_exec=$(echo "$exec_data" | jq -r '.[0].startedAt // "null"' 2>/dev/null | head -1)
if [ "$last_exec" = "null" ] || [ -z "$last_exec" ]; then
idle_days=999
else
idle_days=0
fi
# 確保數值正確
exec_count=$(echo "$exec_count" | tr -d '[:space:]' || echo "0")
success_count=$(echo "$success_count" | tr -d '[:space:]' || echo "0")
failure_count=$(echo "$failure_count" | tr -d '[:space:]' || echo "0")
avg_duration=$(echo "$avg_duration" | tr -d '[:space:]' || echo "0")
# 計算失敗率
if [ -n "$exec_count" ] && [ "$exec_count" -gt 0 ] 2>/dev/null; then
failure_rate=$(( failure_count * 100 / exec_count ))
else
failure_rate=0
fi
# 生成建議
suggestion=$(generate_suggestion "$has_sched" "$has_webh" "$idle_days" "$failure_rate")
# 記錄到資料庫
if [ "$last_exec" = "null" ] || [ -z "$last_exec" ]; then
record_workflow "$wf_id" "$wf_name" "$is_active" "NULL" "$exec_count" "$success_count" "$failure_count" "$avg_duration" "$has_sched" "$has_webh" "$idle_days" "$suggestion"
else
record_workflow "$wf_id" "$wf_name" "$is_active" "'$last_exec'" "$exec_count" "$success_count" "$failure_count" "$avg_duration" "$has_sched" "$has_webh" "$idle_days" "$suggestion"
fi
# 顯示
status_icon="○"
if [ "$is_active" = "true" ]; then
status_icon="●"
fi
idle_info=""
if [ "$idle_days" -ge "$IDLE_THRESHOLD" ]; then
idle_info=" [閒置 $idle_days 天]"
total_idle=$((total_idle + 1))
fi
suggestion_info=""
if [ -n "$suggestion" ]; then
suggestion_info=" [$suggestion]"
fi
echo "$status_icon $wf_name (ID: $wf_id) [$wf_owner]$idle_info$suggestion_info"
echo " 執行: $exec_count (成功: $success_count, 失敗: $failure_count) | 平均: ${avg_duration}ms"
done
echo "----------------------------------------"
echo "閒置 Workflow (> $IDLE_THRESHOLD 天): $total_idle"
echo ""
log "Workflow check completed: $total_count total, $total_idle idle"
# 顯示閒置 workflow
if [ $total_idle -gt 0 ]; then
echo ""
echo "閒置 Workflow 建議:"
psql -U accusys -h localhost -d momentry -t -A -c "
SELECT ' - ' || workflow_name || ': ' || suggestion
FROM monitor_workflows
WHERE idle_days >= $IDLE_THRESHOLD AND suggestion != '';
" 2>/dev/null
fi

1
requirements.txt Normal file
View File

@@ -0,0 +1 @@
faster-whisper>=1.0.0

3
rustfmt.toml Normal file
View File

@@ -0,0 +1,3 @@
edition = "2021"
max_width = 100
tab_spaces = 4

53
scripts/asr_processor.py Normal file
View File

@@ -0,0 +1,53 @@
#!/opt/homebrew/bin/python3.11
import sys
import json
import tempfile
import os
from faster_whisper import WhisperModel
def run_asr(video_path, output_path):
print(f"ASR_START", file=sys.stderr)
print(f"Loading Whisper model...", file=sys.stderr)
model = WhisperModel("tiny", device="cpu", compute_type="int8")
print(f"Transcribing: {video_path}", file=sys.stderr)
segments, info = model.transcribe(video_path, beam_size=5)
print(f"ASR_LANGUAGE:{info.language}", file=sys.stderr)
print(
f"Detected language: {info.language} (probability: {info.language_probability:.2f})",
file=sys.stderr,
)
results = []
total_segments = 0
for segment in segments:
results.append(
{"start": segment.start, "end": segment.end, "text": segment.text.strip()}
)
total_segments += 1
if total_segments % 100 == 0:
print(f"ASR_PROGRESS:{total_segments}", file=sys.stderr)
output = {
"language": info.language,
"language_probability": info.language_probability,
"segments": results,
}
with open(output_path, "w") as f:
json.dump(output, f, indent=2)
print(f"ASR_COMPLETE:{total_segments}", file=sys.stderr)
print(f"ASR complete. {len(results)} segments.", file=sys.stderr)
if __name__ == "__main__":
if len(sys.argv) != 3:
print("Usage: asr_processor.py <video_path> <output_json_path>")
sys.exit(1)
run_asr(sys.argv[1], sys.argv[2])

78
scripts/install_mongodb.sh Executable file
View File

@@ -0,0 +1,78 @@
#!/bin/bash
# MongoDB Installation Script
echo "=== MongoDB Installation ==="
echo ""
# Step 1: Create directories
echo "Step 1: Creating directories..."
sudo mkdir -p /Users/accusys/momentry/var
sudo mkdir -p /Users/accusys/momentry/log
sudo chown -R accusys:staff /Users/accusys/momentry
echo " ✓ Directories created"
# Step 2: Check and backup existing plist
echo ""
echo "Step 2: Checking existing plist..."
PLIST_PATH="/Library/LaunchDaemons/com.momentry.mongodb.plist"
if [ -f "$PLIST_PATH" ]; then
BACKUP_NAME="${PLIST_PATH}.$(date +%Y%m%d%H%M%S).bak"
echo " Backing up existing plist to: $BACKUP_NAME"
sudo mv "$PLIST_PATH" "$BACKUP_NAME"
echo " ✓ Existing plist backed up"
else
echo " ✓ No existing plist found"
fi
# Step 3: Copy new plist
echo ""
echo "Step 3: Copying new plist..."
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
SOURCE_PLIST="$SCRIPT_DIR/momentry_runtime/plist/com.momentry.mongodb.plist"
if [ -f "$SOURCE_PLIST" ]; then
sudo cp "$SOURCE_PLIST" /Library/LaunchDaemons/
echo " ✓ plist copied"
else
echo " ✗ Source plist not found: $SOURCE_PLIST"
exit 1
fi
# Step 4: Start MongoDB
echo ""
echo "Step 4: Starting MongoDB..."
sudo launchctl load /Library/LaunchDaemons/com.momentry.mongodb.plist
echo " ✓ MongoDB started"
# Step 5: Verify
echo ""
echo "Step 5: Verifying..."
sleep 2
echo ""
echo " Port 27017 status:"
lsof -i :27017 || echo " (checking...)"
echo ""
echo " Service status:"
sudo launchctl list | grep momentry || echo " No service found"
# Step 6: Create user (without auth first)
echo ""
echo "Step 6: Creating database user..."
mongosh --eval '
use admin
db.createUser({
user: "accusys",
pwd: "Test3200Test3200",
roles: [{ role: "root", db: "admin" }]
})
' 2>/dev/null || echo " (user creation deferred - MongoDB may still be starting)"
echo ""
echo "=== Installation Complete ==="
echo ""
echo "Connection string:"
echo " mongodb://accusys:Test3200Test3200@localhost:27017/momentry"
echo ""
echo "Remote access enabled (bind_ip: 0.0.0.0)"

View File

@@ -0,0 +1,97 @@
#!/opt/homebrew/bin/python3.11
import sys
import os
import subprocess
import json
import argparse
def get_duration(video_path):
result = subprocess.run(
["ffprobe", "-v", "quiet", "-print_format", "json", "-show_format", video_path],
capture_output=True,
text=True,
)
data = json.loads(result.stdout)
return float(data["format"]["duration"])
def extract_thumbnails(video_path, uuid, output_dir, count=6):
"""Extract evenly-spaced thumbnails from video."""
if not os.path.exists(video_path):
print(f"Error: Video file not found: {video_path}", file=sys.stderr)
return None
duration = get_duration(video_path)
print(f"Video duration: {duration:.2f}s", file=sys.stderr)
# Output directory
thumb_dir = os.path.join(output_dir, uuid)
os.makedirs(thumb_dir, exist_ok=True)
# Calculate timestamps (skip first 10%, skip last 10%)
start_ts = duration * 0.1
end_ts = duration * 0.9
interval = (end_ts - start_ts) / (count - 1) if count > 1 else 0
extracted = []
for i in range(count):
if count == 1:
ts = duration / 2
else:
ts = start_ts + (interval * i)
output_file = os.path.join(thumb_dir, f"thumb_{i:03d}.jpg")
result = subprocess.run(
[
"ffmpeg",
"-y",
"-ss",
str(ts),
"-i",
video_path,
"-vframes",
"1",
"-q:v",
"2",
"-vf",
"scale=320:-1",
output_file,
],
capture_output=True,
)
if result.returncode == 0 and os.path.exists(output_file):
extracted.append(output_file)
print(f" Extracted: {output_file} at {ts:.1f}s", file=sys.stderr)
else:
print(f" Failed to extract frame at {ts:.1f}s", file=sys.stderr)
return extracted
def main():
parser = argparse.ArgumentParser(
description="Extract keyframe thumbnails from video"
)
parser.add_argument("video_path", help="Path to video file")
parser.add_argument("uuid", help="Video UUID")
parser.add_argument("-o", "--output", default="thumbnails", help="Output directory")
parser.add_argument(
"-c", "--count", type=int, default=6, help="Number of thumbnails"
)
args = parser.parse_args()
result = extract_thumbnails(args.video_path, args.uuid, args.output, args.count)
if result:
print(json.dumps({"uuid": args.uuid, "count": len(result), "files": result}))
else:
print(json.dumps({"error": "Failed to extract thumbnails"}))
sys.exit(1)
if __name__ == "__main__":
main()

151
scripts/troubleshoot.sh Executable file
View File

@@ -0,0 +1,151 @@
#!/bin/bash
# Momentry 服務故障排除快速參考
# 用法: ./troubleshoot.sh [service]
# 例如: ./troubleshoot.sh n8n
SERVICE=${1:-all}
echo "========================================"
echo "Momentry 故障排除快速檢查"
echo "========================================"
echo ""
case "$SERVICE" in
all)
echo "執行所有服務健康檢查..."
/Users/accusys/momentry_core_0.1/monitor/service/health_check.sh
;;
postgresql|postgres|pg)
echo "=== PostgreSQL 診斷 ==="
echo "狀態: $(pg_isready -h localhost -p 5432 -U accusys 2>&1)"
echo "連線測試: $(psql -U accusys -h localhost -d momentry -c 'SELECT 1' 2>&1 | head -1)"
echo "數據庫列表:"
psql -U accusys -h localhost -l 2>/dev/null | grep -v "rows)" || echo " 無法連線"
echo ""
echo "日誌: tail -50 /Users/accusys/momentry/log/postgresql.log"
;;
redis)
echo "=== Redis 診斷 ==="
echo "狀態: $(redis-cli -a accusys ping 2>&1)"
echo "記憶體: $(redis-cli -a accusys INFO memory 2>/dev/null | grep used_memory_human | head -1)"
echo "連線數: $(redis-cli -a accusys INFO clients 2>/dev/null | grep connected_clients | head -1)"
echo "Keys: $(redis-cli -a accusys DBSIZE 2>/dev/null)"
echo ""
echo "日誌: tail -50 /Users/accusys/momentry/log/redis.log"
;;
mariadb|mysql)
echo "=== MariaDB 診斷 ==="
echo "狀態: $(mysql -u accusys -e 'SELECT 1' 2>&1 | head -1)"
echo "用戶:"
mysql -u accusys -e "SELECT user, host FROM mysql.user;" 2>/dev/null || echo " 無法連線"
echo ""
echo "日誌: tail -50 /Users/accusys/momentry/log/mariadb.log"
;;
n8n)
echo "=== n8n 診斷 ==="
echo "Web 訪問: $(curl -s -o /dev/null -w '%{http_code}' http://localhost:8085/ 2>&1)"
echo "Port 8085: $(lsof -i :8085 | tail -1)"
echo ""
echo "PostgreSQL 連線:"
psql -U n8n -h localhost -d n8n -c "SELECT COUNT(*) as workflows FROM workflow_entity;" 2>&1
psql -U n8n -h localhost -d n8n -c "SELECT email, \"firstName\", \"roleSlug\" FROM \"user\";" 2>&1
echo ""
echo "Redis Queue:"
redis-cli -a accusys LLEN bull:n8n:wait 2>/dev/null || echo " 無法連線"
echo ""
echo "日誌: tail -50 /Users/accusys/momentry/log/n8n-main.log"
;;
ollama)
echo "=== Ollama 診斷 ==="
echo "API 狀態: $(curl -s http://localhost:11434/api/tags 2>&1 | head -1)"
echo "模型列表:"
curl -s http://localhost:11434/api/tags 2>/dev/null | jq -r '.models[].name' 2>/dev/null || echo " 無法獲取"
echo ""
echo "日誌: tail -50 /Users/accusys/momentry/log/ollama.log"
;;
qdrant)
echo "=== Qdrant 診斷 ==="
echo "API 狀態: $(curl -s -o /dev/null -w '%{http_code}' -H 'api-key: Test3200Test3200' http://localhost:6333/ 2>&1)"
echo "Collections:"
curl -s -H "api-key: Test3200Test3200" http://localhost:6333/collections 2>/dev/null | jq -r '.result[].name' 2>/dev/null || echo " 無法獲取"
echo ""
echo "日誌: tail -50 /Users/accusys/momentry/log/qdrant.log"
;;
caddy)
echo "=== Caddy 診斷 ==="
echo "Admin API: $(curl -s http://localhost:2019/config/ 2>&1 | head -1)"
echo "Port 2019: $(lsof -i :2019 | tail -1)"
echo "Port 443: $(lsof -i :443 | tail -1)"
echo ""
echo "配置: cat /Users/accusys/momentry/etc/Caddyfile"
echo "日誌: tail -50 /Users/accusys/momentry/log/caddy.log"
;;
gitea)
echo "=== Gitea 診斷 ==="
echo "Web: $(curl -s -o /dev/null -w '%{http_code}' http://localhost:3000/ 2>&1)"
echo "Port 3000: $(lsof -i :3000 | tail -1)"
echo ""
echo "日誌: tail -50 /Users/accusys/momentry/log/gitea.log"
;;
sftpgo)
echo "=== SFTPGo 診斷 ==="
echo "HTTP: $(curl -s -o /dev/null -w '%{http_code}' http://localhost:8080/ 2>&1)"
echo "SFTP Port 2022: $(lsof -i :2022 | tail -1)"
echo ""
echo "日誌: tail -50 /Users/accusys/momentry/log/sftpgo.log"
;;
mongodb|mongo)
echo "=== MongoDB 診斷 ==="
mongosh --quiet --username accusys --password Test3200Test3200 --authenticationDatabase admin --eval "db.adminCommand('ping')" 2>&1
echo "數據庫:"
mongosh --quiet --username accusys --password Test3200Test3200 --authenticationDatabase admin --eval "db.adminCommand({listDatabases:1})" 2>&1 | grep name || echo " 無法獲取"
echo ""
echo "日誌: tail -50 /Users/accusys/momentry/log/mongodb.log"
;;
php)
echo "=== PHP-FPM 診斷 ==="
echo "進程: $(pgrep -a php-fpm | head -1)"
echo "Port 9000: $(lsof -i :9000 | tail -1)"
echo ""
echo "日誌: tail -50 /Users/accusys/momentry/log/php.error.log"
;;
rustdesk)
echo "=== RustDesk 診斷 ==="
echo "hbbs: $(pgrep -a hbbs | head -1)"
echo "hbbr: $(pgrep -a hbbr | head -1)"
echo "Port 21116: $(lsof -i :21116 | tail -1)"
echo "Port 21117: $(lsof -i :21117 | tail -1)"
;;
*)
echo "用法: $0 [service]"
echo ""
echo "可用服務:"
echo " all - 執行所有健康檢查"
echo " postgresql - PostgreSQL 診斷"
echo " redis - Redis 診斷"
echo " mariadb - MariaDB 診斷"
echo " n8n - n8n 診斷"
echo " ollama - Ollama 診斷"
echo " qdrant - Qdrant 診斷"
echo " caddy - Caddy 診斷"
echo " gitea - Gitea 診斷"
echo " sftpgo - SFTPGo 診斷"
echo " mongodb - MongoDB 診斷"
echo " php - PHP-FPM 診斷"
echo " rustdesk - RustDesk 診斷"
;;
esac

3
src/api/mod.rs Normal file
View File

@@ -0,0 +1,3 @@
pub mod server;
pub use server::start_server;

20
src/api/server.rs Normal file
View File

@@ -0,0 +1,20 @@
// Placeholder for API server
pub async fn start_server(host: &str, port: u16) -> anyhow::Result<()> {
println!("Starting API server at {}:{}", host, port);
// TODO: Implement Axum server
//
// Routes:
// POST /api/v1/register
// POST /api/v1/process
// POST /api/v1/chunk
// POST /api/v1/vectorize
// POST /api/v1/store
// POST /api/v1/watch
// DELETE /api/v1/watch/{path}
// GET /api/v1/lookup?path=...
// GET /api/v1/resolve?uuid=...
// GET /api/v1/status/{uuid}
// POST /api/v1/query
Ok(())
}

5
src/core/chunk/mod.rs Normal file
View File

@@ -0,0 +1,5 @@
pub mod splitter;
pub mod types;
pub use splitter::{AsrSegment, ChunkSplitter};
pub use types::{Chunk, ChunkType};

View File

@@ -0,0 +1,67 @@
use super::types::{Chunk, ChunkType};
use anyhow::Result;
pub struct ChunkSplitter {
time_based_duration: f64,
}
impl ChunkSplitter {
pub fn new(time_based_duration_seconds: f64) -> Self {
Self {
time_based_duration: time_based_duration_seconds,
}
}
pub fn split_time_based(&self, uuid: &str, duration: f64) -> Vec<Chunk> {
let mut chunks = Vec::new();
let mut index = 0;
let mut current_time = 0.0;
while current_time < duration {
let end_time = (current_time + self.time_based_duration).min(duration);
chunks.push(Chunk::new(
uuid.to_string(),
index,
ChunkType::TimeBased,
current_time,
end_time,
serde_json::json!({
"source": "time_based",
"duration": self.time_based_duration,
}),
));
current_time = end_time;
index += 1;
}
chunks
}
pub fn split_sentence(&self, uuid: &str, asr_segments: &[AsrSegment]) -> Vec<Chunk> {
let mut chunks = Vec::new();
for (index, segment) in asr_segments.iter().enumerate() {
chunks.push(Chunk::new(
uuid.to_string(),
index as u32,
ChunkType::Sentence,
segment.start,
segment.end,
serde_json::json!({
"text": segment.text,
"speaker_id": segment.speaker_id,
}),
));
}
chunks
}
}
#[derive(Debug, Clone)]
pub struct AsrSegment {
pub start: f64,
pub end: f64,
pub text: String,
pub speaker_id: Option<String>,
}

52
src/core/chunk/types.rs Normal file
View File

@@ -0,0 +1,52 @@
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "snake_case")]
pub enum ChunkType {
TimeBased,
Sentence,
Cut,
}
impl ChunkType {
pub fn as_str(&self) -> &'static str {
match self {
ChunkType::TimeBased => "time_based",
ChunkType::Sentence => "sentence",
ChunkType::Cut => "cut",
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Chunk {
pub uuid: String,
pub chunk_id: String,
pub chunk_index: u32,
pub chunk_type: ChunkType,
pub start_time: f64,
pub end_time: f64,
pub content: serde_json::Value,
}
impl Chunk {
pub fn new(
uuid: String,
chunk_index: u32,
chunk_type: ChunkType,
start_time: f64,
end_time: f64,
content: serde_json::Value,
) -> Self {
let chunk_id = format!("{}_{:04}", chunk_type.as_str(), chunk_index);
Self {
uuid,
chunk_id: chunk_id.clone(),
chunk_index,
chunk_type,
start_time,
end_time,
content,
}
}
}

40
src/core/db/mod.rs Normal file
View File

@@ -0,0 +1,40 @@
use anyhow::Result;
use async_trait::async_trait;
use crate::core::chunk::Chunk;
#[derive(Debug, Clone)]
pub struct SearchResult {
pub chunk_id: String,
pub score: f32,
}
#[async_trait]
pub trait Database: Send + Sync {
async fn init() -> Result<Self>
where
Self: Sized;
}
#[async_trait]
pub trait ChunkStore: Send + Sync {
async fn store_chunk(&self, chunk: &Chunk) -> Result<()>;
async fn get_chunks_by_uuid(&self, uuid: &str) -> Result<Vec<Chunk>>;
async fn get_all_chunks(&self) -> Result<Vec<Chunk>>;
}
#[async_trait]
pub trait VectorStore: Send + Sync {
async fn store_vector(&self, chunk_id: &str, vector: &[f32]) -> Result<()>;
async fn search(&self, query_vector: &[f32], limit: usize) -> Result<Vec<SearchResult>>;
}
pub mod mongodb_db;
pub mod postgres_db;
pub mod qdrant_db;
pub mod redis_db;
pub use mongodb_db::MongoDb;
pub use postgres_db::{PostgresDb, VideoRecord};
pub use qdrant_db::QdrantDb;
pub use redis_db::RedisDb;

53
src/core/db/mongodb_db.rs Normal file
View File

@@ -0,0 +1,53 @@
use anyhow::Result;
use async_trait::async_trait;
use std::sync::Arc;
use tokio::sync::RwLock;
use super::Database;
pub struct MongoDb {
cache: Arc<RwLock<MongoCache>>,
}
#[derive(Debug, Default)]
pub struct MongoCache {
documents: std::collections::HashMap<String, serde_json::Value>,
}
#[derive(Debug, serde::Serialize, serde::Deserialize)]
pub struct VideoDocument {
pub uuid: String,
pub file_path: String,
pub file_name: String,
pub probe: serde_json::Value,
pub asr: Option<serde_json::Value>,
pub asrx: Option<serde_json::Value>,
pub ocr: Option<serde_json::Value>,
pub yolo: Option<serde_json::Value>,
pub face: Option<serde_json::Value>,
pub pose: Option<serde_json::Value>,
pub created_at: String,
pub updated_at: String,
}
impl MongoDb {
pub async fn store_video(&self, _doc: &VideoDocument) -> Result<()> {
// TODO: Implement MongoDB client
Ok(())
}
pub async fn get_video(&self, _uuid: &str) -> Result<Option<VideoDocument>> {
// TODO: Implement MongoDB client
Ok(None)
}
}
#[async_trait]
impl Database for MongoDb {
async fn init() -> Result<Self> {
// TODO: Initialize MongoDB client
Ok(Self {
cache: Arc::new(RwLock::new(MongoCache::default())),
})
}
}

286
src/core/db/postgres_db.rs Normal file
View File

@@ -0,0 +1,286 @@
use anyhow::Result;
use async_trait::async_trait;
use serde::{Deserialize, Serialize};
use sqlx::{PgPool, Row};
use std::sync::Arc;
use tokio::sync::RwLock;
use super::Database;
use crate::core::chunk::{Chunk, ChunkType};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct VideoRecord {
pub id: i64,
pub uuid: String,
pub file_path: String,
pub file_name: String,
pub duration: f64,
pub width: u32,
pub height: u32,
pub fps: f64,
pub probe_json: Option<String>,
pub created_at: String,
}
pub struct PostgresDb {
pool: PgPool,
cache: Arc<RwLock<PostgresCache>>,
}
#[derive(Debug, Default)]
pub struct PostgresCache {
videos: std::collections::HashMap<String, VideoRecord>,
chunks: std::collections::HashMap<String, Vec<Chunk>>,
}
impl PostgresDb {
pub async fn new(database_url: &str) -> Result<Self> {
let pool = PgPool::connect(database_url).await?;
let db = Self {
pool,
cache: Arc::new(RwLock::new(PostgresCache::default())),
};
db.init_schema().await?;
Ok(db)
}
pub async fn register_video(&self, record: &VideoRecord) -> Result<i64> {
let result = sqlx::query(
r#"
INSERT INTO videos (uuid, file_path, file_name, duration, width, height, fps, probe_json)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
ON CONFLICT (uuid) DO UPDATE SET
file_path = EXCLUDED.file_path,
file_name = EXCLUDED.file_name,
duration = EXCLUDED.duration,
width = EXCLUDED.width,
height = EXCLUDED.height,
fps = EXCLUDED.fps,
probe_json = EXCLUDED.probe_json,
updated_at = CURRENT_TIMESTAMP
RETURNING id::bigint
"#
)
.bind(&record.uuid)
.bind(&record.file_path)
.bind(&record.file_name)
.bind(record.duration)
.bind(record.width as i32)
.bind(record.height as i32)
.bind(record.fps)
.bind(&record.probe_json)
.fetch_one(&self.pool)
.await?;
let id: i64 = result.get(0);
// Update cache
let mut cache = self.cache.write().await;
let mut record = record.clone();
record.id = id as i64;
cache.videos.insert(record.uuid.clone(), record);
Ok(id)
}
pub async fn get_video_by_uuid(&self, uuid: &str) -> Result<Option<VideoRecord>> {
// Check cache first
{
let cache = self.cache.read().await;
if let Some(video) = cache.videos.get(uuid) {
return Ok(Some(video.clone()));
}
}
let result = sqlx::query_as::<_, (i32, String, String, String, f64, i32, i32, f64, Option<String>)>(
"SELECT id, uuid, file_path, file_name, duration, width, height, fps, probe_json FROM videos WHERE uuid = $1"
)
.bind(uuid)
.fetch_optional(&self.pool)
.await?;
if let Some(r) = result {
let video = VideoRecord {
id: r.0 as i64,
uuid: r.1,
file_path: r.2,
file_name: r.3,
duration: r.4,
width: r.5 as u32,
height: r.6 as u32,
fps: r.7,
probe_json: r.8,
created_at: String::new(),
};
// Update cache
let mut cache = self.cache.write().await;
cache.videos.insert(uuid.to_string(), video.clone());
Ok(Some(video))
} else {
Ok(None)
}
}
pub async fn list_videos(&self) -> Result<Vec<VideoRecord>> {
let rows = sqlx::query_as::<_, (i32, String, String, String, f64, i32, i32, f64, Option<String>)>(
"SELECT id, uuid, file_path, file_name, duration, width, height, fps, probe_json FROM videos ORDER BY id DESC"
)
.fetch_all(&self.pool)
.await?;
let videos: Vec<VideoRecord> = rows
.into_iter()
.map(|r| VideoRecord {
id: r.0 as i64,
uuid: r.1,
file_path: r.2,
file_name: r.3,
duration: r.4,
width: r.5 as u32,
height: r.6 as u32,
fps: r.7,
probe_json: r.8,
created_at: String::new(),
})
.collect();
Ok(videos)
}
async fn init_schema(&self) -> Result<()> {
sqlx::query(
r#"
CREATE TABLE IF NOT EXISTS videos (
id SERIAL PRIMARY KEY,
uuid VARCHAR(32) UNIQUE NOT NULL,
file_path TEXT NOT NULL,
file_name TEXT NOT NULL,
duration DOUBLE PRECISION,
width INTEGER,
height INTEGER,
fps DOUBLE PRECISION,
probe_json TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
"#,
)
.execute(&self.pool)
.await?;
sqlx::query("CREATE INDEX IF NOT EXISTS idx_videos_uuid ON videos(uuid)")
.execute(&self.pool)
.await?;
sqlx::query(
r#"
CREATE TABLE IF NOT EXISTS chunks (
id SERIAL PRIMARY KEY,
uuid VARCHAR(32) 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,
vector_id VARCHAR(64),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
UNIQUE(uuid, chunk_id)
)
"#,
)
.execute(&self.pool)
.await?;
sqlx::query("CREATE INDEX IF NOT EXISTS idx_chunks_uuid ON chunks(uuid)")
.execute(&self.pool)
.await?;
sqlx::query("CREATE INDEX IF NOT EXISTS idx_chunks_type ON chunks(chunk_type)")
.execute(&self.pool)
.await?;
sqlx::query("CREATE INDEX IF NOT EXISTS idx_chunks_time ON chunks(start_time, end_time)")
.execute(&self.pool)
.await?;
Ok(())
}
pub async fn store_chunk(&self, chunk: &Chunk) -> Result<()> {
sqlx::query(
r#"
INSERT INTO chunks (uuid, chunk_id, chunk_index, chunk_type, start_time, end_time, content)
VALUES ($1, $2, $3, $4, $5, $6, $7::jsonb)
ON CONFLICT (uuid, chunk_id) DO UPDATE SET
start_time = EXCLUDED.start_time,
end_time = EXCLUDED.end_time,
content = EXCLUDED.content,
vector_id = EXCLUDED.vector_id
"#
)
.bind(&chunk.uuid)
.bind(&chunk.chunk_id)
.bind(chunk.chunk_index as i32)
.bind(chunk.chunk_type.as_str())
.bind(chunk.start_time)
.bind(chunk.end_time)
.bind(&chunk.content)
.execute(&self.pool)
.await?;
Ok(())
}
pub async fn get_chunks_by_uuid(&self, uuid: &str) -> Result<Vec<Chunk>> {
let rows = sqlx::query(
"SELECT uuid, chunk_id, chunk_index, chunk_type, start_time, end_time, content FROM chunks WHERE uuid = $1 ORDER BY chunk_index"
)
.bind(uuid)
.fetch_all(&self.pool)
.await?;
let chunks: Vec<Chunk> = rows
.into_iter()
.map(|r| {
let chunk_type_str: String = r.get(3);
let chunk_index: i32 = r.get(2);
let chunk_type = match chunk_type_str.as_str() {
"time_based" => ChunkType::TimeBased,
"sentence" => ChunkType::Sentence,
"cut" => ChunkType::Cut,
_ => ChunkType::TimeBased,
};
let content_json: String = r.get(6);
let content: serde_json::Value =
serde_json::from_str(&content_json).unwrap_or(serde_json::json!({}));
Chunk {
uuid: r.get(0),
chunk_id: r.get(1),
chunk_index: chunk_index as u32,
chunk_type,
start_time: r.get(4),
end_time: r.get(5),
content,
}
})
.collect();
Ok(chunks)
}
}
#[async_trait]
impl Database for PostgresDb {
async fn init() -> Result<Self> {
let database_url = std::env::var("DATABASE_URL")
.unwrap_or_else(|_| "postgres://accusys@localhost:5432/momentry".to_string());
Self::new(&database_url).await
}
}

88
src/core/db/qdrant_db.rs Normal file
View File

@@ -0,0 +1,88 @@
use anyhow::Result;
use async_trait::async_trait;
use std::sync::Arc;
use tokio::sync::RwLock;
use super::{Database, SearchResult, VectorStore};
pub struct QdrantDb {
collection_name: String,
cache: Arc<RwLock<QdrantCache>>,
}
#[derive(Debug, Default)]
pub struct QdrantCache {
vectors: std::collections::HashMap<String, Vec<f32>>,
}
impl QdrantDb {
pub async fn init_collection(&self) -> Result<()> {
// TODO: Implement actual Qdrant client
// This is a placeholder
Ok(())
}
pub async fn upsert_vector(&self, chunk_id: &str, vector: &[f32]) -> Result<()> {
let mut cache = self.cache.write().await;
cache.vectors.insert(chunk_id.to_string(), vector.to_vec());
Ok(())
}
}
#[async_trait]
impl Database for QdrantDb {
async fn init() -> Result<Self> {
let collection_name =
std::env::var("QDRANT_COLLECTION").unwrap_or_else(|_| "momentry_chunks".to_string());
let db = Self {
collection_name,
cache: Arc::new(RwLock::new(QdrantCache::default())),
};
db.init_collection().await?;
Ok(db)
}
}
#[async_trait]
impl VectorStore for QdrantDb {
async fn store_vector(&self, chunk_id: &str, vector: &[f32]) -> Result<()> {
self.upsert_vector(chunk_id, vector).await
}
async fn search(&self, query_vector: &[f32], limit: usize) -> Result<Vec<SearchResult>> {
// Simple cosine similarity search (placeholder)
let cache = self.cache.read().await;
let mut results: Vec<SearchResult> = Vec::new();
for (chunk_id, vector) in &cache.vectors {
let similarity = cosine_similarity(query_vector, vector);
results.push(SearchResult {
chunk_id: chunk_id.clone(),
score: similarity,
});
}
results.sort_by(|a, b| b.score.partial_cmp(&a.score).unwrap());
results.truncate(limit);
Ok(results)
}
}
fn cosine_similarity(a: &[f32], b: &[f32]) -> f32 {
if a.len() != b.len() || a.is_empty() {
return 0.0;
}
let dot_product: f32 = a.iter().zip(b.iter()).map(|(x, y)| x * y).sum();
let norm_a: f32 = a.iter().map(|x| x * x).sum::<f32>().sqrt();
let norm_b: f32 = b.iter().map(|x| x * x).sum::<f32>().sqrt();
if norm_a == 0.0 || norm_b == 0.0 {
return 0.0;
}
dot_product / (norm_a * norm_b)
}

65
src/core/db/redis_db.rs Normal file
View File

@@ -0,0 +1,65 @@
use anyhow::Result;
use async_trait::async_trait;
use std::sync::Arc;
use tokio::sync::RwLock;
use super::Database;
pub struct RedisDb {
state: Arc<RwLock<RedisState>>,
}
#[derive(Debug, Clone, Default)]
pub struct RedisState {
pub processing: Vec<String>,
pub completed: Vec<String>,
pub failed: Vec<String>,
}
#[derive(Debug, serde::Serialize, serde::Deserialize)]
pub struct Job {
pub id: String,
pub uuid: String,
pub job_type: String,
pub status: String,
pub progress: f32,
pub created_at: String,
pub updated_at: String,
}
impl RedisDb {
pub async fn push_job(&self, _job: &Job) -> Result<()> {
// TODO: Implement Redis client
Ok(())
}
pub async fn get_pending_jobs(&self) -> Result<Vec<Job>> {
// TODO: Implement Redis client
Ok(vec![])
}
pub async fn update_job_status(
&self,
_job_id: &str,
_status: &str,
_progress: f32,
) -> Result<()> {
// TODO: Implement Redis client
Ok(())
}
pub async fn publish_event(&self, _channel: &str, _message: &str) -> Result<()> {
// TODO: Implement Redis Pub/Sub
Ok(())
}
}
#[async_trait]
impl Database for RedisDb {
async fn init() -> Result<Self> {
// TODO: Initialize Redis client
Ok(Self {
state: Arc::new(RwLock::new(RedisState::default())),
})
}
}

View File

@@ -0,0 +1,66 @@
use anyhow::Result;
pub struct Embedder {
model_path: String,
}
impl Embedder {
pub fn new(model_path: String) -> Self {
Self { model_path }
}
pub async fn embed_text(&self, text: &str) -> Result<Vec<f32>> {
// TODO: Implement comic-embed-text model loading and inference
// This is a placeholder that generates a random 768-dimensional vector
//
// Implementation would use:
// - candle (Rust ML framework) or
// - ort (ONNX Runtime) to run the model
//
// Example with ort:
// let session = Session::builder()?
// .with_execution_providers([CPUExecutionProvider::default().build()])?
// .with_model_from_file(&self.model_path)?;
//
// // Preprocess text to tensor
// let input = preprocess_text(text);
//
// // Run inference
// let output = session.run(vec![input])?;
//
// // Extract embeddings
// let embedding = output[0].view()[..768].to_vec();
let dim = 768;
let mut embedding = vec![0.0f32; dim];
// Simple hash-based embedding for now
let hash = self.hash_text(text);
for i in 0..dim {
embedding[i] = ((hash >> i) & 1) as f32;
}
// Normalize
let norm: f32 = embedding.iter().map(|x| x * x).sum::<f32>().sqrt();
if norm > 0.0 {
for v in &mut embedding {
*v /= norm;
}
}
Ok(embedding)
}
pub async fn embed_chunk_content(&self, chunk: &crate::core::chunk::Chunk) -> Result<Vec<f32>> {
let text = serde_json::to_string(&chunk.content)?;
self.embed_text(&text).await
}
fn hash_text(&self, text: &str) -> u64 {
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
let mut hasher = DefaultHasher::new();
text.hash(&mut hasher);
hasher.finish()
}
}

View File

@@ -0,0 +1,3 @@
pub mod comic_embed;
pub use comic_embed::Embedder;

8
src/core/mod.rs Normal file
View File

@@ -0,0 +1,8 @@
pub mod chunk;
pub mod db;
pub mod embedding;
pub mod overlay;
pub mod probe;
pub mod processor;
pub mod storage;
pub mod thumbnail;

3
src/core/overlay/mod.rs Normal file
View File

@@ -0,0 +1,3 @@
pub mod types;
pub use types::OverlayFlags;

41
src/core/overlay/types.rs Normal file
View File

@@ -0,0 +1,41 @@
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq)]
pub struct OverlayFlags {
pub asr: bool,
pub asrx: bool,
pub ocr: bool,
pub yolo: bool,
pub face: bool,
pub pose: bool,
pub status: bool,
}
impl Default for OverlayFlags {
fn default() -> Self {
Self {
asr: false,
asrx: false,
ocr: false,
yolo: false,
face: false,
pose: false,
status: true,
}
}
}
impl OverlayFlags {
pub fn toggle(&mut self, layer: char) {
match layer {
'a' => self.asr = !self.asr,
'x' => self.asrx = !self.asrx,
'o' => self.ocr = !self.ocr,
'y' => self.yolo = !self.yolo,
'f' => self.face = !self.face,
'p' => self.pose = !self.pose,
'b' => self.status = !self.status,
_ => {}
}
}
}

3
src/core/probe/mod.rs Normal file
View File

@@ -0,0 +1,3 @@
pub mod probe;
pub use probe::{probe_video, ProbeResult};

84
src/core/probe/probe.rs Normal file
View File

@@ -0,0 +1,84 @@
use anyhow::{Context, Result};
use serde::{Deserialize, Serialize};
use std::process::Command;
#[derive(Debug, Serialize, Deserialize)]
pub struct ProbeResult {
pub streams: Vec<StreamInfo>,
pub format: FormatInfo,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct StreamInfo {
pub index: u32,
pub codec_name: Option<String>,
pub codec_type: Option<String>,
pub width: Option<u32>,
pub height: Option<u32>,
pub r_frame_rate: Option<String>,
pub duration: Option<String>,
pub sample_rate: Option<String>,
pub channels: Option<u32>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct FormatInfo {
pub filename: Option<String>,
pub format_name: Option<String>,
pub duration: Option<String>,
pub size: Option<String>,
pub bit_rate: Option<String>,
}
pub fn probe_video(video_path: &str) -> Result<ProbeResult> {
let output = Command::new("ffprobe")
.args([
"-v",
"quiet",
"-print_format",
"json",
"-show_format",
"-show_streams",
video_path,
])
.output()
.context("Failed to run ffprobe")?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
anyhow::bail!("ffprobe failed: {}", stderr);
}
let json_str = String::from_utf8_lossy(&output.stdout);
let json: serde_json::Value =
serde_json::from_str(&json_str).context("Failed to parse ffprobe output")?;
let streams: Vec<StreamInfo> = json["streams"]
.as_array()
.map(|arr| {
arr.iter()
.map(|s| StreamInfo {
index: s["index"].as_u64().unwrap_or(0) as u32,
codec_name: s["codec_name"].as_str().map(String::from),
codec_type: s["codec_type"].as_str().map(String::from),
width: s["width"].as_u64().map(|v| v as u32),
height: s["height"].as_u64().map(|v| v as u32),
r_frame_rate: s["r_frame_rate"].as_str().map(String::from),
duration: s["duration"].as_str().map(String::from),
sample_rate: s["sample_rate"].as_str().map(String::from),
channels: s["channels"].as_u64().map(|v| v as u32),
})
.collect()
})
.unwrap_or_default();
let format = FormatInfo {
filename: json["format"]["filename"].as_str().map(String::from),
format_name: json["format"]["format_name"].as_str().map(String::from),
duration: json["format"]["duration"].as_str().map(String::from),
size: json["format"]["size"].as_str().map(String::from),
bit_rate: json["format"]["bit_rate"].as_str().map(String::from),
};
Ok(ProbeResult { streams, format })
}

73
src/core/processor/asr.rs Normal file
View File

@@ -0,0 +1,73 @@
use anyhow::{Context, Result};
use serde::{Deserialize, Serialize};
use std::path::Path;
use std::process::Command;
#[derive(Debug, Serialize, Deserialize)]
pub struct AsrResult {
pub language: Option<String>,
pub language_probability: Option<f64>,
pub segments: Vec<AsrSegment>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct AsrSegment {
pub start: f64,
pub end: f64,
pub text: String,
}
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");
let venv_python = Path::new(env!("CARGO_MANIFEST_DIR"))
.join("venv")
.join("bin")
.join("python");
println!("[ASR] Starting ASR processing...");
println!("[ASR] Video: {}", video_path);
let output = Command::new(venv_python)
.arg(script_path)
.arg(video_path)
.arg(output_path)
.output()
.context("Failed to run ASR processor")?;
let stderr = String::from_utf8_lossy(&output.stderr);
for line in stderr.lines() {
if line.starts_with("ASR_START") {
println!("[ASR] Loading model...");
} else if line.starts_with("ASR_LANGUAGE:") {
let lang = line.trim_start_matches("ASR_LANGUAGE:");
println!("[ASR] Detected language: {}", lang);
} else if line.starts_with("ASR_PROGRESS:") {
let count = line.trim_start_matches("ASR_PROGRESS:");
println!("[ASR] Processed {} segments...", count);
} else if line.starts_with("ASR_COMPLETE:") {
let count = line.trim_start_matches("ASR_COMPLETE:");
println!("[ASR] Completed! Total: {} segments", count);
}
}
if !output.status.success() {
anyhow::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")?;
println!(
"[ASR] Result: {} segments, language: {:?}",
result.segments.len(),
result.language
);
Ok(result)
}

View File

@@ -0,0 +1,28 @@
use anyhow::Result;
use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize)]
pub struct AsrxResult {
pub segments: Vec<AsrxSegment>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct AsrxSegment {
pub start: f64,
pub end: f64,
pub text: String,
pub speaker_id: String,
pub speaker_embedding: Option<Vec<f32>>,
}
pub async fn process_asrx(video_path: &str, output_path: &str) -> Result<AsrxResult> {
// TODO: Implement speaker diarization
// Options:
// 1. Use pyannote.audio
// 2. Use whisperx
// 3. Use Python subprocess
println!("Processing speaker diarization for: {}", video_path);
Ok(AsrxResult { segments: vec![] })
}

View File

@@ -0,0 +1,36 @@
use anyhow::Result;
use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize)]
pub struct FaceResult {
pub frames: Vec<FaceFrame>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct FaceFrame {
pub frame: u64,
pub timestamp: f64,
pub faces: Vec<Face>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct Face {
pub face_id: String,
pub x: i32,
pub y: i32,
pub width: i32,
pub height: i32,
pub confidence: f32,
pub embedding: Option<Vec<f32>>,
}
pub async fn process_face(video_path: &str, output_path: &str) -> Result<FaceResult> {
// TODO: Implement face detection
// Options:
// 1. Use MTCNN or RetinaFace with ONNX
// 2. Use Python subprocess
println!("Processing face detection for: {}", video_path);
Ok(FaceResult { frames: vec![] })
}

13
src/core/processor/mod.rs Normal file
View File

@@ -0,0 +1,13 @@
pub mod asr;
pub mod asrx;
pub mod face;
pub mod ocr;
pub mod pose;
pub mod yolo;
pub use asr::{process_asr, AsrResult, AsrSegment};
pub use asrx::process_asrx;
pub use face::process_face;
pub use ocr::process_ocr;
pub use pose::process_pose;
pub use yolo::process_yolo;

36
src/core/processor/ocr.rs Normal file
View File

@@ -0,0 +1,36 @@
use anyhow::Result;
use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize)]
pub struct OcrResult {
pub frames: Vec<OcrFrame>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct OcrFrame {
pub frame: u64,
pub timestamp: f64,
pub texts: Vec<OcrText>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct OcrText {
pub text: String,
pub x: i32,
pub y: i32,
pub width: i32,
pub height: i32,
pub confidence: f32,
}
pub async fn process_ocr(video_path: &str, output_path: &str) -> Result<OcrResult> {
// TODO: Implement OCR processing
// Options:
// 1. Use tesseract
// 2. Use Python pytesseract via subprocess
// 3. Use Rust OCR library
println!("Processing OCR for: {}", video_path);
Ok(OcrResult { frames: vec![] })
}

View File

@@ -0,0 +1,47 @@
use anyhow::Result;
use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize)]
pub struct PoseResult {
pub frames: Vec<PoseFrame>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct PoseFrame {
pub frame: u64,
pub timestamp: f64,
pub persons: Vec<PersonPose>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct PersonPose {
pub keypoints: Vec<Keypoint>,
pub bbox: Bbox,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct Keypoint {
pub name: String,
pub x: f32,
pub y: f32,
pub confidence: f32,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct Bbox {
pub x: i32,
pub y: i32,
pub width: i32,
pub height: i32,
}
pub async fn process_pose(video_path: &str, output_path: &str) -> Result<PoseResult> {
// TODO: Implement pose estimation
// Options:
// 1. Use MoveNet or PoseNet with ONNX
// 2. Use Python subprocess with ultralytics
println!("Processing pose estimation for: {}", video_path);
Ok(PoseResult { frames: vec![] })
}

View File

@@ -0,0 +1,36 @@
use anyhow::Result;
use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize)]
pub struct YoloResult {
pub frames: Vec<YoloFrame>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct YoloFrame {
pub frame: u64,
pub timestamp: f64,
pub objects: Vec<YoloObject>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct YoloObject {
pub class_name: String,
pub class_id: u32,
pub x: i32,
pub y: i32,
pub width: i32,
pub height: i32,
pub confidence: f32,
}
pub async fn process_yolo(video_path: &str, output_path: &str) -> Result<YoloResult> {
// TODO: Implement YOLO processing
// Options:
// 1. Use ONNX Runtime (ort) with YOLO model
// 2. Use Python subprocess with ultralytics
println!("Processing YOLO for: {}", video_path);
Ok(YoloResult { frames: vec![] })
}

View File

@@ -0,0 +1,57 @@
use anyhow::Result;
use std::fs;
use std::path::{Path, PathBuf};
pub struct FileManager {
base_dir: PathBuf,
}
impl FileManager {
pub fn new(base_dir: PathBuf) -> Self {
Self { base_dir }
}
pub fn get_json_path(&self, uuid: &str, suffix: &str) -> PathBuf {
self.base_dir.join(format!("{}.{}.json", uuid, suffix))
}
pub fn save_json(&self, uuid: &str, suffix: &str, content: &str) -> Result<PathBuf> {
let path = self.get_json_path(uuid, suffix);
if let Some(parent) = path.parent() {
fs::create_dir_all(parent)?;
}
fs::write(&path, content)?;
Ok(path)
}
pub fn load_json(&self, uuid: &str, suffix: &str) -> Result<String> {
let path = self.get_json_path(uuid, suffix);
Ok(fs::read_to_string(path)?)
}
pub fn exists(&self, uuid: &str, suffix: &str) -> bool {
self.get_json_path(uuid, suffix).exists()
}
pub fn list_video_files(dir: &Path) -> Result<Vec<PathBuf>> {
let mut videos = Vec::new();
if !dir.exists() {
return Ok(videos);
}
for entry in fs::read_dir(dir)? {
let entry = entry?;
let path = entry.path();
if let Some(ext) = path.extension() {
let ext = ext.to_string_lossy().to_lowercase();
if matches!(ext.as_str(), "mp4" | "mkv" | "avi" | "mov" | "webm" | "m4v") {
videos.push(path);
}
}
}
Ok(videos)
}
}

5
src/core/storage/mod.rs Normal file
View File

@@ -0,0 +1,5 @@
pub mod file_manager;
pub mod uuid;
pub use file_manager::FileManager;
pub use uuid::compute_uuid;

44
src/core/storage/uuid.rs Normal file
View File

@@ -0,0 +1,44 @@
use sha2::{Digest, Sha256};
use std::path::PathBuf;
/// Compute UUID from file path using SHA256
/// UUID = SHA256(user_path + filename)[0:16]
pub fn compute_uuid(user_path: &str, filename: &str) -> String {
let key = format!("{}/{}", user_path.trim_end_matches('/'), filename);
let hash = Sha256::digest(key.as_bytes());
let hash_str = hex::encode(hash);
hash_str[0..16].to_string()
}
/// Compute UUID from full file path
pub fn compute_uuid_from_path(full_path: &str) -> String {
let path = PathBuf::from(full_path);
let parent = path
.parent()
.map(|p| p.to_string_lossy().to_string())
.unwrap_or_default();
let filename = path
.file_name()
.map(|n| n.to_string_lossy().to_string())
.unwrap_or_default();
compute_uuid(&parent, &filename)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_uuid_computation() {
let uuid = compute_uuid("/Users/test/Videos", "video.mp4");
assert_eq!(uuid.len(), 16);
println!("UUID: {}", uuid);
}
#[test]
fn test_uuid_from_path() {
let uuid = compute_uuid_from_path("/Users/test/Videos/video.mp4");
assert_eq!(uuid.len(), 16);
}
}

108
src/core/thumbnail/mod.rs Normal file
View File

@@ -0,0 +1,108 @@
use anyhow::{Context, Result};
use serde::{Deserialize, Serialize};
use std::path::{Path, PathBuf};
use std::process::Command;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ThumbnailResult {
pub uuid: String,
pub count: usize,
pub files: Vec<String>,
}
pub struct ThumbnailExtractor {
output_dir: PathBuf,
count: u32,
}
impl ThumbnailExtractor {
pub fn new(output_dir: PathBuf, count: u32) -> Self {
Self { output_dir, count }
}
pub fn extract(&self, video_path: &str, uuid: &str) -> Result<ThumbnailResult> {
let script_path = Path::new(env!("CARGO_MANIFEST_DIR"))
.join("scripts")
.join("thumbnail_extractor.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(uuid)
.arg("-o")
.arg(&self.output_dir)
.arg("-c")
.arg(self.count.to_string())
.output()
.context("Failed to run thumbnail extractor")?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
anyhow::bail!("Thumbnail extraction failed: {}", stderr);
}
let json_str = String::from_utf8_lossy(&output.stdout);
let result: ThumbnailResult =
serde_json::from_str(&json_str).context("Failed to parse thumbnail result")?;
Ok(result)
}
pub fn get_or_create(&self, video_path: &str, uuid: &str) -> Result<ThumbnailResult> {
let thumb_dir = self.output_dir.join(uuid);
// Check if thumbnails already exist
if thumb_dir.exists() {
let files: Vec<String> = (0..self.count)
.map(|i| thumb_dir.join(format!("thumb_{:03}.jpg", i)))
.filter(|p| p.exists())
.map(|p| p.to_string_lossy().to_string())
.collect();
if files.len() as u32 == self.count {
return Ok(ThumbnailResult {
uuid: uuid.to_string(),
count: files.len(),
files,
});
}
}
// Extract new thumbnails
self.extract(video_path, uuid)
}
pub fn get_thumbnails(&self, uuid: &str) -> Option<Vec<String>> {
let thumb_dir = self.output_dir.join(uuid);
if !thumb_dir.exists() {
return None;
}
let files: Vec<String> = (0..10)
.map(|i| thumb_dir.join(format!("thumb_{:03}.jpg", i)))
.filter(|p| p.exists())
.map(|p| p.to_string_lossy().to_string())
.collect();
if files.is_empty() {
None
} else {
Some(files)
}
}
pub fn cleanup(&self, uuid: &str) -> Result<()> {
let thumb_dir = self.output_dir.join(uuid);
if thumb_dir.exists() {
std::fs::remove_dir_all(&thumb_dir).context("Failed to remove thumbnail directory")?;
}
Ok(())
}
}

8
src/lib.rs Normal file
View File

@@ -0,0 +1,8 @@
pub mod core;
pub use core::chunk::{Chunk, ChunkSplitter, ChunkType};
pub use core::db::{Database, MongoDb, PostgresDb, QdrantDb, RedisDb, VideoRecord};
pub use core::probe::ProbeResult;
pub use core::storage::file_manager::FileManager;
pub use core::storage::uuid;
pub use core::thumbnail::{ThumbnailExtractor, ThumbnailResult};

340
src/main.rs Normal file
View File

@@ -0,0 +1,340 @@
use anyhow::{Context, Result};
use clap::{Parser, Subcommand};
use std::path::Path;
use momentry_core::{Database, PostgresDb, VideoRecord};
#[derive(Parser)]
#[command(name = "momentry")]
#[command(about = "Digital asset management system with video analysis and RAG")]
struct Cli {
#[command(subcommand)]
command: Commands,
}
#[derive(Subcommand)]
enum Commands {
/// Register a video file
Register {
/// Video file path or URL
path: String,
},
/// Process video (generate all JSON files)
Process {
/// UUID or path
target: String,
},
/// Generate chunks and store in database
Chunk {
/// UUID
uuid: String,
},
/// Vectorize chunks
Vectorize {
/// UUID (or 'all' for all)
uuid: String,
},
/// Play video with overlays
Play {
/// Video path or UUID
target: String,
},
/// Start watching directories
Watch {
/// Directories to watch (comma separated)
directories: Option<String>,
},
/// Start API server
Server {
/// Host
#[arg(long, default_value = "127.0.0.1")]
host: String,
/// Port
#[arg(long, default_value = "3000")]
port: u16,
},
/// Query using RAG
Query {
/// Query text
query: String,
},
/// Lookup UUID from path
Lookup {
/// File path
path: String,
},
/// Resolve path from UUID
Resolve {
/// UUID
uuid: String,
},
/// Generate thumbnails for videos
Thumbnails {
/// UUID (optional, generates for all if not specified)
uuid: Option<String>,
/// Number of thumbnails per video
#[arg(short, long, default_value = "6")]
count: u32,
},
}
#[tokio::main]
async fn main() -> Result<()> {
tracing_subscriber::fmt::init();
let cli = Cli::parse();
match cli.command {
Commands::Register { path } => {
println!("Registering: {}", path);
// Compute UUID
let uuid = momentry_core::uuid::compute_uuid_from_path(&path);
println!("UUID: {}", uuid);
// Run ffprobe
let probe_result = momentry_core::core::probe::probe_video(&path)?;
println!("\nVideo probe results:");
let duration = probe_result
.format
.duration
.as_ref()
.and_then(|s| s.parse::<f64>().ok())
.unwrap_or(0.0);
println!(" Duration: {}s", duration);
if let Some(size) = &probe_result.format.size {
println!(" Size: {}", size);
}
let mut width = 0u32;
let mut height = 0u32;
let mut fps = 0.0;
for stream in &probe_result.streams {
if stream.codec_type.as_deref() == Some("video") {
width = stream.width.unwrap_or(0);
height = stream.height.unwrap_or(0);
if let Some(fps_str) = &stream.r_frame_rate {
if let Some((num, den)) = fps_str.split_once('/') {
if let (Ok(n), Ok(d)) = (num.parse::<f64>(), den.parse::<f64>()) {
if d > 0.0 {
fps = n / d;
}
}
}
}
println!(" Video: {}x{}", width, height);
if let Some(fps) = &stream.r_frame_rate {
println!(" FPS: {}", fps);
}
}
if stream.codec_type.as_deref() == Some("audio") {
println!(" Audio: {} channels", stream.channels.unwrap_or(0));
if let Some(sr) = &stream.sample_rate {
println!(" Sample Rate: {}", sr);
}
}
}
// Save probe JSON to file
let file_manager = momentry_core::FileManager::new(std::path::PathBuf::from("."));
let json_str = serde_json::to_string_pretty(&probe_result)?;
let json_path = file_manager.save_json(&uuid, "probe", &json_str)?;
println!("\nProbe JSON saved to: {:?}", json_path);
// Store in PostgreSQL
println!("\nStoring in database...");
let db = PostgresDb::init().await?;
let file_path = Path::new(&path)
.canonicalize()
.map(|p| p.to_string_lossy().to_string())
.unwrap_or_else(|_| path.clone());
let file_name = Path::new(&path)
.file_name()
.map(|n| n.to_string_lossy().to_string())
.unwrap_or_default();
let record = VideoRecord {
id: 0,
uuid: uuid.clone(),
file_path,
file_name,
duration,
width,
height,
fps,
probe_json: Some(json_str),
created_at: String::new(),
};
let video_id = db.register_video(&record).await?;
println!("Video registered with ID: {}", video_id);
Ok(())
}
Commands::Process { target } => {
println!("Processing: {}", target);
// Compute UUID if path is given
let uuid = if target.len() == 16 && !target.contains('/') {
target.clone()
} else {
momentry_core::uuid::compute_uuid_from_path(&target)
};
// Get video from database
let db = PostgresDb::init().await?;
let video = db
.get_video_by_uuid(&uuid)
.await?
.ok_or_else(|| anyhow::anyhow!("Video not found: {}", uuid))?;
let video_path = &video.file_path;
let file_manager = momentry_core::FileManager::new(std::path::PathBuf::from("."));
// Process ASR
println!("\nRunning ASR...");
let asr_path = format!("{}.asr.json", uuid);
let asr_result =
momentry_core::core::processor::process_asr(video_path, &asr_path).await?;
let asr_json = serde_json::to_string_pretty(&asr_result)?;
std::fs::write(&asr_path, &asr_json)?;
println!("ASR saved to: {}", asr_path);
println!(" {} segments found", asr_result.segments.len());
// TODO: Process OCR, YOLO, Face, Pose, ASRx
println!("\nOther processors not yet implemented.");
Ok(())
}
Commands::Chunk { uuid } => {
println!("Chunking: {}", uuid);
let db = PostgresDb::init().await?;
let video = db
.get_video_by_uuid(&uuid)
.await?
.ok_or_else(|| anyhow::anyhow!("Video not found: {}", uuid))?;
// Read ASR JSON
let asr_path = format!("{}.asr.json", uuid);
let asr_json = std::fs::read_to_string(&asr_path)
.context("ASR file not found. Run 'process' first.")?;
let asr_result: momentry_core::core::processor::asr::AsrResult =
serde_json::from_str(&asr_json)?;
println!("Processing {} ASR segments...", asr_result.segments.len());
// Split into sentence chunks
let mut sentence_chunks = Vec::new();
for (i, seg) in asr_result.segments.iter().enumerate() {
let chunk = momentry_core::Chunk::new(
uuid.clone(),
i as u32,
momentry_core::ChunkType::Sentence,
seg.start,
seg.end,
serde_json::json!({
"text": seg.text,
}),
);
sentence_chunks.push(chunk);
}
// Split into time-based chunks (10 seconds)
let splitter = momentry_core::core::chunk::ChunkSplitter::new(10.0);
let time_chunks = splitter.split_time_based(&uuid, video.duration);
// Store in database
println!("Storing {} sentence chunks...", sentence_chunks.len());
for chunk in &sentence_chunks {
db.store_chunk(chunk).await?;
}
println!("Storing {} time-based chunks...", time_chunks.len());
for chunk in &time_chunks {
db.store_chunk(chunk).await?;
}
println!(
"Done! {} total chunks stored.",
sentence_chunks.len() + time_chunks.len()
);
Ok(())
}
Commands::Vectorize { uuid } => {
println!("Vectorizing: {}", uuid);
// TODO: Implement vectorize
Ok(())
}
Commands::Play { target } => {
println!("Playing: {}", target);
// TODO: Implement play
Ok(())
}
Commands::Watch { directories } => {
println!("Starting watcher: {:?}", directories);
// TODO: Implement watch
Ok(())
}
Commands::Server { host, port } => {
println!("Starting API server at {}:{}", host, port);
// TODO: Implement server
Ok(())
}
Commands::Query { query } => {
println!("Query: {}", query);
// TODO: Implement query
Ok(())
}
Commands::Lookup { path } => {
let uuid = momentry_core::uuid::compute_uuid_from_path(&path);
println!("Path: {}", path);
println!("UUID: {}", uuid);
Ok(())
}
Commands::Resolve { uuid } => {
println!("Resolving UUID: {}", uuid);
// TODO: Look up path from UUID in database
println!("(Database lookup not implemented yet)");
Ok(())
}
Commands::Thumbnails { uuid, count } => {
let db = PostgresDb::init().await?;
let videos = if let Some(ref uuid) = uuid {
vec![db
.get_video_by_uuid(uuid)
.await?
.ok_or_else(|| anyhow::anyhow!("Video not found: {}", uuid))?]
} else {
db.list_videos().await?
};
let output_dir = std::path::PathBuf::from("thumbnails");
let extractor = momentry_core::ThumbnailExtractor::new(output_dir, count);
for video in videos {
println!(
"\nGenerating thumbnails for: {} ({})",
video.file_name, video.uuid
);
match extractor.get_or_create(&video.file_path, &video.uuid) {
Ok(result) => {
println!(" Generated {} thumbnails", result.count);
}
Err(e) => {
println!(" Error: {}", e);
}
}
}
println!("\nThumbnails generated successfully!");
Ok(())
}
}
}

3
src/player/mod.rs Normal file
View File

@@ -0,0 +1,3 @@
pub mod player;
pub use player::{play_video, PlayerConfig};

22
src/player/player.rs Normal file
View File

@@ -0,0 +1,22 @@
use crate::core::overlay::OverlayFlags;
pub struct PlayerConfig {
pub sync_delay_ms: u64,
pub overlay_flags: OverlayFlags,
}
impl Default for PlayerConfig {
fn default() -> Self {
Self {
sync_delay_ms: 800,
overlay_flags: OverlayFlags::default(),
}
}
}
pub async fn play_video(path: &str, config: PlayerConfig) -> anyhow::Result<()> {
// TODO: Implement video player with overlay
// Will integrate existing video_player functionality
println!("Playing: {} with config: {:?}", path, config);
Ok(())
}

3
src/watcher/mod.rs Normal file
View File

@@ -0,0 +1,3 @@
pub mod watcher;
pub use watcher::{watch_directories, WatcherConfig};

Some files were not shown because too many files have changed in this diff Show More