From de14bd6afa620d80476138a852f8aa423b1f948b Mon Sep 17 00:00:00 2001 From: accusys Date: Mon, 16 Mar 2026 15:07:33 +0800 Subject: [PATCH] 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 --- .gitignore | 11 + AGENTS.md | 168 + Cargo.lock | 4260 +++++++++++++++++ Cargo.toml | 59 + MOMENTRY_INTEGRATION_GUIDE.md | 443 ++ docs/INSTALL_CADDY.md | 451 ++ docs/INSTALL_GITEA.md | 394 ++ docs/INSTALL_MARIADB.md | 380 ++ docs/INSTALL_MONGODB.md | 367 ++ docs/INSTALL_N8N.md | 466 ++ docs/INSTALL_OLLAMA.md | 359 ++ docs/INSTALL_PHP.md | 379 ++ docs/INSTALL_POSTGRESQL.md | 372 ++ docs/INSTALL_QDRANT.md | 456 ++ docs/INSTALL_REDIS.md | 367 ++ docs/INSTALL_RUSTDESK.md | 284 ++ docs/INSTALL_SFTPGO.md | 360 ++ docs/NODEJS.md | 423 ++ docs/PYTHON.md | 483 ++ docs/SERVICES.md | 726 +++ docs/SERVICE_ADDITION_GUIDE.md | 659 +++ momentry_runtime/build.sh | 32 + momentry_runtime/check.sh | 86 + momentry_runtime/env/momentry.env | 65 + .../plist/com.momentry.agent.plist | 47 + .../plist/com.momentry.caddy.plist | 42 + .../plist/com.momentry.gitea.plist | 44 + .../plist/com.momentry.mariadb.plist | 32 + .../plist/com.momentry.mongodb.plist | 39 + .../plist/com.momentry.monitor.plist | 49 + .../plist/com.momentry.n8n.main.plist | 102 + .../plist/com.momentry.n8n.worker.plist | 105 + .../plist/com.momentry.ollama.plist | 44 + momentry_runtime/plist/com.momentry.php.plist | 34 + .../plist/com.momentry.postgresql.plist | 39 + .../plist/com.momentry.qdrant.plist | 46 + .../plist/com.momentry.redis.plist | 41 + .../plist/com.momentry.rustdesk.hbbr.plist | 33 + .../plist/com.momentry.rustdesk.hbbs.plist | 33 + .../plist/com.momentry.sftpgo.plist | 42 + momentry_runtime/plist/template.service.plist | 39 + momentry_runtime/start.sh | 59 + momentry_runtime/stop.sh | 25 + monitor/MONITORING.md | 366 ++ monitor/SKILL_TROUBLESHOOTING.md | 388 ++ monitor/config/monitor_config.yaml | 503 ++ monitor/control/monitor_control.sh | 362 ++ monitor/database/mongodb_monitor.sh | 82 + monitor/database/postgres_monitor.sh | 130 + monitor/database/qdrant_monitor.sh | 124 + monitor/database/redis_monitor.sh | 111 + monitor/database/schema.sql | 492 ++ monitor/portal/page_monitor.sh | 175 + monitor/service/external_monitor.sh | 93 + monitor/service/health_check.sh | 370 ++ monitor/service/node_monitor.sh | 270 ++ monitor/service/python_monitor.sh | 281 ++ monitor/storage/backup_monitor.sh | 375 ++ monitor/storage/storage_manager.sh | 193 + monitor/users/session_tracker.sh | 163 + monitor/workflow/n8n_workflow_monitor.sh | 265 + requirements.txt | 1 + rustfmt.toml | 3 + scripts/asr_processor.py | 53 + scripts/install_mongodb.sh | 78 + scripts/thumbnail_extractor.py | 97 + scripts/troubleshoot.sh | 151 + src/api/mod.rs | 3 + src/api/server.rs | 20 + src/core/chunk/mod.rs | 5 + src/core/chunk/splitter.rs | 67 + src/core/chunk/types.rs | 52 + src/core/db/mod.rs | 40 + src/core/db/mongodb_db.rs | 53 + src/core/db/postgres_db.rs | 286 ++ src/core/db/qdrant_db.rs | 88 + src/core/db/redis_db.rs | 65 + src/core/embedding/comic_embed.rs | 66 + src/core/embedding/mod.rs | 3 + src/core/mod.rs | 8 + src/core/overlay/mod.rs | 3 + src/core/overlay/types.rs | 41 + src/core/probe/mod.rs | 3 + src/core/probe/probe.rs | 84 + src/core/processor/asr.rs | 73 + src/core/processor/asrx.rs | 28 + src/core/processor/face.rs | 36 + src/core/processor/mod.rs | 13 + src/core/processor/ocr.rs | 36 + src/core/processor/pose.rs | 47 + src/core/processor/yolo.rs | 36 + src/core/storage/file_manager.rs | 57 + src/core/storage/mod.rs | 5 + src/core/storage/uuid.rs | 44 + src/core/thumbnail/mod.rs | 108 + src/lib.rs | 8 + src/main.rs | 340 ++ src/player/mod.rs | 3 + src/player/player.rs | 22 + src/watcher/mod.rs | 3 + src/watcher/watcher.rs | 41 + 101 files changed, 19858 insertions(+) create mode 100644 .gitignore create mode 100644 AGENTS.md create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 MOMENTRY_INTEGRATION_GUIDE.md create mode 100644 docs/INSTALL_CADDY.md create mode 100644 docs/INSTALL_GITEA.md create mode 100644 docs/INSTALL_MARIADB.md create mode 100644 docs/INSTALL_MONGODB.md create mode 100644 docs/INSTALL_N8N.md create mode 100644 docs/INSTALL_OLLAMA.md create mode 100644 docs/INSTALL_PHP.md create mode 100644 docs/INSTALL_POSTGRESQL.md create mode 100644 docs/INSTALL_QDRANT.md create mode 100644 docs/INSTALL_REDIS.md create mode 100644 docs/INSTALL_RUSTDESK.md create mode 100644 docs/INSTALL_SFTPGO.md create mode 100644 docs/NODEJS.md create mode 100644 docs/PYTHON.md create mode 100644 docs/SERVICES.md create mode 100644 docs/SERVICE_ADDITION_GUIDE.md create mode 100755 momentry_runtime/build.sh create mode 100755 momentry_runtime/check.sh create mode 100644 momentry_runtime/env/momentry.env create mode 100644 momentry_runtime/plist/com.momentry.agent.plist create mode 100644 momentry_runtime/plist/com.momentry.caddy.plist create mode 100644 momentry_runtime/plist/com.momentry.gitea.plist create mode 100644 momentry_runtime/plist/com.momentry.mariadb.plist create mode 100644 momentry_runtime/plist/com.momentry.mongodb.plist create mode 100644 momentry_runtime/plist/com.momentry.monitor.plist create mode 100644 momentry_runtime/plist/com.momentry.n8n.main.plist create mode 100644 momentry_runtime/plist/com.momentry.n8n.worker.plist create mode 100644 momentry_runtime/plist/com.momentry.ollama.plist create mode 100644 momentry_runtime/plist/com.momentry.php.plist create mode 100644 momentry_runtime/plist/com.momentry.postgresql.plist create mode 100644 momentry_runtime/plist/com.momentry.qdrant.plist create mode 100644 momentry_runtime/plist/com.momentry.redis.plist create mode 100644 momentry_runtime/plist/com.momentry.rustdesk.hbbr.plist create mode 100644 momentry_runtime/plist/com.momentry.rustdesk.hbbs.plist create mode 100644 momentry_runtime/plist/com.momentry.sftpgo.plist create mode 100644 momentry_runtime/plist/template.service.plist create mode 100755 momentry_runtime/start.sh create mode 100755 momentry_runtime/stop.sh create mode 100644 monitor/MONITORING.md create mode 100644 monitor/SKILL_TROUBLESHOOTING.md create mode 100644 monitor/config/monitor_config.yaml create mode 100755 monitor/control/monitor_control.sh create mode 100755 monitor/database/mongodb_monitor.sh create mode 100755 monitor/database/postgres_monitor.sh create mode 100755 monitor/database/qdrant_monitor.sh create mode 100755 monitor/database/redis_monitor.sh create mode 100644 monitor/database/schema.sql create mode 100755 monitor/portal/page_monitor.sh create mode 100755 monitor/service/external_monitor.sh create mode 100755 monitor/service/health_check.sh create mode 100755 monitor/service/node_monitor.sh create mode 100755 monitor/service/python_monitor.sh create mode 100755 monitor/storage/backup_monitor.sh create mode 100755 monitor/storage/storage_manager.sh create mode 100755 monitor/users/session_tracker.sh create mode 100755 monitor/workflow/n8n_workflow_monitor.sh create mode 100644 requirements.txt create mode 100644 rustfmt.toml create mode 100644 scripts/asr_processor.py create mode 100755 scripts/install_mongodb.sh create mode 100644 scripts/thumbnail_extractor.py create mode 100755 scripts/troubleshoot.sh create mode 100644 src/api/mod.rs create mode 100644 src/api/server.rs create mode 100644 src/core/chunk/mod.rs create mode 100644 src/core/chunk/splitter.rs create mode 100644 src/core/chunk/types.rs create mode 100644 src/core/db/mod.rs create mode 100644 src/core/db/mongodb_db.rs create mode 100644 src/core/db/postgres_db.rs create mode 100644 src/core/db/qdrant_db.rs create mode 100644 src/core/db/redis_db.rs create mode 100644 src/core/embedding/comic_embed.rs create mode 100644 src/core/embedding/mod.rs create mode 100644 src/core/mod.rs create mode 100644 src/core/overlay/mod.rs create mode 100644 src/core/overlay/types.rs create mode 100644 src/core/probe/mod.rs create mode 100644 src/core/probe/probe.rs create mode 100644 src/core/processor/asr.rs create mode 100644 src/core/processor/asrx.rs create mode 100644 src/core/processor/face.rs create mode 100644 src/core/processor/mod.rs create mode 100644 src/core/processor/ocr.rs create mode 100644 src/core/processor/pose.rs create mode 100644 src/core/processor/yolo.rs create mode 100644 src/core/storage/file_manager.rs create mode 100644 src/core/storage/mod.rs create mode 100644 src/core/storage/uuid.rs create mode 100644 src/core/thumbnail/mod.rs create mode 100644 src/lib.rs create mode 100644 src/main.rs create mode 100644 src/player/mod.rs create mode 100644 src/player/player.rs create mode 100644 src/watcher/mod.rs create mode 100644 src/watcher/watcher.rs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2f8823c --- /dev/null +++ b/.gitignore @@ -0,0 +1,11 @@ +.env +.env.local +target/ +venv/ +thumbnails/ +*.asr.json +*.probe.json +test_asr.json +.DS_Store +*.log +.ruff_cache/ diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..9749602 --- /dev/null +++ b/AGENTS.md @@ -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` for application code +- Use `thiserror` for library code +- Use `.context()` for error context +- Use `anyhow::bail!()` for early returns + +```rust +fn example() -> Result { + 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 + 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) diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..099dd5a --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,4260 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "ahash" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" +dependencies = [ + "cfg-if", + "getrandom 0.3.4", + "once_cell", + "version_check", + "zerocopy", +] + +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anstream" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" + +[[package]] +name = "anstyle-parse" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys 0.61.2", +] + +[[package]] +name = "anyhow" +version = "1.0.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" + +[[package]] +name = "async-stream" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b5a71a6f37880a80d1d7f19efd781e4b5de42c88f0722cc13bcb6cc2cfe8476" +dependencies = [ + "async-stream-impl", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-stream-impl" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "async-trait" +version = "0.1.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "atoi" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f28d99ec8bfea296261ca1af174f24225171fea9664ba9003cbebee704810528" +dependencies = [ + "num-traits", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "axum" +version = "0.7.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edca88bc138befd0323b20752846e6587272d3b03b0343c8ea28a6f819e6e71f" +dependencies = [ + "async-trait", + "axum-core", + "bytes", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "itoa", + "matchit", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "rustversion", + "serde", + "serde_json", + "serde_path_to_error", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tower 0.5.3", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-core" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09f2bd6146b97ae3359fa0cc6d6b376d9539582c7b4220f041a33ec24c226199" +dependencies = [ + "async-trait", + "bytes", + "futures-util", + "http", + "http-body", + "http-body-util", + "mime", + "pin-project-lite", + "rustversion", + "sync_wrapper", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "base64" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" + +[[package]] +name = "base64" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "base64ct" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" +dependencies = [ + "serde_core", +] + +[[package]] +name = "bitvec" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c" +dependencies = [ + "funty", + "radium", + "tap", + "wyz", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "bson" +version = "2.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969a9ba84b0ff843813e7249eed1678d9b6607ce5a3b8f0a47af3fcf7978e6e" +dependencies = [ + "ahash", + "base64 0.22.1", + "bitvec", + "getrandom 0.2.17", + "getrandom 0.3.4", + "hex", + "indexmap 2.13.0", + "js-sys", + "once_cell", + "rand 0.9.2", + "serde", + "serde_bytes", + "serde_json", + "time", + "uuid", +] + +[[package]] +name = "bumpalo" +version = "3.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" + +[[package]] +name = "cc" +version = "1.2.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aebf35691d1bfb0ac386a69bac2fde4dd276fb618cf8bf4f5318fe285e821bb2" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + +[[package]] +name = "chrono" +version = "0.4.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" +dependencies = [ + "iana-time-zone", + "num-traits", + "windows-link", +] + +[[package]] +name = "clap" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b193af5b67834b676abd72466a96c1024e6a6ad978a1f484bd90b85c94041351" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim 0.11.1", +] + +[[package]] +name = "clap_derive" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1110bd8a634a1ab8cb04345d8d878267d57c3cf1b38d91b71af6686408bbca6a" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "clap_lex" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" + +[[package]] +name = "colorchoice" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" + +[[package]] +name = "combine" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +dependencies = [ + "bytes", + "futures-core", + "memchr", + "pin-project-lite", + "tokio", + "tokio-util", +] + +[[package]] +name = "concurrent-queue" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + +[[package]] +name = "convert_case" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" + +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crc" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5eb8a2a1cd12ab0d987a5d5e825195d372001a4094a0376319d5a0ad71c1ba0d" +dependencies = [ + "crc-catalog", +] + +[[package]] +name = "crc-catalog" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" + +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-channel" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-queue" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f58bbc28f91df819d0aa2a2c00cd19754769c2fad90579b3592b1c9ba7a3115" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "darling" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a01d95850c592940db9b8194bc39f4bc0e89dee5c4265e4b1807c34a9aba453c" +dependencies = [ + "darling_core 0.13.4", + "darling_macro 0.13.4", +] + +[[package]] +name = "darling" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" +dependencies = [ + "darling_core 0.20.11", + "darling_macro 0.20.11", +] + +[[package]] +name = "darling_core" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "859d65a907b6852c9361e3185c862aae7fafd2887876799fa55f5f99dc40d610" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim 0.10.0", + "syn 1.0.109", +] + +[[package]] +name = "darling_core" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim 0.11.1", + "syn 2.0.117", +] + +[[package]] +name = "darling_macro" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c972679f83bdf9c42bd905396b6c3588a843a17f0f16dfcfa3e2c5d57441835" +dependencies = [ + "darling_core 0.13.4", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "darling_macro" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" +dependencies = [ + "darling_core 0.20.11", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "data-encoding" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7a1e2f27636f116493b8b860f5546edb47c8d8f8ea73e1d2a20be88e28d1fea" + +[[package]] +name = "der" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" +dependencies = [ + "const-oid", + "pem-rfc7468", + "zeroize", +] + +[[package]] +name = "deranged" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" +dependencies = [ + "powerfmt", +] + +[[package]] +name = "derivative" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "derive_builder" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "507dfb09ea8b7fa618fcf76e953f4f5e192547945816d5358edffe39f6f94947" +dependencies = [ + "derive_builder_macro", +] + +[[package]] +name = "derive_builder_core" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d5bcf7b024d6835cfb3d473887cd966994907effbe9227e8c8219824d06c4e8" +dependencies = [ + "darling 0.20.11", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "derive_builder_macro" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab63b0e2bf4d5928aff72e83a7dace85d7bba5fe12dcc3c5a572d78caffd3f3c" +dependencies = [ + "derive_builder_core", + "syn 2.0.117", +] + +[[package]] +name = "derive_more" +version = "0.99.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6edb4b64a43d977b8e99788fe3a04d483834fba1215a7e02caa415b626497f7f" +dependencies = [ + "convert_case", + "proc-macro2", + "quote", + "rustc_version 0.4.1", + "syn 2.0.117", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "const-oid", + "crypto-common", + "subtle", +] + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "dotenv" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77c90badedccf4105eca100756a0b1289e191f6fcbdadd3cee1d2f614f97da8f" + +[[package]] +name = "dotenvy" +version = "0.15.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" +dependencies = [ + "serde", +] + +[[package]] +name = "enum-as-inner" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21cdad81446a7f7dc43f6a77409efeb9733d2fa65553efef6018ef257c959b73" +dependencies = [ + "heck 0.4.1", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "etcetera" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "136d1b5283a1ab77bd9257427ffd09d8667ced0570b6f938942bc7568ed5b943" +dependencies = [ + "cfg-if", + "home", + "windows-sys 0.48.0", +] + +[[package]] +name = "event-listener" +version = "5.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab" +dependencies = [ + "concurrent-queue", + "parking", + "pin-project-lite", +] + +[[package]] +name = "filetime" +version = "0.2.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f98844151eee8917efc50bd9e8318cb963ae8b297431495d3f758616ea5c57db" +dependencies = [ + "cfg-if", + "libc", + "libredox", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "flate2" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "flume" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da0e4dd2a88388a1f4ccc7c9ce104604dab68d9f408dc34cd45823d5a9069095" +dependencies = [ + "futures-core", + "futures-sink", + "spin", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "fsevent-sys" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76ee7a02da4d231650c7cea31349b889be2f45ddb3ef3032d2ec8185f6313fd2" +dependencies = [ + "libc", +] + +[[package]] +name = "funty" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" + +[[package]] +name = "futures" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-executor" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-intrusive" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d930c203dd0b6ff06e0201a4a2fe9149b43c684fd4420555b26d21b1a02956f" +dependencies = [ + "futures-core", + "lock_api", + "parking_lot", +] + +[[package]] +name = "futures-io" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" + +[[package]] +name = "futures-macro" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "futures-sink" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" + +[[package]] +name = "futures-task" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "r-efi 5.3.0", + "wasip2", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" +dependencies = [ + "cfg-if", + "libc", + "r-efi 6.0.0", + "wasip2", + "wasip3", +] + +[[package]] +name = "h2" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f44da3a8150a6703ed5d34e164b875fd14c2cdab9af1252a9a1020bde2bdc54" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http", + "indexmap 2.13.0", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" + +[[package]] +name = "hashlink" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1" +dependencies = [ + "hashbrown 0.15.5", +] + +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hkdf" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" +dependencies = [ + "hmac", +] + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + +[[package]] +name = "home" +version = "0.5.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc627f471c528ff0c4a49e1d5e60450c8f6461dd6d10ba9dcd3a61d3dff7728d" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "http" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "hyper" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "h2", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "pin-utils", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" +dependencies = [ + "http", + "hyper", + "hyper-util", + "rustls 0.23.37", + "rustls-pki-types", + "tokio", + "tokio-rustls 0.26.4", + "tower-service", + "webpki-roots 1.0.6", +] + +[[package]] +name = "hyper-timeout" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b90d566bffbce6a75bd8b09a05aa8c2cb1fabb6cb348f8840c9e4c90a0d83b0" +dependencies = [ + "hyper", + "hyper-util", + "pin-project-lite", + "tokio", + "tower-service", +] + +[[package]] +name = "hyper-util" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" +dependencies = [ + "base64 0.22.1", + "bytes", + "futures-channel", + "futures-util", + "http", + "http-body", + "hyper", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2 0.6.3", + "tokio", + "tower-service", + "tracing", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "icu_collections" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" +dependencies = [ + "displaydoc", + "potential_utf", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" + +[[package]] +name = "icu_properties" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af" + +[[package]] +name = "icu_provider" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "idna" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "418a0a6fab821475f634efe3ccc45c013f742efe03d853e8d3355d5cb850ecf8" +dependencies = [ + "matches", + "unicode-bidi", + "unicode-normalization", +] + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", +] + +[[package]] +name = "indexmap" +version = "2.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" +dependencies = [ + "equivalent", + "hashbrown 0.16.1", + "serde", + "serde_core", +] + +[[package]] +name = "inotify" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8069d3ec154eb856955c1c0fbffefbf5f3c40a104ec912d4797314c1801abff" +dependencies = [ + "bitflags 1.3.2", + "inotify-sys", + "libc", +] + +[[package]] +name = "inotify-sys" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e05c02b5e89bff3b946cedeca278abc628fe811e604f027c45a8aa3cf793d0eb" +dependencies = [ + "libc", +] + +[[package]] +name = "ipconfig" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b58db92f96b720de98181bbbe63c831e87005ab460c1bf306eb2622b4707997f" +dependencies = [ + "socket2 0.5.10", + "widestring", + "windows-sys 0.48.0", + "winreg", +] + +[[package]] +name = "ipnet" +version = "2.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" + +[[package]] +name = "iri-string" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c91338f0783edbd6195decb37bae672fd3b165faffb89bf7b9e6942f8b1a731a" +dependencies = [ + "memchr", + "serde", +] + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" + +[[package]] +name = "js-sys" +version = "0.3.91" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b49715b7073f385ba4bc528e5747d02e66cb39c6146efb66b781f131f0fb399c" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "kqueue" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eac30106d7dce88daf4a3fcb4879ea939476d5074a9b7ddd0fb97fa4bed5596a" +dependencies = [ + "kqueue-sys", + "libc", +] + +[[package]] +name = "kqueue-sys" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed9625ffda8729b85e45cf04090035ac368927b8cebc34898e7c120f52e4838b" +dependencies = [ + "bitflags 1.3.2", + "libc", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +dependencies = [ + "spin", +] + +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + +[[package]] +name = "libc" +version = "0.2.183" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d" + +[[package]] +name = "libm" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" + +[[package]] +name = "libredox" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1744e39d1d6a9948f4f388969627434e31128196de472883b39f148769bfe30a" +dependencies = [ + "bitflags 2.11.0", + "libc", + "plain", + "redox_syscall 0.7.3", +] + +[[package]] +name = "libsqlite3-sys" +version = "0.30.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e99fb7a497b1e3339bc746195567ed8d3e24945ecd636e3619d20b9de9e9149" +dependencies = [ + "cc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "linked-hash-map" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" + +[[package]] +name = "litemap" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "lru-cache" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31e24f1ad8321ca0e8a1e0ac13f23cb668e6f5466c2c57319f6a5cf1cc8e3b1c" +dependencies = [ + "linked-hash-map", +] + +[[package]] +name = "lru-slab" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" + +[[package]] +name = "matches" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2532096657941c2fea9c289d370a250971c689d4f143798ff67113ec042024a5" + +[[package]] +name = "matchit" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" + +[[package]] +name = "md-5" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" +dependencies = [ + "cfg-if", + "digest", +] + +[[package]] +name = "memchr" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "mio" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" +dependencies = [ + "libc", + "log", + "wasi", + "windows-sys 0.48.0", +] + +[[package]] +name = "mio" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "momentry_core" +version = "0.1.0" +dependencies = [ + "anyhow", + "async-trait", + "axum", + "clap", + "dotenv", + "hex", + "mongodb", + "notify", + "qdrant-client", + "redis", + "sdl2", + "serde", + "serde_json", + "sha2", + "sqlx", + "thiserror 1.0.69", + "tokio", + "tower 0.4.13", + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "mongodb" +version = "2.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef206acb1b72389b49bc9985efe7eb1f8a9bb18e5680d262fac26c07f44025f1" +dependencies = [ + "async-trait", + "base64 0.13.1", + "bitflags 1.3.2", + "bson", + "chrono", + "derivative", + "derive_more", + "futures-core", + "futures-executor", + "futures-io", + "futures-util", + "hex", + "hmac", + "lazy_static", + "md-5", + "pbkdf2", + "percent-encoding", + "rand 0.8.5", + "rustc_version_runtime", + "rustls 0.21.12", + "rustls-pemfile 1.0.4", + "serde", + "serde_bytes", + "serde_with", + "sha-1", + "sha2", + "socket2 0.4.10", + "stringprep", + "strsim 0.10.0", + "take_mut", + "thiserror 1.0.69", + "tokio", + "tokio-rustls 0.24.1", + "tokio-util", + "trust-dns-proto", + "trust-dns-resolver", + "typed-builder", + "uuid", + "webpki-roots 0.25.4", +] + +[[package]] +name = "notify" +version = "6.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6205bd8bb1e454ad2e27422015fb5e4f2bcc7e08fa8f27058670d208324a4d2d" +dependencies = [ + "bitflags 2.11.0", + "crossbeam-channel", + "filetime", + "fsevent-sys", + "inotify", + "kqueue", + "libc", + "log", + "mio 0.8.11", + "walkdir", + "windows-sys 0.48.0", +] + +[[package]] +name = "nu-ansi-term" +version = "0.50.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "num-bigint-dig" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e661dda6640fad38e827a6d4a310ff4763082116fe217f279885c97f511bb0b7" +dependencies = [ + "lazy_static", + "libm", + "num-integer", + "num-iter", + "num-traits", + "rand 0.8.5", + "smallvec", + "zeroize", +] + +[[package]] +name = "num-conv" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf97ec579c3c42f953ef76dbf8d55ac91fb219dde70e49aa4a6b7d74e9919050" + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-iter" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", + "libm", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + +[[package]] +name = "openssl-probe" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" + +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall 0.5.18", + "smallvec", + "windows-link", +] + +[[package]] +name = "pbkdf2" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83a0692ec44e4cf1ef28ca317f14f8f07da2d95ec3fa01f86e4467b725e60917" +dependencies = [ + "digest", +] + +[[package]] +name = "pem-rfc7468" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" +dependencies = [ + "base64ct", +] + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "pin-project" +version = "1.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1749c7ed4bcaf4c3d0a3efc28538844fb29bcdd7d2b67b2be7e20ba861ff517" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b20ed30f105399776b9c883e68e536ef602a16ae6f596d2c473591d6ad64c6" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pkcs1" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f" +dependencies = [ + "der", + "pkcs8", + "spki", +] + +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der", + "spki", +] + +[[package]] +name = "pkg-config" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" + +[[package]] +name = "plain" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" + +[[package]] +name = "potential_utf" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" +dependencies = [ + "zerovec", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn 2.0.117", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "prost" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2796faa41db3ec313a31f7624d9286acf277b52de526150b7e69f3debf891ee5" +dependencies = [ + "bytes", + "prost-derive", +] + +[[package]] +name = "prost-derive" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a56d757972c98b346a9b766e3f02746cde6dd1cd1d1d563472929fdd74bec4d" +dependencies = [ + "anyhow", + "itertools", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "prost-types" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52c2c1bf36ddb1a1c396b3601a3cec27c2462e45f07c386894ec3ccf5332bd16" +dependencies = [ + "prost", +] + +[[package]] +name = "qdrant-client" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5d0a9b168ecf8f30a3eb7e8f4766e3050701242ffbe99838b58e6c4251e7211" +dependencies = [ + "anyhow", + "derive_builder", + "futures", + "futures-util", + "parking_lot", + "prost", + "prost-types", + "reqwest", + "semver 1.0.27", + "serde", + "serde_json", + "thiserror 1.0.69", + "tokio", + "tonic", +] + +[[package]] +name = "quinn" +version = "0.11.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" +dependencies = [ + "bytes", + "cfg_aliases", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash", + "rustls 0.23.37", + "socket2 0.6.3", + "thiserror 2.0.18", + "tokio", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-proto" +version = "0.11.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "434b42fec591c96ef50e21e886936e66d3cc3f737104fdb9b737c40ffb94c098" +dependencies = [ + "bytes", + "getrandom 0.3.4", + "lru-slab", + "rand 0.9.2", + "ring", + "rustc-hash", + "rustls 0.23.37", + "rustls-pki-types", + "slab", + "thiserror 2.0.18", + "tinyvec", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-udp" +version = "0.5.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd" +dependencies = [ + "cfg_aliases", + "libc", + "once_cell", + "socket2 0.6.3", + "tracing", + "windows-sys 0.60.2", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "radium" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.5", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.5", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.17", +] + +[[package]] +name = "rand_core" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +dependencies = [ + "getrandom 0.3.4", +] + +[[package]] +name = "redis" +version = "0.25.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0d7a6955c7511f60f3ba9e86c6d02b3c3f144f8c24b288d1f4e18074ab8bbec" +dependencies = [ + "async-trait", + "bytes", + "combine", + "futures-util", + "itoa", + "percent-encoding", + "pin-project-lite", + "ryu", + "sha1_smol", + "socket2 0.5.10", + "tokio", + "tokio-util", + "url", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags 2.11.0", +] + +[[package]] +name = "redox_syscall" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce70a74e890531977d37e532c34d45e9055d2409ed08ddba14529471ed0be16" +dependencies = [ + "bitflags 2.11.0", +] + +[[package]] +name = "reqwest" +version = "0.12.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" +dependencies = [ + "base64 0.22.1", + "bytes", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-util", + "js-sys", + "log", + "percent-encoding", + "pin-project-lite", + "quinn", + "rustls 0.23.37", + "rustls-pki-types", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tokio-rustls 0.26.4", + "tokio-util", + "tower 0.5.3", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-streams", + "web-sys", + "webpki-roots 1.0.6", +] + +[[package]] +name = "resolv-conf" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e061d1b48cb8d38042de4ae0a7a6401009d6143dc80d2e2d6f31f0bdd6470c7" + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.17", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "rsa" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8573f03f5883dcaebdfcf4725caa1ecb9c15b2ef50c43a07b816e06799bb12d" +dependencies = [ + "const-oid", + "digest", + "num-bigint-dig", + "num-integer", + "num-traits", + "pkcs1", + "pkcs8", + "rand_core 0.6.4", + "signature", + "spki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustc-hash" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" + +[[package]] +name = "rustc_version" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "138e3e0acb6c9fb258b19b67cb8abd63c00679d2851805ea151465464fe9030a" +dependencies = [ + "semver 0.9.0", +] + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver 1.0.27", +] + +[[package]] +name = "rustc_version_runtime" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d31b7153270ebf48bf91c65ae5b0c00e749c4cfad505f66530ac74950249582f" +dependencies = [ + "rustc_version 0.2.3", + "semver 0.9.0", +] + +[[package]] +name = "rustls" +version = "0.21.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f56a14d1f48b391359b22f731fd4bd7e43c97f3c50eee276f3aa09c94784d3e" +dependencies = [ + "log", + "ring", + "rustls-webpki 0.101.7", + "sct", +] + +[[package]] +name = "rustls" +version = "0.23.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "758025cb5fccfd3bc2fd74708fd4682be41d99e5dff73c377c0646c6012c73a4" +dependencies = [ + "log", + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki 0.103.9", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-native-certs" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "612460d5f7bea540c490b2b6395d8e34a953e52b491accd6c86c8164c5932a63" +dependencies = [ + "openssl-probe", + "rustls-pki-types", + "schannel", + "security-framework", +] + +[[package]] +name = "rustls-pemfile" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c" +dependencies = [ + "base64 0.21.7", +] + +[[package]] +name = "rustls-pemfile" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "rustls-pki-types" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd" +dependencies = [ + "web-time", + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.101.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765" +dependencies = [ + "ring", + "untrusted", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7df23109aa6c1567d1c575b9952556388da57401e4ace1d15f79eedad0d8f53" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "schannel" +version = "0.1.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "sct" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414" +dependencies = [ + "ring", + "untrusted", +] + +[[package]] +name = "sdl2" +version = "0.38.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d42407afc6a8ab67e36f92e80b8ba34cbdc55aaeed05249efe9a2e8d0e9feef" +dependencies = [ + "bitflags 1.3.2", + "lazy_static", + "libc", + "sdl2-sys", +] + +[[package]] +name = "sdl2-sys" +version = "0.38.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ff61407fc75d4b0bbc93dc7e4d6c196439965fbef8e4a4f003a36095823eac0" +dependencies = [ + "cfg-if", + "libc", + "version-compare", +] + +[[package]] +name = "security-framework" +version = "3.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" +dependencies = [ + "bitflags 2.11.0", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "semver" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d7eb9ef2c18661902cc47e535f9bc51b78acd254da71d375c2f6720d9a40403" +dependencies = [ + "semver-parser", +] + +[[package]] +name = "semver" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" + +[[package]] +name = "semver-parser" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_bytes" +version = "0.11.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5d440709e79d88e51ac01c4b72fc6cb7314017bb7da9eeff678aa94c10e3ea8" +dependencies = [ + "serde", + "serde_core", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "serde_json" +version = "1.0.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +dependencies = [ + "indexmap 2.13.0", + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_path_to_error" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457" +dependencies = [ + "itoa", + "serde", + "serde_core", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "serde_with" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "678b5a069e50bf00ecd22d0cd8ddf7c236f68581b03db652061ed5eb13a312ff" +dependencies = [ + "serde", + "serde_with_macros", +] + +[[package]] +name = "serde_with_macros" +version = "1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e182d6ec6f05393cc0e5ed1bf81ad6db3a8feedf8ee515ecdd369809bcce8082" +dependencies = [ + "darling 0.13.4", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "sha-1" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5058ada175748e33390e40e872bd0fe59a19f265d0158daa551c5a88a76009c" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sha1_smol" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbfa15b3dddfee50a0fff136974b3e1bde555604ba463834a7eb7deb6417705d" + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + +[[package]] +name = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +dependencies = [ + "digest", + "rand_core 0.6.4", +] + +[[package]] +name = "simd-adler32" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2" + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" +dependencies = [ + "serde", +] + +[[package]] +name = "socket2" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7916fc008ca5542385b89a3d3ce689953c143e9304a9bf8beec1de48994c0d" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "socket2" +version = "0.5.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "socket2" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +dependencies = [ + "lock_api", +] + +[[package]] +name = "spki" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der", +] + +[[package]] +name = "sqlx" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fefb893899429669dcdd979aff487bd78f4064e5e7907e4269081e0ef7d97dc" +dependencies = [ + "sqlx-core", + "sqlx-macros", + "sqlx-mysql", + "sqlx-postgres", + "sqlx-sqlite", +] + +[[package]] +name = "sqlx-core" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee6798b1838b6a0f69c007c133b8df5866302197e404e8b6ee8ed3e3a5e68dc6" +dependencies = [ + "base64 0.22.1", + "bytes", + "crc", + "crossbeam-queue", + "either", + "event-listener", + "futures-core", + "futures-intrusive", + "futures-io", + "futures-util", + "hashbrown 0.15.5", + "hashlink", + "indexmap 2.13.0", + "log", + "memchr", + "once_cell", + "percent-encoding", + "serde", + "serde_json", + "sha2", + "smallvec", + "thiserror 2.0.18", + "tokio", + "tokio-stream", + "tracing", + "url", +] + +[[package]] +name = "sqlx-macros" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2d452988ccaacfbf5e0bdbc348fb91d7c8af5bee192173ac3636b5fb6e6715d" +dependencies = [ + "proc-macro2", + "quote", + "sqlx-core", + "sqlx-macros-core", + "syn 2.0.117", +] + +[[package]] +name = "sqlx-macros-core" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19a9c1841124ac5a61741f96e1d9e2ec77424bf323962dd894bdb93f37d5219b" +dependencies = [ + "dotenvy", + "either", + "heck 0.5.0", + "hex", + "once_cell", + "proc-macro2", + "quote", + "serde", + "serde_json", + "sha2", + "sqlx-core", + "sqlx-mysql", + "sqlx-postgres", + "sqlx-sqlite", + "syn 2.0.117", + "tokio", + "url", +] + +[[package]] +name = "sqlx-mysql" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa003f0038df784eb8fecbbac13affe3da23b45194bd57dba231c8f48199c526" +dependencies = [ + "atoi", + "base64 0.22.1", + "bitflags 2.11.0", + "byteorder", + "bytes", + "crc", + "digest", + "dotenvy", + "either", + "futures-channel", + "futures-core", + "futures-io", + "futures-util", + "generic-array", + "hex", + "hkdf", + "hmac", + "itoa", + "log", + "md-5", + "memchr", + "once_cell", + "percent-encoding", + "rand 0.8.5", + "rsa", + "serde", + "sha1", + "sha2", + "smallvec", + "sqlx-core", + "stringprep", + "thiserror 2.0.18", + "tracing", + "whoami", +] + +[[package]] +name = "sqlx-postgres" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db58fcd5a53cf07c184b154801ff91347e4c30d17a3562a635ff028ad5deda46" +dependencies = [ + "atoi", + "base64 0.22.1", + "bitflags 2.11.0", + "byteorder", + "crc", + "dotenvy", + "etcetera", + "futures-channel", + "futures-core", + "futures-util", + "hex", + "hkdf", + "hmac", + "home", + "itoa", + "log", + "md-5", + "memchr", + "once_cell", + "rand 0.8.5", + "serde", + "serde_json", + "sha2", + "smallvec", + "sqlx-core", + "stringprep", + "thiserror 2.0.18", + "tracing", + "whoami", +] + +[[package]] +name = "sqlx-sqlite" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2d12fe70b2c1b4401038055f90f151b78208de1f9f89a7dbfd41587a10c3eea" +dependencies = [ + "atoi", + "flume", + "futures-channel", + "futures-core", + "futures-executor", + "futures-intrusive", + "futures-util", + "libsqlite3-sys", + "log", + "percent-encoding", + "serde", + "serde_urlencoded", + "sqlx-core", + "thiserror 2.0.18", + "tracing", + "url", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "stringprep" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b4df3d392d81bd458a8a621b8bffbd2302a12ffe288a9d931670948749463b1" +dependencies = [ + "unicode-bidi", + "unicode-normalization", + "unicode-properties", +] + +[[package]] +name = "strsim" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "take_mut" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f764005d11ee5f36500a149ace24e00e3da98b0158b3e2d53a7495660d3f4d60" + +[[package]] +name = "tap" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl 2.0.18", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "thread_local" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "time" +version = "0.3.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" +dependencies = [ + "deranged", + "itoa", + "num-conv", + "powerfmt", + "serde_core", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" + +[[package]] +name = "time-macros" +version = "0.2.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tinystr" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tinyvec" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.50.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27ad5e34374e03cfffefc301becb44e9dc3c17584f414349ebe29ed26661822d" +dependencies = [ + "bytes", + "libc", + "mio 1.1.1", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2 0.6.3", + "tokio-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-macros" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c55a2eff8b69ce66c84f85e1da1c233edc36ceb85a2058d11b0d6a3c7e7569c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "tokio-rustls" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081" +dependencies = [ + "rustls 0.21.12", + "tokio", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" +dependencies = [ + "rustls 0.23.37", + "tokio", +] + +[[package]] +name = "tokio-stream" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32da49809aab5c3bc678af03902d4ccddea2a87d028d86392a4b1560c6906c70" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.7.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" +dependencies = [ + "bytes", + "futures-core", + "futures-io", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tonic" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877c5b330756d856ffcc4553ab34a5684481ade925ecc54bcd1bf02b1d0d4d52" +dependencies = [ + "async-stream", + "async-trait", + "axum", + "base64 0.22.1", + "bytes", + "flate2", + "h2", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-timeout", + "hyper-util", + "percent-encoding", + "pin-project", + "prost", + "rustls-native-certs", + "rustls-pemfile 2.2.0", + "socket2 0.5.10", + "tokio", + "tokio-rustls 0.26.4", + "tokio-stream", + "tower 0.4.13", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c" +dependencies = [ + "futures-core", + "futures-util", + "indexmap 1.9.3", + "pin-project", + "pin-project-lite", + "rand 0.8.5", + "slab", + "tokio", + "tokio-util", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-http" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" +dependencies = [ + "bitflags 2.11.0", + "bytes", + "futures-util", + "http", + "http-body", + "iri-string", + "pin-project-lite", + "tower 0.5.3", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "log", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e" +dependencies = [ + "nu-ansi-term", + "sharded-slab", + "smallvec", + "thread_local", + "tracing-core", + "tracing-log", +] + +[[package]] +name = "trust-dns-proto" +version = "0.21.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c31f240f59877c3d4bb3b3ea0ec5a6a0cff07323580ff8c7a605cd7d08b255d" +dependencies = [ + "async-trait", + "cfg-if", + "data-encoding", + "enum-as-inner", + "futures-channel", + "futures-io", + "futures-util", + "idna 0.2.3", + "ipnet", + "lazy_static", + "log", + "rand 0.8.5", + "smallvec", + "thiserror 1.0.69", + "tinyvec", + "tokio", + "url", +] + +[[package]] +name = "trust-dns-resolver" +version = "0.21.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4ba72c2ea84515690c9fcef4c6c660bb9df3036ed1051686de84605b74fd558" +dependencies = [ + "cfg-if", + "futures-util", + "ipconfig", + "lazy_static", + "log", + "lru-cache", + "parking_lot", + "resolv-conf", + "smallvec", + "thiserror 1.0.69", + "tokio", + "trust-dns-proto", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "typed-builder" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89851716b67b937e393b3daa8423e67ddfc4bbbf1654bcf05488e95e0828db0c" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "typenum" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" + +[[package]] +name = "unicode-bidi" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-normalization" +version = "0.1.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fd4f6878c9cb28d874b009da9e8d183b5abc80117c40bbd187a1fde336be6e8" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-properties" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna 1.1.0", + "percent-encoding", + "serde", +] + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "uuid" +version = "1.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a68d3c8f01c0cfa54a75291d83601161799e4a89a39e0929f4b0354d88757a37" +dependencies = [ + "getrandom 0.4.2", + "js-sys", + "serde_core", + "wasm-bindgen", +] + +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "version-compare" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "579a42fc0b8e0c63b76519a339be31bed574929511fa53c1a3acae26eb258f29" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.2+wasi-0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" + +[[package]] +name = "wasm-bindgen" +version = "0.2.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6532f9a5c1ece3798cb1c2cfdba640b9b3ba884f5db45973a6f442510a87d38e" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9c5522b3a28661442748e09d40924dfb9ca614b21c00d3fd135720e48b67db8" +dependencies = [ + "cfg-if", + "futures-util", + "js-sys", + "once_cell", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18a2d50fcf105fb33bb15f00e7a77b772945a2ee45dcf454961fd843e74c18e6" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03ce4caeaac547cdf713d280eda22a730824dd11e6b8c3ca9e42247b25c631e3" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 2.0.117", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75a326b8c223ee17883a4251907455a2431acc2791c98c26279376490c378c16" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasm-encoder" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap 2.13.0", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "wasm-streams" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags 2.11.0", + "hashbrown 0.15.5", + "indexmap 2.13.0", + "semver 1.0.27", +] + +[[package]] +name = "web-sys" +version = "0.3.91" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "854ba17bb104abfb26ba36da9729addc7ce7f06f5c0f90f3c391f8461cca21f9" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webpki-roots" +version = "0.25.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f20c57d8d7db6d3b86154206ae5d8fba62dd39573114de97c2cb0578251f8e1" + +[[package]] +name = "webpki-roots" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22cfaf3c063993ff62e73cb4311efde4db1efb31ab78a3e5c457939ad5cc0bed" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "whoami" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d4a4db5077702ca3015d3d02d74974948aba2ad9e12ab7df718ee64ccd7e97d" +dependencies = [ + "libredox", + "wasite", +] + +[[package]] +name = "widestring" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72069c3113ab32ab29e5584db3c6ec55d416895e60715417b5b883a357c3e471" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.5", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + +[[package]] +name = "winreg" +version = "0.50.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" +dependencies = [ + "cfg-if", + "windows-sys 0.48.0", +] + +[[package]] +name = "wit-bindgen" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +dependencies = [ + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen-core" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck 0.5.0", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +dependencies = [ + "anyhow", + "heck 0.5.0", + "indexmap 2.13.0", + "prettyplease", + "syn 2.0.117", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn 2.0.117", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", + "bitflags 2.11.0", + "indexmap 2.13.0", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap 2.13.0", + "log", + "semver 1.0.27", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] + +[[package]] +name = "writeable" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" + +[[package]] +name = "wyz" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" +dependencies = [ + "tap", +] + +[[package]] +name = "yoke" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", + "synstructure", +] + +[[package]] +name = "zerocopy" +version = "0.8.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2578b716f8a7a858b7f02d5bd870c14bf4ddbbcf3a4c05414ba6503640505e3" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e6cc098ea4d3bd6246687de65af3f920c430e236bee1e3bf2e441463f08a02f" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "zerofrom" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" + +[[package]] +name = "zerotrie" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..e51f889 --- /dev/null +++ b/Cargo.toml @@ -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" diff --git a/MOMENTRY_INTEGRATION_GUIDE.md b/MOMENTRY_INTEGRATION_GUIDE.md new file mode 100644 index 0000000..26d1425 --- /dev/null +++ b/MOMENTRY_INTEGRATION_GUIDE.md @@ -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 + + + + + Label + com.momentry.{service_name} + UserName + accusys + WorkingDirectory + /Users/accusys/momentry/var/{service_name} + ProgramArguments + + /opt/homebrew/bin/executable + + EnvironmentVariables + + PATH + /opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin + + RunAtLoad + + KeepAlive + + StandardOutPath + /Users/accusys/momentry/log/{service_name}.log + StandardErrorPath + /Users/accusys/momentry/log/{service_name}.error.log + + +``` + +### 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 | 新增服務添加規範 (第九章)、模板文件 | diff --git a/docs/INSTALL_CADDY.md b/docs/INSTALL_CADDY.md new file mode 100644 index 0000000..19a24c9 --- /dev/null +++ b/docs/INSTALL_CADDY.md @@ -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 +``` + +### 需要重新載入配置 + +```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/ diff --git a/docs/INSTALL_GITEA.md b/docs/INSTALL_GITEA.md new file mode 100644 index 0000000..94549b1 --- /dev/null +++ b/docs/INSTALL_GITEA.md @@ -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 +``` + +### 需要重新載入 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/ diff --git a/docs/INSTALL_MARIADB.md b/docs/INSTALL_MARIADB.md new file mode 100644 index 0000000..f4a8c36 --- /dev/null +++ b/docs/INSTALL_MARIADB.md @@ -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 +``` + +### 需要重新載入 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/ diff --git a/docs/INSTALL_MONGODB.md b/docs/INSTALL_MONGODB.md new file mode 100644 index 0000000..64a21cb --- /dev/null +++ b/docs/INSTALL_MONGODB.md @@ -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 + +# 確認停止 +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 + +# 啟動 (後台) +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 +``` + +--- + +## 檔案位置 + +| 類型 | 路徑 | 說明 | +|------|------|------| +| 數據目錄 | `/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 (共用 - 不要刪除!) diff --git a/docs/INSTALL_N8N.md b/docs/INSTALL_N8N.md new file mode 100644 index 0000000..6947582 --- /dev/null +++ b/docs/INSTALL_N8N.md @@ -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 +``` + +### 數據庫連線失敗 + +```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 diff --git a/docs/INSTALL_OLLAMA.md b/docs/INSTALL_OLLAMA.md new file mode 100644 index 0000000..2e7e915 --- /dev/null +++ b/docs/INSTALL_OLLAMA.md @@ -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 +``` + +### 需要重新載入 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/ diff --git a/docs/INSTALL_PHP.md b/docs/INSTALL_PHP.md new file mode 100644 index 0000000..9282c72 --- /dev/null +++ b/docs/INSTALL_PHP.md @@ -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 +``` + +### 需要重新載入 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/ diff --git a/docs/INSTALL_POSTGRESQL.md b/docs/INSTALL_POSTGRESQL.md new file mode 100644 index 0000000..d463f22 --- /dev/null +++ b/docs/INSTALL_POSTGRESQL.md @@ -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 +``` + +### 需要重新載入 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/ diff --git a/docs/INSTALL_QDRANT.md b/docs/INSTALL_QDRANT.md new file mode 100644 index 0000000..7ec8c76 --- /dev/null +++ b/docs/INSTALL_QDRANT.md @@ -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 + +# 確認停止 +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 +``` + +### 需要重新載入 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/ diff --git a/docs/INSTALL_REDIS.md b/docs/INSTALL_REDIS.md new file mode 100644 index 0000000..f8526be --- /dev/null +++ b/docs/INSTALL_REDIS.md @@ -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 +``` + +### 需要重新載入 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/ diff --git a/docs/INSTALL_RUSTDESK.md b/docs/INSTALL_RUSTDESK.md new file mode 100644 index 0000000..71c2552 --- /dev/null +++ b/docs/INSTALL_RUSTDESK.md @@ -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 +``` + +### 需要重新載入 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 添加服務器公式 diff --git a/docs/INSTALL_SFTPGO.md b/docs/INSTALL_SFTPGO.md new file mode 100644 index 0000000..df1059f --- /dev/null +++ b/docs/INSTALL_SFTPGO.md @@ -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 +``` + +### 需要重新載入 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/ diff --git a/docs/NODEJS.md b/docs/NODEJS.md new file mode 100644 index 0000000..8c9bb61 --- /dev/null +++ b/docs/NODEJS.md @@ -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 + +# 例如安裝 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 +ProgramArguments + + /opt/homebrew/opt/node@22/bin/node + /opt/homebrew/lib/node_modules/n8n/bin/n8n + start + + +EnvironmentVariables + + PATH + /opt/homebrew/opt/node@22/bin:/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin + +``` + +### 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 | grep bin/node + +# 3. 確認路徑優先順序 +echo $PATH + +# 4. 查看 launchctl 服務的環境變數 +sudo launchctl list | grep + +# 5. 檢查 Port 佔用 +lsof -i : +``` + +### 預防措施 + +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 | 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 +``` + +--- + +## 版本切換腳本 + +建立快速切換腳本 `~/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 | 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 監控腳本 diff --git a/docs/PYTHON.md b/docs/PYTHON.md new file mode 100644 index 0000000..2ce055b --- /dev/null +++ b/docs/PYTHON.md @@ -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 + +作者: 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 監控腳本 diff --git a/docs/SERVICES.md b/docs/SERVICES.md new file mode 100644 index 0000000..56f5461 --- /dev/null +++ b/docs/SERVICES.md @@ -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' + + + + + Label + com.momentry.postgresql + ProgramArguments + + /opt/homebrew/opt/postgresql@18/bin/pg_ctl + -D + /opt/homebrew/var/postgresql@18 + -l + /opt/homebrew/var/postgresql@18/logfile + start + + RunAtLoad + + KeepAlive + + WorkingDirectory + /opt/homebrew/var/postgresql@18 + UserName + accusys + + +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' + + + + + Label + com.momentry.redis + UserName + accusys + WorkingDirectory + /Users/accusys/momentry/var/redis + ProgramArguments + + /opt/homebrew/opt/redis/bin/redis-server + /opt/homebrew/etc/redis.conf + + EnvironmentVariables + + REDIS_PASSWORD + accusys + + RunAtLoad + + KeepAlive + + StandardOutPath + /Users/accusys/momentry/log/redis.log + StandardErrorPath + /Users/accusys/momentry/log/redis.error.log + + +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' + + + + + Label + com.momentry.ollama + UserName + accusys + WorkingDirectory + /Users/accusys/momentry/var/ollama + ProgramArguments + + /opt/homebrew/bin/ollama + serve + + EnvironmentVariables + + OLLAMA_HOST + 0.0.0.0:11434 + OLLAMA_MODELS + /Users/accusys/momentry/var/ollama/models + + RunAtLoad + + KeepAlive + + StandardOutPath + /Users/accusys/momentry/log/ollama.log + StandardErrorPath + /Users/accusys/momentry/log/ollama.error.log + + +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/ | diff --git a/docs/SERVICE_ADDITION_GUIDE.md b/docs/SERVICE_ADDITION_GUIDE.md new file mode 100644 index 0000000..52133a1 --- /dev/null +++ b/docs/SERVICE_ADDITION_GUIDE.md @@ -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 + + + + + Label + com.momentry.{service_name} + + UserName + accusys + + WorkingDirectory + /Users/accusys/momentry/var/{service_name} + + ProgramArguments + + /path/to/executable + --arg1 + value1 + + + EnvironmentVariables + + PATH + /opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin + + + + RunAtLoad + + + KeepAlive + + + StandardOutPath + /Users/accusys/momentry/log/{service_name}.log + + StandardErrorPath + /Users/accusys/momentry/log/{service_name}.error.log + + +``` + +### 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' + + + + + Label + com.momentry.SERVICE_NAME + UserName + accusys + WorkingDirectory + /Users/accusys/momentry/var/SERVICE_NAME + ProgramArguments + + /path/to/executable + + RunAtLoad + + KeepAlive + + StandardOutPath + /Users/accusys/momentry/log/SERVICE_NAME.log + StandardErrorPath + /Users/accusys/momentry/log/SERVICE_NAME.error.log + + +EOF +``` + +--- + +## 十一、版本歷史 + +| 版本 | 日期 | 內容 | +|------|------|------| +| 1.0 | 2026-03-15 | 初始版本 | +| 2.0 | 2026-03-15 | 統一 Plist 位置、移除 root/用戶區分、加入運行方式分類 | +| 2.1 | 2026-03-15 | 新增服務備份作業、服務完整刪除作業 | diff --git a/momentry_runtime/build.sh b/momentry_runtime/build.sh new file mode 100755 index 0000000..05cfcbb --- /dev/null +++ b/momentry_runtime/build.sh @@ -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" diff --git a/momentry_runtime/check.sh b/momentry_runtime/check.sh new file mode 100755 index 0000000..be37633 --- /dev/null +++ b/momentry_runtime/check.sh @@ -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) diff --git a/momentry_runtime/env/momentry.env b/momentry_runtime/env/momentry.env new file mode 100644 index 0000000..09f86ed --- /dev/null +++ b/momentry_runtime/env/momentry.env @@ -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 diff --git a/momentry_runtime/plist/com.momentry.agent.plist b/momentry_runtime/plist/com.momentry.agent.plist new file mode 100644 index 0000000..132b34e --- /dev/null +++ b/momentry_runtime/plist/com.momentry.agent.plist @@ -0,0 +1,47 @@ + + + + + Label + com.momentry.agent + + ProgramArguments + + /Users/accusys/momentry_core_0.1/momentry_runtime/start.sh + + + RunAtLoad + + + KeepAlive + + + EnvironmentVariables + + HOME + /Users/accusys + USER + accusys + PATH + /opt/homebrew/bin:/opt/homebrew/sbin:/usr/local/bin:/usr/bin:/bin:/Users/accusys/.cargo/bin + DATABASE_URL + postgres://accusys@localhost:5432/momentry + REDIS_URL + redis://accusys:accusys@localhost:6379 + OLLAMA_HOST + http://localhost:11434 + + + StandardOutPath + /Users/accusys/momentry_core_0.1/momentry_runtime/logs/stdout.log + + StandardErrorPath + /Users/accusys/momentry_core_0.1/momentry_runtime/logs/stderr.log + + WorkingDirectory + /Users/accusys/momentry_core_0.1 + + ProcessType + Interactive + + diff --git a/momentry_runtime/plist/com.momentry.caddy.plist b/momentry_runtime/plist/com.momentry.caddy.plist new file mode 100644 index 0000000..b1876e6 --- /dev/null +++ b/momentry_runtime/plist/com.momentry.caddy.plist @@ -0,0 +1,42 @@ + + + + + Label + com.momentry.caddy + + UserName + root + + EnvironmentVariables + + HOME + /opt/homebrew/var/lib/caddy + XDG_DATA_HOME + /opt/homebrew/var/lib + + + WorkingDirectory + /Users/accusys/momentry/var/caddy + + ProgramArguments + + /opt/homebrew/opt/caddy/bin/caddy + run + --config + /Users/accusys/momentry/etc/Caddyfile + + + RunAtLoad + + + KeepAlive + + + StandardErrorPath + /Users/accusys/momentry/log/caddy.error.log + + StandardOutPath + /Users/accusys/momentry/log/caddy.log + + diff --git a/momentry_runtime/plist/com.momentry.gitea.plist b/momentry_runtime/plist/com.momentry.gitea.plist new file mode 100644 index 0000000..cc719cc --- /dev/null +++ b/momentry_runtime/plist/com.momentry.gitea.plist @@ -0,0 +1,44 @@ + + + + + Label + com.momentry.gitea + + UserName + accusys + + WorkingDirectory + /Users/accusys/momentry/var/gitea + + ProgramArguments + + /opt/homebrew/opt/gitea/bin/gitea + web + --config + /Users/accusys/momentry/etc/gitea/app.ini + + + EnvironmentVariables + + HOME + /Users/accusys + GITEA_WORK_DIR + /Users/accusys/momentry/var/gitea + PATH + /opt/homebrew/bin:/usr/bin:/bin:/usr/sbin:/sbin + + + RunAtLoad + + + KeepAlive + + + StandardOutPath + /Users/accusys/momentry/log/gitea.log + + StandardErrorPath + /Users/accusys/momentry/log/gitea.error.log + + diff --git a/momentry_runtime/plist/com.momentry.mariadb.plist b/momentry_runtime/plist/com.momentry.mariadb.plist new file mode 100644 index 0000000..73088f8 --- /dev/null +++ b/momentry_runtime/plist/com.momentry.mariadb.plist @@ -0,0 +1,32 @@ + + + + + Label + com.momentry.mariadb + + UserName + accusys + + WorkingDirectory + /Users/accusys/momentry/var/mariadb + + ProgramArguments + + /opt/homebrew/opt/mariadb/bin/mariadbd-safe + --datadir=/Users/accusys/momentry/var/mariadb + + + RunAtLoad + + + KeepAlive + + + StandardErrorPath + /Users/accusys/momentry/log/mariadb.error.log + + StandardOutPath + /Users/accusys/momentry/log/mariadb.log + + diff --git a/momentry_runtime/plist/com.momentry.mongodb.plist b/momentry_runtime/plist/com.momentry.mongodb.plist new file mode 100644 index 0000000..f38f0fb --- /dev/null +++ b/momentry_runtime/plist/com.momentry.mongodb.plist @@ -0,0 +1,39 @@ + + + + + Label + com.momentry.mongodb + + ProgramArguments + + /opt/homebrew/bin/mongod + --dbpath + /Users/accusys/momentry/var + --logpath + /Users/accusys/momentry/log/mongodb.log + --port + 27017 + --bind_ip + 0.0.0.0 + + + RunAtLoad + + + KeepAlive + + + UserName + accusys + + StandardErrorPath + /Users/accusys/momentry/log/mongodb.error.log + + StandardOutPath + /Users/accusys/momentry/log/mongodb.log + + WorkingDirectory + /Users/accusys/momentry/var/mongodb/ + + diff --git a/momentry_runtime/plist/com.momentry.monitor.plist b/momentry_runtime/plist/com.momentry.monitor.plist new file mode 100644 index 0000000..24b080e --- /dev/null +++ b/momentry_runtime/plist/com.momentry.monitor.plist @@ -0,0 +1,49 @@ + + + + + Label + com.momentry.monitor + + UserName + accusys + + WorkingDirectory + /Users/accusys/momentry_core_0.1/monitor + + ProgramArguments + + /bin/bash + /Users/accusys/momentry_core_0.1/monitor/control/monitor_control.sh + check + all + + + EnvironmentVariables + + PATH + /opt/homebrew/opt/node@22/bin:/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin + + + RunAtLoad + + + StartInterval + 300 + + KeepAlive + + SuccessfulExit + + + + StandardOutPath + /Users/accusys/momentry/log/monitor/monitor.log + + StandardErrorPath + /Users/accusys/momentry/log/monitor/monitor.error.log + + ProcessType + Background + + diff --git a/momentry_runtime/plist/com.momentry.n8n.main.plist b/momentry_runtime/plist/com.momentry.n8n.main.plist new file mode 100644 index 0000000..4b69755 --- /dev/null +++ b/momentry_runtime/plist/com.momentry.n8n.main.plist @@ -0,0 +1,102 @@ + + + + + Label + com.momentry.n8n.main + + UserName + accusys + + WorkingDirectory + /Users/accusys/momentry/var/n8n + + ProgramArguments + + /opt/homebrew/opt/node@22/bin/node + /opt/homebrew/lib/node_modules/n8n/bin/n8n + start + + + EnvironmentVariables + + PATH + /opt/homebrew/opt/node@22/bin:/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin + + N8N_ENCRYPTION_KEY + Test3200Test3200Test3200 + + DOMAIN_NAME + n8n.momentry.ddns.net + + WEBHOOK_URL + https://n8n.momentry.ddns.net/ + + N8N_PORT + 5678 + + N8N_LISTEN_ADDRESS + 0.0.0.0 + + N8N_USER_MANAGEMENT_JWT_DURATION + 1h + + GENERIC_TIMEZONE + Asia/Taipei + + TZ + Asia/Taipei + + N8N_LOG_LEVEL + info + + DB_TYPE + postgresdb + + DB_POSTGRESDB_HOST + 127.0.0.1 + + DB_POSTGRESDB_PORT + 5432 + + DB_POSTGRESDB_DATABASE + n8n + + DB_POSTGRESDB_USER + n8n + + DB_POSTGRESDB_PASSWORD + accusys + + EXECUTIONS_MODE + queue + + QUEUE_BULL_REDIS_HOST + 127.0.0.1 + + QUEUE_BULL_REDIS_PORT + 6379 + + QUEUE_BULL_REDIS_PASSWORD + accusys + + N8N_USER_FOLDER + /Users/accusys/momentry/var/n8n + + N8N_METRICS + true + + + RunAtLoad + + + KeepAlive + + + StandardOutPath + /Users/accusys/momentry/log/n8n-main.log + + StandardErrorPath + /Users/accusys/momentry/log/n8n-main.error.log + + diff --git a/momentry_runtime/plist/com.momentry.n8n.worker.plist b/momentry_runtime/plist/com.momentry.n8n.worker.plist new file mode 100644 index 0000000..6aa0313 --- /dev/null +++ b/momentry_runtime/plist/com.momentry.n8n.worker.plist @@ -0,0 +1,105 @@ + + + + + Label + com.momentry.n8n.worker + + UserName + accusys + + GroupName + staff + + WorkingDirectory + /Users/accusys/momentry/var/n8n + + ProgramArguments + + /opt/homebrew/opt/node@22/bin/node + /opt/homebrew/lib/node_modules/n8n/bin/n8n + worker + + + EnvironmentVariables + + PATH + /opt/homebrew/opt/node@22/bin:/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin + + N8N_ENCRYPTION_KEY + Test3200Test3200Test3200 + + DOMAIN_NAME + n8n.momentry.ddns.net + + WEBHOOK_URL + https://n8n.momentry.ddns.net/ + + N8N_LISTEN_ADDRESS + 0.0.0.0 + + GENERIC_TIMEZONE + Asia/Taipei + + TZ + Asia/Taipei + + N8N_LOG_LEVEL + info + + DB_TYPE + postgresdb + + DB_POSTGRESDB_HOST + 127.0.0.1 + + DB_POSTGRESDB_PORT + 5432 + + DB_POSTGRESDB_DATABASE + n8n + + DB_POSTGRESDB_USER + n8n + + DB_POSTGRESDB_PASSWORD + accusys + + EXECUTIONS_MODE + queue + + QUEUE_BULL_REDIS_HOST + 127.0.0.1 + + QUEUE_BULL_REDIS_PORT + 6379 + + QUEUE_BULL_REDIS_PASSWORD + accusys + + N8N_USER_FOLDER + /Users/accusys/momentry/var/n8n + + N8N_RUNNERS_BROKER_PORT + 5690 + + N8N_RUNNERS_LAUNCHER_HEALTH_CHECK_PORT + 5691 + + N8N_METRICS + true + + + RunAtLoad + + + KeepAlive + + + StandardOutPath + /Users/accusys/momentry/log/n8n-worker.log + + StandardErrorPath + /Users/accusys/momentry/log/n8n-worker.error.log + + diff --git a/momentry_runtime/plist/com.momentry.ollama.plist b/momentry_runtime/plist/com.momentry.ollama.plist new file mode 100644 index 0000000..2c6fa6a --- /dev/null +++ b/momentry_runtime/plist/com.momentry.ollama.plist @@ -0,0 +1,44 @@ + + + + + Label + com.momentry.ollama + + UserName + accusys + + EnvironmentVariables + + OLLAMA_HOST + 0.0.0.0:11434 + OLLAMA_MODELS + /Users/accusys/momentry/var/ollama/models + OLLAMA_FLASH_ATTENTION + 1 + OLLAMA_KV_CACHE_TYPE + q8_0 + + + WorkingDirectory + /Users/accusys/momentry/var/ollama + + ProgramArguments + + /opt/homebrew/opt/ollama/bin/ollama + serve + + + RunAtLoad + + + KeepAlive + + + StandardErrorPath + /Users/accusys/momentry/log/ollama.error.log + + StandardOutPath + /Users/accusys/momentry/log/ollama.log + + diff --git a/momentry_runtime/plist/com.momentry.php.plist b/momentry_runtime/plist/com.momentry.php.plist new file mode 100644 index 0000000..97a993f --- /dev/null +++ b/momentry_runtime/plist/com.momentry.php.plist @@ -0,0 +1,34 @@ + + + + + Label + com.momentry.php + + UserName + accusys + + WorkingDirectory + /Users/accusys/momentry/var + + ProgramArguments + + /opt/homebrew/opt/php/sbin/php-fpm + --nodaemonize + --fpm-config + /Users/accusys/momentry/etc/php/8.5/php-fpm.conf + + + RunAtLoad + + + KeepAlive + + + StandardErrorPath + /Users/accusys/momentry/log/php.error.log + + StandardOutPath + /Users/accusys/momentry/log/php.log + + diff --git a/momentry_runtime/plist/com.momentry.postgresql.plist b/momentry_runtime/plist/com.momentry.postgresql.plist new file mode 100644 index 0000000..d799ab2 --- /dev/null +++ b/momentry_runtime/plist/com.momentry.postgresql.plist @@ -0,0 +1,39 @@ + + + + + Label + com.momentry.postgresql + + UserName + accusys + + EnvironmentVariables + + LC_ALL + en_US.UTF-8 + + + WorkingDirectory + /Users/accusys/momentry/var/postgresql + + ProgramArguments + + /opt/homebrew/opt/postgresql@18/bin/postgres + -D + /Users/accusys/momentry/var/postgresql + + + RunAtLoad + + + KeepAlive + + + StandardErrorPath + /Users/accusys/momentry/log/postgresql.error.log + + StandardOutPath + /Users/accusys/momentry/log/postgresql.log + + diff --git a/momentry_runtime/plist/com.momentry.qdrant.plist b/momentry_runtime/plist/com.momentry.qdrant.plist new file mode 100644 index 0000000..51fdafa --- /dev/null +++ b/momentry_runtime/plist/com.momentry.qdrant.plist @@ -0,0 +1,46 @@ + + + + + Label + com.momentry.qdrant + + UserName + accusys + + WorkingDirectory + /Users/accusys/momentry/var/qdrant/ + + ProgramArguments + + /Users/accusys/.cargo/bin/qdrant + + + EnvironmentVariables + + QDRANT__STORAGE__STORAGE_PATH + /Users/accusys/momentry/var/qdrant/ + + QDRANT__SERVICE__HOST + 0.0.0.0 + + QDRANT__SERVICE__HTTP_PORT + 6333 + + HOME + /Users/accusys + + QDRANT__SERVICE__API_KEY + Test3200Test3200Test3200 + + + RunAtLoad + + KeepAlive + + StandardOutPath + /Users/accusys/momentry/log/qdrant.log + StandardErrorPath + /opt/homebrew/var/log/qdrant.error.log + + diff --git a/momentry_runtime/plist/com.momentry.redis.plist b/momentry_runtime/plist/com.momentry.redis.plist new file mode 100644 index 0000000..5cc8b14 --- /dev/null +++ b/momentry_runtime/plist/com.momentry.redis.plist @@ -0,0 +1,41 @@ + + + + + Label + com.momentry.redis + + UserName + accusys + + WorkingDirectory + /Users/accusys/momentry/var/redis + + ProgramArguments + + /opt/homebrew/opt/redis/bin/redis-server + --port + 6379 + --bind + 0.0.0.0 + --requirepass + accusys + --dir + /Users/accusys/momentry/var/redis + --logfile + /Users/accusys/momentry/log/redis.log + + + RunAtLoad + + + KeepAlive + + + StandardErrorPath + /Users/accusys/momentry/log/redis.error.log + + StandardOutPath + /Users/accusys/momentry/log/redis.log + + diff --git a/momentry_runtime/plist/com.momentry.rustdesk.hbbr.plist b/momentry_runtime/plist/com.momentry.rustdesk.hbbr.plist new file mode 100644 index 0000000..9f4f06b --- /dev/null +++ b/momentry_runtime/plist/com.momentry.rustdesk.hbbr.plist @@ -0,0 +1,33 @@ + + + + + Label + com.momentry.rustdesk.hbbr + + UserName + accusys + + WorkingDirectory + /Users/accusys/momentry/var/rustdesk + + ProgramArguments + + /opt/homebrew/bin/hbbr + -k + _ + + + RunAtLoad + + + KeepAlive + + + StandardOutPath + /Users/accusys/momentry/log/rustdesk-hbbr.log + + StandardErrorPath + /Users/accusys/momentry/log/rustdesk-hbbr.error.log + + diff --git a/momentry_runtime/plist/com.momentry.rustdesk.hbbs.plist b/momentry_runtime/plist/com.momentry.rustdesk.hbbs.plist new file mode 100644 index 0000000..5e58f96 --- /dev/null +++ b/momentry_runtime/plist/com.momentry.rustdesk.hbbs.plist @@ -0,0 +1,33 @@ + + + + + Label + com.momentry.rustdesk.hbbs + + UserName + accusys + + WorkingDirectory + /Users/accusys/momentry/var/rustdesk + + ProgramArguments + + /opt/homebrew/bin/hbbs + -k + _ + + + RunAtLoad + + + KeepAlive + + + StandardOutPath + /Users/accusys/momentry/log/rustdesk-hbbs.log + + StandardErrorPath + /Users/accusys/momentry/log/rustdesk-hbbs.error.log + + diff --git a/momentry_runtime/plist/com.momentry.sftpgo.plist b/momentry_runtime/plist/com.momentry.sftpgo.plist new file mode 100644 index 0000000..78659c1 --- /dev/null +++ b/momentry_runtime/plist/com.momentry.sftpgo.plist @@ -0,0 +1,42 @@ + + + + + Label + com.momentry.sftpgo + + UserName + accusys + + WorkingDirectory + /Users/accusys/workspace/sftpgo + + ProgramArguments + + /opt/homebrew/opt/sftpgo/bin/sftpgo + serve + --config-file + /Users/accusys/momentry/etc/sftpgo/sftpgo.json + + + EnvironmentVariables + + PATH + /opt/homebrew/bin:/opt/homebrew/sbin:/usr/bin:/bin + HOME + /Users/accusys + + + RunAtLoad + + + KeepAlive + + + StandardOutPath + /Users/accusys/momentry/log/sftpgo.log + + StandardErrorPath + /Users/accusys/momentry/log/sftpgo.error.log + + diff --git a/momentry_runtime/plist/template.service.plist b/momentry_runtime/plist/template.service.plist new file mode 100644 index 0000000..b73f581 --- /dev/null +++ b/momentry_runtime/plist/template.service.plist @@ -0,0 +1,39 @@ + + + + + Label + com.momentry.SERVICE_NAME + + UserName + accusys + + WorkingDirectory + /Users/accusys/momentry/var/SERVICE_NAME + + ProgramArguments + + /opt/homebrew/bin/executable + --arg1 + value1 + + + EnvironmentVariables + + PATH + /opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin + + + RunAtLoad + + + KeepAlive + + + StandardOutPath + /Users/accusys/momentry/log/SERVICE_NAME.log + + StandardErrorPath + /Users/accusys/momentry/log/SERVICE_NAME.error.log + + diff --git a/momentry_runtime/start.sh b/momentry_runtime/start.sh new file mode 100755 index 0000000..fe02b57 --- /dev/null +++ b/momentry_runtime/start.sh @@ -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)" diff --git a/momentry_runtime/stop.sh b/momentry_runtime/stop.sh new file mode 100755 index 0000000..9f52f7a --- /dev/null +++ b/momentry_runtime/stop.sh @@ -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 diff --git a/monitor/MONITORING.md b/monitor/MONITORING.md new file mode 100644 index 0000000..80294ec --- /dev/null +++ b/monitor/MONITORING.md @@ -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) diff --git a/monitor/SKILL_TROUBLESHOOTING.md b/monitor/SKILL_TROUBLESHOOTING.md new file mode 100644 index 0000000..3d7ed32 --- /dev/null +++ b/monitor/SKILL_TROUBLESHOOTING.md @@ -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 : + +# 常用端口對照 +# 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 # 下載模型 +ollama run # 運行模型 +``` + +--- + +### 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..plist +``` + +### 停止服務 + +```bash +sudo launchctl unload /Library/LaunchDaemons/com.momentry..plist +``` + +### 重啟服務 + +```bash +sudo launchctl unload /Library/LaunchDaemons/com.momentry..plist +sudo launchctl load /Library/LaunchDaemons/com.momentry..plist +``` + +### 查看服務日誌 + +```bash +tail -f /Users/accusys/momentry/log/.log +tail -f /Users/accusys/momentry/log/.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 + +# 2. 檢查端口 +lsof -i : + +# 3. 檢查日誌 +tail -100 /Users/accusys/momentry/log/.error.log + +# 4. 重新載入 +sudo launchctl unload /Library/LaunchDaemons/com.momentry..plist +sudo launchctl load /Library/LaunchDaemons/com.momentry..plist +``` + +### 2. 認證失敗 + +```bash +# 檢查用戶是否存在 +psql -U -h localhost -d -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/` diff --git a/monitor/config/monitor_config.yaml b/monitor/config/monitor_config.yaml new file mode 100644 index 0000000..f422175 --- /dev/null +++ b/monitor/config/monitor_config.yaml @@ -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 diff --git a/monitor/control/monitor_control.sh b/monitor/control/monitor_control.sh new file mode 100755 index 0000000..f25dfc5 --- /dev/null +++ b/monitor/control/monitor_control.sh @@ -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 [options] + +命令: + status 查看監控狀態 + check 執行特定層級檢查 + layers: service, workflow, portal, database, users, storage, external, backup, node, python, all + monitor 持續監控 (每 5 分鐘) + init 初始化監控數據庫表 + logs [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 ' 執行檢查" + 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 "$@" diff --git a/monitor/database/mongodb_monitor.sh b/monitor/database/mongodb_monitor.sh new file mode 100755 index 0000000..fc5d28c --- /dev/null +++ b/monitor/database/mongodb_monitor.sh @@ -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 "========================================" diff --git a/monitor/database/postgres_monitor.sh b/monitor/database/postgres_monitor.sh new file mode 100755 index 0000000..3c449a7 --- /dev/null +++ b/monitor/database/postgres_monitor.sh @@ -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" diff --git a/monitor/database/qdrant_monitor.sh b/monitor/database/qdrant_monitor.sh new file mode 100755 index 0000000..a4fc557 --- /dev/null +++ b/monitor/database/qdrant_monitor.sh @@ -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" diff --git a/monitor/database/redis_monitor.sh b/monitor/database/redis_monitor.sh new file mode 100755 index 0000000..393d30f --- /dev/null +++ b/monitor/database/redis_monitor.sh @@ -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" diff --git a/monitor/database/schema.sql b/monitor/database/schema.sql new file mode 100644 index 0000000..6dff313 --- /dev/null +++ b/monitor/database/schema.sql @@ -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); diff --git a/monitor/portal/page_monitor.sh b/monitor/portal/page_monitor.sh new file mode 100755 index 0000000..38f592b --- /dev/null +++ b/monitor/portal/page_monitor.sh @@ -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" diff --git a/monitor/service/external_monitor.sh b/monitor/service/external_monitor.sh new file mode 100755 index 0000000..25a406e --- /dev/null +++ b/monitor/service/external_monitor.sh @@ -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" diff --git a/monitor/service/health_check.sh b/monitor/service/health_check.sh new file mode 100755 index 0000000..f36279c --- /dev/null +++ b/monitor/service/health_check.sh @@ -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" diff --git a/monitor/service/node_monitor.sh b/monitor/service/node_monitor.sh new file mode 100755 index 0000000..66a6331 --- /dev/null +++ b/monitor/service/node_monitor.sh @@ -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 diff --git a/monitor/service/python_monitor.sh b/monitor/service/python_monitor.sh new file mode 100755 index 0000000..1a6e3de --- /dev/null +++ b/monitor/service/python_monitor.sh @@ -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 diff --git a/monitor/storage/backup_monitor.sh b/monitor/storage/backup_monitor.sh new file mode 100755 index 0000000..32e51ec --- /dev/null +++ b/monitor/storage/backup_monitor.sh @@ -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 diff --git a/monitor/storage/storage_manager.sh b/monitor/storage/storage_manager.sh new file mode 100755 index 0000000..708f3e5 --- /dev/null +++ b/monitor/storage/storage_manager.sh @@ -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 diff --git a/monitor/users/session_tracker.sh b/monitor/users/session_tracker.sh new file mode 100755 index 0000000..4384ea8 --- /dev/null +++ b/monitor/users/session_tracker.sh @@ -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" diff --git a/monitor/workflow/n8n_workflow_monitor.sh b/monitor/workflow/n8n_workflow_monitor.sh new file mode 100755 index 0000000..331ee68 --- /dev/null +++ b/monitor/workflow/n8n_workflow_monitor.sh @@ -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 </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 diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..144536a --- /dev/null +++ b/requirements.txt @@ -0,0 +1 @@ +faster-whisper>=1.0.0 diff --git a/rustfmt.toml b/rustfmt.toml new file mode 100644 index 0000000..0d4a297 --- /dev/null +++ b/rustfmt.toml @@ -0,0 +1,3 @@ +edition = "2021" +max_width = 100 +tab_spaces = 4 diff --git a/scripts/asr_processor.py b/scripts/asr_processor.py new file mode 100644 index 0000000..8a5d0ee --- /dev/null +++ b/scripts/asr_processor.py @@ -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 ") + sys.exit(1) + + run_asr(sys.argv[1], sys.argv[2]) diff --git a/scripts/install_mongodb.sh b/scripts/install_mongodb.sh new file mode 100755 index 0000000..6f67f74 --- /dev/null +++ b/scripts/install_mongodb.sh @@ -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)" diff --git a/scripts/thumbnail_extractor.py b/scripts/thumbnail_extractor.py new file mode 100644 index 0000000..d0798bb --- /dev/null +++ b/scripts/thumbnail_extractor.py @@ -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() diff --git a/scripts/troubleshoot.sh b/scripts/troubleshoot.sh new file mode 100755 index 0000000..34084d9 --- /dev/null +++ b/scripts/troubleshoot.sh @@ -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 diff --git a/src/api/mod.rs b/src/api/mod.rs new file mode 100644 index 0000000..0dff490 --- /dev/null +++ b/src/api/mod.rs @@ -0,0 +1,3 @@ +pub mod server; + +pub use server::start_server; diff --git a/src/api/server.rs b/src/api/server.rs new file mode 100644 index 0000000..8d07abf --- /dev/null +++ b/src/api/server.rs @@ -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(()) +} diff --git a/src/core/chunk/mod.rs b/src/core/chunk/mod.rs new file mode 100644 index 0000000..8b275bd --- /dev/null +++ b/src/core/chunk/mod.rs @@ -0,0 +1,5 @@ +pub mod splitter; +pub mod types; + +pub use splitter::{AsrSegment, ChunkSplitter}; +pub use types::{Chunk, ChunkType}; diff --git a/src/core/chunk/splitter.rs b/src/core/chunk/splitter.rs new file mode 100644 index 0000000..827fe8d --- /dev/null +++ b/src/core/chunk/splitter.rs @@ -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 { + 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 { + 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, +} diff --git a/src/core/chunk/types.rs b/src/core/chunk/types.rs new file mode 100644 index 0000000..20bcd59 --- /dev/null +++ b/src/core/chunk/types.rs @@ -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, + } + } +} diff --git a/src/core/db/mod.rs b/src/core/db/mod.rs new file mode 100644 index 0000000..5a6b6d9 --- /dev/null +++ b/src/core/db/mod.rs @@ -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 + 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>; + async fn get_all_chunks(&self) -> Result>; +} + +#[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>; +} + +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; diff --git a/src/core/db/mongodb_db.rs b/src/core/db/mongodb_db.rs new file mode 100644 index 0000000..7fe29ef --- /dev/null +++ b/src/core/db/mongodb_db.rs @@ -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>, +} + +#[derive(Debug, Default)] +pub struct MongoCache { + documents: std::collections::HashMap, +} + +#[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, + pub asrx: Option, + pub ocr: Option, + pub yolo: Option, + pub face: Option, + pub pose: Option, + 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> { + // TODO: Implement MongoDB client + Ok(None) + } +} + +#[async_trait] +impl Database for MongoDb { + async fn init() -> Result { + // TODO: Initialize MongoDB client + Ok(Self { + cache: Arc::new(RwLock::new(MongoCache::default())), + }) + } +} diff --git a/src/core/db/postgres_db.rs b/src/core/db/postgres_db.rs new file mode 100644 index 0000000..68cc64d --- /dev/null +++ b/src/core/db/postgres_db.rs @@ -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, + pub created_at: String, +} + +pub struct PostgresDb { + pool: PgPool, + cache: Arc>, +} + +#[derive(Debug, Default)] +pub struct PostgresCache { + videos: std::collections::HashMap, + chunks: std::collections::HashMap>, +} + +impl PostgresDb { + pub async fn new(database_url: &str) -> Result { + 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 { + 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> { + // 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)>( + "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> { + let rows = sqlx::query_as::<_, (i32, String, String, String, f64, i32, i32, f64, Option)>( + "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 = 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> { + 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 = 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 { + let database_url = std::env::var("DATABASE_URL") + .unwrap_or_else(|_| "postgres://accusys@localhost:5432/momentry".to_string()); + Self::new(&database_url).await + } +} diff --git a/src/core/db/qdrant_db.rs b/src/core/db/qdrant_db.rs new file mode 100644 index 0000000..b7d5a47 --- /dev/null +++ b/src/core/db/qdrant_db.rs @@ -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>, +} + +#[derive(Debug, Default)] +pub struct QdrantCache { + vectors: std::collections::HashMap>, +} + +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 { + 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> { + // Simple cosine similarity search (placeholder) + let cache = self.cache.read().await; + let mut results: Vec = 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::().sqrt(); + let norm_b: f32 = b.iter().map(|x| x * x).sum::().sqrt(); + + if norm_a == 0.0 || norm_b == 0.0 { + return 0.0; + } + + dot_product / (norm_a * norm_b) +} diff --git a/src/core/db/redis_db.rs b/src/core/db/redis_db.rs new file mode 100644 index 0000000..09ac03a --- /dev/null +++ b/src/core/db/redis_db.rs @@ -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>, +} + +#[derive(Debug, Clone, Default)] +pub struct RedisState { + pub processing: Vec, + pub completed: Vec, + pub failed: Vec, +} + +#[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> { + // 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 { + // TODO: Initialize Redis client + Ok(Self { + state: Arc::new(RwLock::new(RedisState::default())), + }) + } +} diff --git a/src/core/embedding/comic_embed.rs b/src/core/embedding/comic_embed.rs new file mode 100644 index 0000000..dc8de27 --- /dev/null +++ b/src/core/embedding/comic_embed.rs @@ -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> { + // 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::().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> { + 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() + } +} diff --git a/src/core/embedding/mod.rs b/src/core/embedding/mod.rs new file mode 100644 index 0000000..53a990a --- /dev/null +++ b/src/core/embedding/mod.rs @@ -0,0 +1,3 @@ +pub mod comic_embed; + +pub use comic_embed::Embedder; diff --git a/src/core/mod.rs b/src/core/mod.rs new file mode 100644 index 0000000..0bd8e4a --- /dev/null +++ b/src/core/mod.rs @@ -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; diff --git a/src/core/overlay/mod.rs b/src/core/overlay/mod.rs new file mode 100644 index 0000000..2f436f4 --- /dev/null +++ b/src/core/overlay/mod.rs @@ -0,0 +1,3 @@ +pub mod types; + +pub use types::OverlayFlags; diff --git a/src/core/overlay/types.rs b/src/core/overlay/types.rs new file mode 100644 index 0000000..d2f351a --- /dev/null +++ b/src/core/overlay/types.rs @@ -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, + _ => {} + } + } +} diff --git a/src/core/probe/mod.rs b/src/core/probe/mod.rs new file mode 100644 index 0000000..cc0eb14 --- /dev/null +++ b/src/core/probe/mod.rs @@ -0,0 +1,3 @@ +pub mod probe; + +pub use probe::{probe_video, ProbeResult}; diff --git a/src/core/probe/probe.rs b/src/core/probe/probe.rs new file mode 100644 index 0000000..8adf933 --- /dev/null +++ b/src/core/probe/probe.rs @@ -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, + pub format: FormatInfo, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct StreamInfo { + pub index: u32, + pub codec_name: Option, + pub codec_type: Option, + pub width: Option, + pub height: Option, + pub r_frame_rate: Option, + pub duration: Option, + pub sample_rate: Option, + pub channels: Option, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct FormatInfo { + pub filename: Option, + pub format_name: Option, + pub duration: Option, + pub size: Option, + pub bit_rate: Option, +} + +pub fn probe_video(video_path: &str) -> Result { + 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 = 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 }) +} diff --git a/src/core/processor/asr.rs b/src/core/processor/asr.rs new file mode 100644 index 0000000..31e0e2e --- /dev/null +++ b/src/core/processor/asr.rs @@ -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, + pub language_probability: Option, + pub segments: Vec, +} + +#[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 { + 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) +} diff --git a/src/core/processor/asrx.rs b/src/core/processor/asrx.rs new file mode 100644 index 0000000..a99f07f --- /dev/null +++ b/src/core/processor/asrx.rs @@ -0,0 +1,28 @@ +use anyhow::Result; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Serialize, Deserialize)] +pub struct AsrxResult { + pub segments: Vec, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct AsrxSegment { + pub start: f64, + pub end: f64, + pub text: String, + pub speaker_id: String, + pub speaker_embedding: Option>, +} + +pub async fn process_asrx(video_path: &str, output_path: &str) -> Result { + // 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![] }) +} diff --git a/src/core/processor/face.rs b/src/core/processor/face.rs new file mode 100644 index 0000000..289bd63 --- /dev/null +++ b/src/core/processor/face.rs @@ -0,0 +1,36 @@ +use anyhow::Result; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Serialize, Deserialize)] +pub struct FaceResult { + pub frames: Vec, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct FaceFrame { + pub frame: u64, + pub timestamp: f64, + pub faces: Vec, +} + +#[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>, +} + +pub async fn process_face(video_path: &str, output_path: &str) -> Result { + // 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![] }) +} diff --git a/src/core/processor/mod.rs b/src/core/processor/mod.rs new file mode 100644 index 0000000..5c499a8 --- /dev/null +++ b/src/core/processor/mod.rs @@ -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; diff --git a/src/core/processor/ocr.rs b/src/core/processor/ocr.rs new file mode 100644 index 0000000..058075d --- /dev/null +++ b/src/core/processor/ocr.rs @@ -0,0 +1,36 @@ +use anyhow::Result; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Serialize, Deserialize)] +pub struct OcrResult { + pub frames: Vec, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct OcrFrame { + pub frame: u64, + pub timestamp: f64, + pub texts: Vec, +} + +#[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 { + // 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![] }) +} diff --git a/src/core/processor/pose.rs b/src/core/processor/pose.rs new file mode 100644 index 0000000..48b8d46 --- /dev/null +++ b/src/core/processor/pose.rs @@ -0,0 +1,47 @@ +use anyhow::Result; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Serialize, Deserialize)] +pub struct PoseResult { + pub frames: Vec, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct PoseFrame { + pub frame: u64, + pub timestamp: f64, + pub persons: Vec, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct PersonPose { + pub keypoints: Vec, + 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 { + // 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![] }) +} diff --git a/src/core/processor/yolo.rs b/src/core/processor/yolo.rs new file mode 100644 index 0000000..40dd2eb --- /dev/null +++ b/src/core/processor/yolo.rs @@ -0,0 +1,36 @@ +use anyhow::Result; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Serialize, Deserialize)] +pub struct YoloResult { + pub frames: Vec, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct YoloFrame { + pub frame: u64, + pub timestamp: f64, + pub objects: Vec, +} + +#[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 { + // 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![] }) +} diff --git a/src/core/storage/file_manager.rs b/src/core/storage/file_manager.rs new file mode 100644 index 0000000..6c68d61 --- /dev/null +++ b/src/core/storage/file_manager.rs @@ -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 { + 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 { + 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> { + 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) + } +} diff --git a/src/core/storage/mod.rs b/src/core/storage/mod.rs new file mode 100644 index 0000000..67ede4f --- /dev/null +++ b/src/core/storage/mod.rs @@ -0,0 +1,5 @@ +pub mod file_manager; +pub mod uuid; + +pub use file_manager::FileManager; +pub use uuid::compute_uuid; diff --git a/src/core/storage/uuid.rs b/src/core/storage/uuid.rs new file mode 100644 index 0000000..649b79e --- /dev/null +++ b/src/core/storage/uuid.rs @@ -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); + } +} diff --git a/src/core/thumbnail/mod.rs b/src/core/thumbnail/mod.rs new file mode 100644 index 0000000..ce94de5 --- /dev/null +++ b/src/core/thumbnail/mod.rs @@ -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, +} + +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 { + 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 { + let thumb_dir = self.output_dir.join(uuid); + + // Check if thumbnails already exist + if thumb_dir.exists() { + let files: Vec = (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> { + let thumb_dir = self.output_dir.join(uuid); + + if !thumb_dir.exists() { + return None; + } + + let files: Vec = (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(()) + } +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..f012333 --- /dev/null +++ b/src/lib.rs @@ -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}; diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..34a8791 --- /dev/null +++ b/src/main.rs @@ -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, + }, + /// 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, + /// 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::().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::(), den.parse::()) { + 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(()) + } + } +} diff --git a/src/player/mod.rs b/src/player/mod.rs new file mode 100644 index 0000000..19ea794 --- /dev/null +++ b/src/player/mod.rs @@ -0,0 +1,3 @@ +pub mod player; + +pub use player::{play_video, PlayerConfig}; diff --git a/src/player/player.rs b/src/player/player.rs new file mode 100644 index 0000000..aa3ba18 --- /dev/null +++ b/src/player/player.rs @@ -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(()) +} diff --git a/src/watcher/mod.rs b/src/watcher/mod.rs new file mode 100644 index 0000000..416a61c --- /dev/null +++ b/src/watcher/mod.rs @@ -0,0 +1,3 @@ +pub mod watcher; + +pub use watcher::{watch_directories, WatcherConfig}; diff --git a/src/watcher/watcher.rs b/src/watcher/watcher.rs new file mode 100644 index 0000000..6160c3b --- /dev/null +++ b/src/watcher/watcher.rs @@ -0,0 +1,41 @@ +use anyhow::Result; +use notify::{Config, Event, EventKind, RecommendedWatcher, RecursiveMode, Watcher}; +use std::path::Path; +use std::sync::Arc; +use tokio::sync::mpsc; + +pub struct WatcherConfig { + pub directories: Vec, + pub poll_interval_ms: u64, +} + +impl Default for WatcherConfig { + fn default() -> Self { + Self { + directories: vec![], + poll_interval_ms: 5000, + } + } +} + +pub async fn watch_directories(config: WatcherConfig, tx: mpsc::Sender) -> Result<()> { + // TODO: Implement directory watcher + // + // Options: + // 1. Use notify crate for file system events + // 2. Use polling as fallback + // + // When new video file is detected: + // - Send job to Redis queue + // - Trigger registration process + + println!("Watching directories: {:?}", config.directories); + + for dir in &config.directories { + if Path::new(dir).exists() { + println!("Directory exists: {}", dir); + } + } + + Ok(()) +}