From 383201cacd4c663f55ec9c6c858cff1577872aef Mon Sep 17 00:00:00 2001 From: accusys Date: Wed, 25 Mar 2026 14:52:51 +0800 Subject: [PATCH] feat: Initial v0.9 release with API Key authentication ## v0.9.20260325_144654 ### Features - API Key Authentication System - Job Worker System - V2 Backup Versioning ### Bug Fixes - get_processor_results_by_job column mapping Co-authored-by: OpenCode --- .markdownlint.json | 21 + .markdownlintrc | 21 + CHANGELOG.md | 143 ++ a1b10138a6bbb0cd.cut.json | 1 + a1b10138a6bbb0cd.face.json | 1 + a1b10138a6bbb0cd.ocr.json | 1 + a1b10138a6bbb0cd.pose.json | 1 + a1b10138a6bbb0cd.story.json | 1 + a1b10138a6bbb0cd.yolo.json | 1 + docs/API_ACCESS.md | 193 +++ docs/API_CURL_EXAMPLES.md | 492 ++++++ docs/API_ENDPOINTS.md | 33 +- docs/API_EXAMPLES.md | 726 +++++++++ docs/API_INDEX.md | 102 ++ docs/API_KEY_ARCHITECTURE.md | 195 +++ docs/API_KEY_INTEGRATION_TESTS.md | 236 +++ docs/API_KEY_MANAGEMENT.md | 699 +++++++++ docs/API_KEY_OPTIMIZATION.md | 399 +++++ docs/API_N8N_GUIDE.md | 193 +++ docs/API_REFERENCE.md | 447 ++++++ docs/API_WORDPRESS_GUIDE.md | 270 ++++ docs/ARCHITECTURE_EVALUATION.md | 331 ++++ docs/BUILD_VERSION_RECORD.md | 667 ++++++++ docs/CACHE_ARCHITECTURE_PLAN.md | 1106 ++++++++++++++ docs/CHUNK_DESIGN.md | 534 +++++++ docs/CHUNK_SPEC.md | 529 ++++++- docs/DEMO_MANUAL.md | 674 +++++++++ docs/DEVELOPMENT_LOG.md | 116 ++ docs/DOCS_STANDARD.md | 474 ++++++ docs/DOCUMENT_EMBEDDING_STRATEGY.md | 151 ++ docs/FILE_CHANGE_MANAGEMENT.md | 323 ++++ docs/FRESH_MAC_INSTALLATION.md | 707 +++++++++ docs/INSTALL_CADDY.md | 16 + docs/INSTALL_GITEA.md | 16 + docs/INSTALL_GITEA_MCP.md | 393 +++++ docs/INSTALL_MARIADB.md | 16 + docs/INSTALL_MOMENTRY_API.md | 464 ++++++ docs/INSTALL_MONGODB.md | 73 +- docs/INSTALL_N8N.md | 43 +- docs/INSTALL_OLLAMA.md | 18 +- docs/INSTALL_PHP.md | 16 + docs/INSTALL_POSTGRESQL.md | 27 +- docs/INSTALL_QDRANT.md | 16 + docs/INSTALL_REDIS.md | 126 +- docs/INSTALL_RUSTDESK.md | 16 + docs/INSTALL_SFTPGO.md | 705 ++++++++- docs/INSTALL_WORDPRESS.md | 332 ++++ docs/JSON_OUTPUT_SPEC.md | 16 + docs/MAC_INSTALLATION_PLAN.md | 782 ++++++++++ docs/MOMENTRY_RAG_PRESENTATION.md | 322 ++++ .../Momentry_Core_API.postman_collection.json | 127 ++ docs/N8N_API_FIX_SUMMARY.md | 106 ++ docs/N8N_DEMO.md | 220 +++ docs/N8N_DEMO_EXECUTION_LOG.md | 341 +++++ docs/N8N_DEMO_WORKFLOW.md | 677 +++++++++ docs/N8N_HTTP_REQUEST_GUIDE.md | 245 +++ docs/N8N_INTEGRATION_GUIDE.md | 562 +++++++ docs/N8N_MCP_SETUP.md | 211 +++ docs/N8N_MCP_TEST_REPORT.md | 178 +++ docs/N8N_SETUP_COMPLETE.md | 152 ++ docs/N8N_VIDEO_SEARCH_SUCCESS.md | 279 ++++ docs/N8N_VIEW_OUTPUT_GUIDE.md | 141 ++ docs/N8N_WORKFLOW_VIDEO_RAG_MCP.md | 169 +++ docs/NODEJS.md | 19 +- docs/OPENCODE_GUIDE.md | 430 ++++++ docs/OPENCODE_MCP_INSTALL.md | 535 +++++++ docs/PENDING_ISSUES.md | 813 ++++++++++ docs/PROCESSING_PIPELINE.md | 271 ++++ docs/PYTHON.md | 74 + docs/RUST_DEVELOPMENT.md | 61 +- docs/SEARCH_PROMPTS.md | 121 ++ docs/SERVICE_ADDITION_GUIDE.md | 57 +- docs/SFTPGO_DEMO_USER.md | 504 ++++++ docs/TEST_AND_BENCHMARK_PLAN.md | 1185 +++++++++++++++ docs/USER_MANAGEMENT_PLAN.md | 425 ++++++ docs/USER_MANUAL.md | 469 ++++++ docs/VERSION_MANAGEMENT.md | 245 +++ docs/VIDEO_PROCESSING_SPEC.md | 1347 +++++++++++++++++ docs/YOLO_RESUME_INTEGRATION.md | 102 ++ docs/n8n_workflow_simple.json | 110 ++ docs/n8n_workflow_simple_test.json | 89 ++ docs/n8n_workflow_video_rag_mcp.json | 94 ++ docs/n8n_workflow_video_search.json | 125 ++ docs/test_all.sh | 100 ++ docs/test_momentry_api.sh | 33 + docs/test_workflow.sh | 104 ++ id_ecdsa.pub | 1 + id_ed25519.pub | 1 + id_rsa.pub | 1 + migrations/001_api_key_management.sql | 94 ++ migrations/003_job_worker.sql | 140 ++ momentry_runtime/plist/com.momentry.api.plist | 58 + .../plist/com.momentry.gitea-mcp-server.plist | 30 + .../plist/com.momentry.mongodb.plist | 7 +- .../plist/com.momentry.n8n.main.plist | 9 + .../plist/com.momentry.n8n.worker.plist | 18 +- .../plist/com.momentry.sftpgo.plist | 4 + .../plist/com.momentry.worker.plist | 58 + monitor/common/load_credentials.sh | 35 + monitor/config/monitor_config.yaml | 8 - monitor/service/health_check.sh | 92 +- monitor/workflow/backup_n8n_api.py | 625 ++++++++ monitor/workflow/backup_n8n_mcp.py | 481 ++++++ monitor/workflow/backup_n8n_workflows.sh | 376 +++++ note.md | 86 ++ opencode.json | 12 + requirements.txt | 34 +- .../redis_publisher.cpython-311.pyc | Bin 0 -> 9145 bytes scripts/add_yolo_to_chunks.py | 137 ++ scripts/asr_processor.py | 45 +- scripts/asrx_processor.py | 110 ++ scripts/caption_processor.py | 305 ++++ scripts/chinese_vector_test.py | 170 +++ scripts/compare_search.py | 131 ++ scripts/comprehensive_search_test.py | 316 ++++ scripts/cut_processor.py | 106 ++ scripts/face_processor.py | 154 ++ scripts/natural_language_top10.py | 169 +++ scripts/natural_language_vector_detailed.py | 272 ++++ scripts/natural_language_vector_test.py | 220 +++ scripts/object_search.py | 165 ++ scripts/ocr_processor.py | 155 ++ scripts/pose_processor.py | 168 ++ scripts/redis_publisher.py | 184 +++ scripts/setup_fresh_mac.sh | 170 +++ scripts/story_processor.py | 345 +++++ scripts/sync_to_mongodb.py | 122 ++ scripts/test_multilingual.py | 191 +++ scripts/test_object_search.py | 84 + scripts/test_v2_detailed.py | 156 ++ scripts/test_v2_model.py | 188 +++ scripts/test_v2_with_text.py | 133 ++ scripts/yolo_processor.py | 483 ++++++ sftpgo.db | Bin 0 -> 356352 bytes src/api/middleware.rs | 120 ++ src/api/mod.rs | 1 + src/api/server.rs | 78 +- src/core/api_key/anomaly.rs | 193 +++ src/core/api_key/audit_logger.rs | 193 +++ src/core/api_key/blacklist.rs | 203 +++ src/core/api_key/cleanup.rs | 172 +++ src/core/api_key/encryption.rs | 211 +++ src/core/api_key/error.rs | 184 +++ src/core/api_key/export.rs | 226 +++ src/core/api_key/gitea.rs | 304 ++++ src/core/api_key/mod.rs | 45 + src/core/api_key/models.rs | 228 +++ src/core/api_key/n8n.rs | 211 +++ src/core/api_key/report.rs | 233 +++ src/core/api_key/rotation.rs | 319 ++++ src/core/api_key/service.rs | 276 ++++ src/core/api_key/strength.rs | 209 +++ src/core/api_key/validator.rs | 310 ++++ src/core/api_key/webhook.rs | 311 ++++ src/core/cache/keys.rs | 85 ++ src/core/cache/mod.rs | 10 + src/core/cache/mongo_cache.rs | 311 ++++ src/core/cache/tests.rs | 120 ++ src/core/chunk/splitter.rs | 11 +- src/core/chunk/types.rs | 95 +- src/core/config.rs | 8 - src/core/db/mod.rs | 15 +- src/core/db/mongodb_db.rs | 282 +++- src/core/db/postgres_db.rs | 23 +- src/core/db/qdrant_db.rs | 371 ++++- src/core/db/redis_db.rs | 1 + src/core/db/sync_db.rs | 155 ++ src/core/embedding/comic_embed.rs | 104 +- src/core/mod.rs | 3 + src/core/probe/{probe.rs => ffprobe.rs} | 0 src/core/processor/asr.rs | 134 +- src/core/processor/asrx.rs | 142 +- src/core/processor/caption.rs | 77 + src/core/processor/cut.rs | 127 ++ src/core/processor/executor.rs | 395 +++++ src/core/processor/face.rs | 135 +- src/core/processor/mod.rs | 18 +- src/core/processor/ocr.rs | 133 +- src/core/processor/pose.rs | 155 +- src/core/processor/story.rs | 250 +++ src/core/processor/yolo.rs | 139 +- src/core/storage/mod.rs | 2 + src/core/storage/output_dir.rs | 226 +++ src/core/storage/uuid.rs | 79 +- src/lib.rs | 15 +- src/player/api_client.rs | 196 +++ src/player/asr_overlay.rs | 181 +++ src/player/chunk_selector.rs | 333 ++++ src/player/main.rs | 990 ++++++++++++ src/player/mod.rs | 10 + src/player/selector.rs | 163 ++ src/ui/mod.rs | 1 + src/ui/progress/mod.rs | 413 +++++ 193 files changed, 40268 insertions(+), 422 deletions(-) create mode 100644 .markdownlint.json create mode 100644 .markdownlintrc create mode 100644 CHANGELOG.md create mode 120000 a1b10138a6bbb0cd.cut.json create mode 120000 a1b10138a6bbb0cd.face.json create mode 120000 a1b10138a6bbb0cd.ocr.json create mode 120000 a1b10138a6bbb0cd.pose.json create mode 120000 a1b10138a6bbb0cd.story.json create mode 120000 a1b10138a6bbb0cd.yolo.json create mode 100644 docs/API_ACCESS.md create mode 100644 docs/API_CURL_EXAMPLES.md create mode 100644 docs/API_EXAMPLES.md create mode 100644 docs/API_INDEX.md create mode 100644 docs/API_KEY_ARCHITECTURE.md create mode 100644 docs/API_KEY_INTEGRATION_TESTS.md create mode 100644 docs/API_KEY_MANAGEMENT.md create mode 100644 docs/API_KEY_OPTIMIZATION.md create mode 100644 docs/API_N8N_GUIDE.md create mode 100644 docs/API_REFERENCE.md create mode 100644 docs/API_WORDPRESS_GUIDE.md create mode 100644 docs/ARCHITECTURE_EVALUATION.md create mode 100644 docs/BUILD_VERSION_RECORD.md create mode 100644 docs/CACHE_ARCHITECTURE_PLAN.md create mode 100644 docs/CHUNK_DESIGN.md create mode 100644 docs/DEMO_MANUAL.md create mode 100644 docs/DOCS_STANDARD.md create mode 100644 docs/DOCUMENT_EMBEDDING_STRATEGY.md create mode 100644 docs/FILE_CHANGE_MANAGEMENT.md create mode 100644 docs/FRESH_MAC_INSTALLATION.md create mode 100644 docs/INSTALL_GITEA_MCP.md create mode 100644 docs/INSTALL_MOMENTRY_API.md create mode 100644 docs/INSTALL_WORDPRESS.md create mode 100644 docs/MAC_INSTALLATION_PLAN.md create mode 100644 docs/MOMENTRY_RAG_PRESENTATION.md create mode 100644 docs/Momentry_Core_API.postman_collection.json create mode 100644 docs/N8N_API_FIX_SUMMARY.md create mode 100644 docs/N8N_DEMO.md create mode 100644 docs/N8N_DEMO_EXECUTION_LOG.md create mode 100644 docs/N8N_DEMO_WORKFLOW.md create mode 100644 docs/N8N_HTTP_REQUEST_GUIDE.md create mode 100644 docs/N8N_INTEGRATION_GUIDE.md create mode 100644 docs/N8N_MCP_SETUP.md create mode 100644 docs/N8N_MCP_TEST_REPORT.md create mode 100644 docs/N8N_SETUP_COMPLETE.md create mode 100644 docs/N8N_VIDEO_SEARCH_SUCCESS.md create mode 100644 docs/N8N_VIEW_OUTPUT_GUIDE.md create mode 100644 docs/N8N_WORKFLOW_VIDEO_RAG_MCP.md create mode 100644 docs/OPENCODE_GUIDE.md create mode 100644 docs/OPENCODE_MCP_INSTALL.md create mode 100644 docs/PENDING_ISSUES.md create mode 100644 docs/PROCESSING_PIPELINE.md create mode 100644 docs/SEARCH_PROMPTS.md create mode 100644 docs/SFTPGO_DEMO_USER.md create mode 100644 docs/TEST_AND_BENCHMARK_PLAN.md create mode 100644 docs/USER_MANAGEMENT_PLAN.md create mode 100644 docs/USER_MANUAL.md create mode 100644 docs/VERSION_MANAGEMENT.md create mode 100644 docs/VIDEO_PROCESSING_SPEC.md create mode 100644 docs/YOLO_RESUME_INTEGRATION.md create mode 100644 docs/n8n_workflow_simple.json create mode 100644 docs/n8n_workflow_simple_test.json create mode 100644 docs/n8n_workflow_video_rag_mcp.json create mode 100644 docs/n8n_workflow_video_search.json create mode 100755 docs/test_all.sh create mode 100755 docs/test_momentry_api.sh create mode 100755 docs/test_workflow.sh create mode 100644 id_ecdsa.pub create mode 100644 id_ed25519.pub create mode 100644 id_rsa.pub create mode 100644 migrations/001_api_key_management.sql create mode 100644 migrations/003_job_worker.sql create mode 100644 momentry_runtime/plist/com.momentry.api.plist create mode 100644 momentry_runtime/plist/com.momentry.gitea-mcp-server.plist create mode 100644 momentry_runtime/plist/com.momentry.worker.plist create mode 100644 monitor/common/load_credentials.sh create mode 100755 monitor/workflow/backup_n8n_api.py create mode 100644 monitor/workflow/backup_n8n_mcp.py create mode 100755 monitor/workflow/backup_n8n_workflows.sh create mode 100644 note.md create mode 100644 opencode.json create mode 100644 scripts/__pycache__/redis_publisher.cpython-311.pyc create mode 100644 scripts/add_yolo_to_chunks.py create mode 100755 scripts/asrx_processor.py create mode 100644 scripts/caption_processor.py create mode 100644 scripts/chinese_vector_test.py create mode 100644 scripts/compare_search.py create mode 100644 scripts/comprehensive_search_test.py create mode 100755 scripts/cut_processor.py create mode 100755 scripts/face_processor.py create mode 100644 scripts/natural_language_top10.py create mode 100644 scripts/natural_language_vector_detailed.py create mode 100644 scripts/natural_language_vector_test.py create mode 100644 scripts/object_search.py create mode 100755 scripts/ocr_processor.py create mode 100755 scripts/pose_processor.py create mode 100644 scripts/redis_publisher.py create mode 100644 scripts/setup_fresh_mac.sh create mode 100644 scripts/story_processor.py create mode 100644 scripts/sync_to_mongodb.py create mode 100644 scripts/test_multilingual.py create mode 100644 scripts/test_object_search.py create mode 100644 scripts/test_v2_detailed.py create mode 100644 scripts/test_v2_model.py create mode 100644 scripts/test_v2_with_text.py create mode 100755 scripts/yolo_processor.py create mode 100644 sftpgo.db create mode 100644 src/api/middleware.rs create mode 100644 src/core/api_key/anomaly.rs create mode 100644 src/core/api_key/audit_logger.rs create mode 100644 src/core/api_key/blacklist.rs create mode 100644 src/core/api_key/cleanup.rs create mode 100644 src/core/api_key/encryption.rs create mode 100644 src/core/api_key/error.rs create mode 100644 src/core/api_key/export.rs create mode 100644 src/core/api_key/gitea.rs create mode 100644 src/core/api_key/mod.rs create mode 100644 src/core/api_key/models.rs create mode 100644 src/core/api_key/n8n.rs create mode 100644 src/core/api_key/report.rs create mode 100644 src/core/api_key/rotation.rs create mode 100644 src/core/api_key/service.rs create mode 100644 src/core/api_key/strength.rs create mode 100644 src/core/api_key/validator.rs create mode 100644 src/core/api_key/webhook.rs create mode 100644 src/core/cache/keys.rs create mode 100644 src/core/cache/mod.rs create mode 100644 src/core/cache/mongo_cache.rs create mode 100644 src/core/cache/tests.rs create mode 100644 src/core/db/sync_db.rs rename src/core/probe/{probe.rs => ffprobe.rs} (100%) create mode 100644 src/core/processor/caption.rs create mode 100644 src/core/processor/cut.rs create mode 100644 src/core/processor/executor.rs create mode 100644 src/core/processor/story.rs create mode 100644 src/core/storage/output_dir.rs create mode 100644 src/player/api_client.rs create mode 100644 src/player/asr_overlay.rs create mode 100644 src/player/chunk_selector.rs create mode 100644 src/player/main.rs create mode 100644 src/player/selector.rs create mode 100644 src/ui/mod.rs create mode 100644 src/ui/progress/mod.rs diff --git a/.markdownlint.json b/.markdownlint.json new file mode 100644 index 0000000..c6f88ce --- /dev/null +++ b/.markdownlint.json @@ -0,0 +1,21 @@ +{ + "default": true, + "MD003": false, + "MD009": false, + "MD010": false, + "MD013": false, + "MD022": false, + "MD024": false, + "MD025": false, + "MD031": false, + "MD032": false, + "MD033": false, + "MD034": false, + "MD036": false, + "MD040": false, + "MD046": false, + "MD055": false, + "MD056": false, + "MD058": false, + "MD060": false +} diff --git a/.markdownlintrc b/.markdownlintrc new file mode 100644 index 0000000..c6f88ce --- /dev/null +++ b/.markdownlintrc @@ -0,0 +1,21 @@ +{ + "default": true, + "MD003": false, + "MD009": false, + "MD010": false, + "MD013": false, + "MD022": false, + "MD024": false, + "MD025": false, + "MD031": false, + "MD032": false, + "MD033": false, + "MD034": false, + "MD036": false, + "MD040": false, + "MD046": false, + "MD055": false, + "MD056": false, + "MD058": false, + "MD060": false +} diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..3e3a8ed --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,143 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). + +## [Unreleased] + +### Added +- Gitea API token integration +- n8n API key integration +- API key caching with Moka +- Rate limiting for API key validation +- Constant-time hash comparison +- OpenAPI documentation with utoipa + +## [0.1.0] - 2026-03-21 + +### Added + +#### API Key Management System +- API key generation with secure random (UUID v4) +- SHA256 key hashing +- 5 key types: System, User, Service, Integration, Emergency +- Key expiration with configurable TTL +- Grace period for key rotation + +#### Anomaly Detection +- High request rate detection (>1000/min) +- High error rate detection (>50%) +- Multiple IP detection (>5/hour) +- Unusual time activity detection +- Redis Pub/Sub for anomaly alerts + +#### Rotation Mechanism +- Automatic rotation scheduling +- Manual rotation requests +- Forced rotation for security incidents +- Grace period management per key type: + - System: 72 hours + - User: 24 hours + - Service: 48 hours + - Integration: 24 hours + - Emergency: 0 hours (immediate) + +#### PostgreSQL Integration +- `api_keys` table for key storage +- `api_key_audit_log` table for audit trail +- `api_key_anomalies` table for anomaly records +- Full CRUD operations for API keys + +#### Redis Integration +- Anomaly alert Pub/Sub (`momentry:anomaly:alerts`) +- Key anomaly state tracking +- Real-time alert notifications + +#### CLI Commands +- `momentry api-key create` - Create new API key +- `momentry api-key list` - List all API keys +- `momentry api-key validate` - Validate an API key +- `momentry api-key revoke` - Revoke an API key +- `momentry api-key rotate` - Request key rotation +- `momentry api-key stats` - Show statistics + +#### Gitea Integration +- Create Gitea Personal Access Tokens +- List user tokens +- Delete tokens +- Local token tracking +- CLI commands: + - `momentry gitea create` + - `momentry gitea list` + - `momentry gitea delete` + - `momentry gitea verify` + +#### n8n Integration +- Create n8n API keys +- List API keys +- Delete API keys +- Local key tracking +- CLI commands: + - `momentry n8n create` + - `momentry n8n list` + - `momentry n8n delete` + - `momentry n8n verify` + +#### Security Features +- Constant-time hash comparison (subtle crate) +- Rate limiting for validation attempts +- IP-based lockout after failed attempts +- Configurable thresholds via environment variables + +#### Performance Optimizations +- Moka-based API key validation cache +- Configurable TTL and capacity +- Reduced database queries for hot keys + +#### Documentation +- API Key Management design document +- Redis user configuration guide +- Gitea token integration guide +- n8n API key integration guide +- Optimization plan with task codes + +### Environment Variables + +#### API Key Configuration +``` +CACHE_TTL_SECONDS=300 # Cache TTL (default: 300) +CACHE_MAX_CAPACITY=10000 # Max cache entries (default: 10000) +RATE_LIMIT_MAX_ATTEMPTS=5 # Max failed attempts (default: 5) +RATE_LIMIT_WINDOW_SECONDS=900 # Lockout duration (default: 900) +``` + +#### Service URLs +``` +GITEA_URL=http://localhost:3000 +N8N_URL=https://n8n.momentry.ddns.net +``` + +### Database Schema + +#### Tables Created +- `api_keys` - API key storage +- `api_key_audit_log` - Audit trail +- `api_key_anomalies` - Anomaly records +- `gitea_tokens` - Gitea token tracking +- `n8n_api_keys` - n8n API key tracking + +### Dependencies Added +- `uuid` - UUID generation +- `subtle` - Constant-time comparison +- `moka` - Async cache +- `utoipa` - OpenAPI documentation +- `utoipa-swagger-ui` - Swagger UI + +--- + +## Version History + +| Version | Date | Description | +|---------|------|-------------| +| 0.1.0 | 2026-03-21 | Initial release with API Key Management | diff --git a/a1b10138a6bbb0cd.cut.json b/a1b10138a6bbb0cd.cut.json new file mode 120000 index 0000000..31c7e3c --- /dev/null +++ b/a1b10138a6bbb0cd.cut.json @@ -0,0 +1 @@ +/Users/accusys/momentry_core_0.1/output/a1b10138a6bbb0cd.cut.json \ No newline at end of file diff --git a/a1b10138a6bbb0cd.face.json b/a1b10138a6bbb0cd.face.json new file mode 120000 index 0000000..e481d5e --- /dev/null +++ b/a1b10138a6bbb0cd.face.json @@ -0,0 +1 @@ +/Users/accusys/momentry_core_0.1/output/a1b10138a6bbb0cd.face.json \ No newline at end of file diff --git a/a1b10138a6bbb0cd.ocr.json b/a1b10138a6bbb0cd.ocr.json new file mode 120000 index 0000000..edfdb5a --- /dev/null +++ b/a1b10138a6bbb0cd.ocr.json @@ -0,0 +1 @@ +/Users/accusys/momentry_core_0.1/output/a1b10138a6bbb0cd.ocr.json \ No newline at end of file diff --git a/a1b10138a6bbb0cd.pose.json b/a1b10138a6bbb0cd.pose.json new file mode 120000 index 0000000..fb33446 --- /dev/null +++ b/a1b10138a6bbb0cd.pose.json @@ -0,0 +1 @@ +/Users/accusys/momentry_core_0.1/output/a1b10138a6bbb0cd.pose.json \ No newline at end of file diff --git a/a1b10138a6bbb0cd.story.json b/a1b10138a6bbb0cd.story.json new file mode 120000 index 0000000..392fd81 --- /dev/null +++ b/a1b10138a6bbb0cd.story.json @@ -0,0 +1 @@ +/Users/accusys/momentry_core_0.1/output/a1b10138a6bbb0cd.story.json \ No newline at end of file diff --git a/a1b10138a6bbb0cd.yolo.json b/a1b10138a6bbb0cd.yolo.json new file mode 120000 index 0000000..2423006 --- /dev/null +++ b/a1b10138a6bbb0cd.yolo.json @@ -0,0 +1 @@ +/Users/accusys/momentry_core_0.1/output/a1b10138a6bbb0cd.yolo.json \ No newline at end of file diff --git a/docs/API_ACCESS.md b/docs/API_ACCESS.md new file mode 100644 index 0000000..79701db --- /dev/null +++ b/docs/API_ACCESS.md @@ -0,0 +1,193 @@ +# Momentry Core API 存取指南 + +## 基本網址 + +| 環境 | URL | 說明 | +|------|-----|------| +| **本地開發** | `http://localhost:3002` | 直接訪問 API,繞過反向代理 | +| **外部訪問** | `https://api.momentry.ddns.net` | 通過 Caddy 反向代理訪問,需網路可達 | + +### 何時使用哪個 URL + +**使用 `localhost:3002`:** +- 開發/測試環境 +- 直接在伺服器上操作 +- 當反向代理有問題時 + +**使用 `api.momentry.ddns.net`:** +- n8n workflow 中呼叫 API +- 外部系統整合 +- 生產環境 + +## 認證 +目前為開放狀態(示範用途無需認證)。正式環境將實作 API Key。 + +--- + +## 影片搜尋 API + +### 語意搜尋 + +**端點:** `POST /api/v1/search` + +**請求:** +```json +{ + "query": "charade", + "limit": 5, + "uuid": "a1b10138a6bbb0cd" +} +``` + +| 欄位 | 類型 | 必填 | 說明 | +|------|------|------|------| +| `query` | 字串 | 是 | 搜尋文字 | +| `limit` | 整數 | 否 | 最大回傳結果數(預設 10) | +| `uuid` | 字串 | 否 | 依影片 UUID 過濾 | + +**回應:** +```json +{ + "results": [ + { + "uuid": "a1b10138a6bbb0cd", + "chunk_id": "sentence_0006", + "chunk_type": "sentence", + "start_time": 48.8, + "end_time": 55.44, + "text": "fun plot twists, Woody Dialog and charming performances...", + "score": 0.526 + } + ], + "query": "charade" +} +``` + +--- + +### n8n 整合搜尋 + +**端點:** `POST /api/v1/n8n/search` + +**請求:** +```json +{ + "query": "charade", + "limit": 5 +} +``` + +**回應:** +```json +{ + "query": "charade", + "count": 5, + "hits": [ + { + "id": "sentence_0006", + "vid": "a1b10138a6bbb0cd", + "start": 48.8, + "end": 55.44, + "title": "Chunk sentence_0006", + "text": "fun plot twists...", + "score": 0.526, + "media_url": "https://wp.momentry.ddns.net/Old_Time_Movie_Show_-_Charade_1963.HD.mov" + } + ] +} +``` + +--- + +## 影片管理 API + +### 列出所有影片 +**端點:** `GET /api/v1/videos` + +### 查詢影片資訊 +**端點:** `GET /api/v1/lookup?uuid={uuid}` 或 `GET /api/v1/lookup?path={path}` + +### 取得處理進度 +**端點:** `GET /api/v1/progress/{uuid}` + +--- + +## 區塊資料結構 + +每個搜尋結果包含影片播放的時間資訊: + +| 欄位 | 說明 | +|------|------| +| `uuid` | 影片識別碼 | +| `chunk_id` | 區塊唯一識別碼 | +| `chunk_type` | 類型:`sentence`、`cut`、`time_based` | +| `start_time` | 開始時間(秒) | +| `end_time` | 結束時間(秒) | +| `text` | 語音轉文字內容 | +| `score` | 相關性分數(0-1) | + +--- + +## 整合範例 + +### JavaScript/fetch +```javascript +const response = await fetch('http://localhost:3002/api/v1/search', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ query: 'charade', limit: 5 }) +}); +const data = await response.json(); +console.log(data.results); +``` + +### PHP/cURL +```php +$ch = curl_init('http://localhost:3002/api/v1/search'); +curl_setopt($ch, CURLOPT_POST, true); +curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode([ + 'query' => 'charade', + 'limit' => 5 +])); +curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']); +$response = curl_exec($ch); +$data = json_decode($response, true); +``` + +--- + +## 影片嵌入網址 + +影片可透過 SFTPGo 分享連結存取: +``` +https://wp.momentry.ddns.net/{檔案名稱} +``` + +**手動建立分享連結:** +1. 開啟 SFTPGo Web UI:`http://localhost:8080` +2. 使用帳號 `demo` / 密碼 `demopassword123` 登入 +3. 導航至 `Files` → 選擇影片檔案 +4. 點擊 `Share` → `Create Link` +5. 複製產生的分享連結 + +使用搜尋結果中的 `start_time` 和 `end_time` 來嵌入影片片段。 + +--- + +## 服務列表 + +| 服務 | 網址 | 用途 | +|------|------|------| +| Momentry API | `http://localhost:3002` | 核心 API | +| SFTPGo | `http://localhost:8080` | 檔案儲存 | +| Qdrant | `http://localhost:6333` | 向量搜尋 | +| PostgreSQL | `localhost:5432` | 資料庫 | + +--- + +## 示範影片 + +- **檔案:** `Old_Time_Movie_Show_-_Charade_1963.HD.mov` +- **UUID:** `a1b10138a6bbb0cd` +- **長度:** 約 6879 秒(約 1.9 小時) +- **區塊數:** 3886 個(句子 + 場景 + 時間) diff --git a/docs/API_CURL_EXAMPLES.md b/docs/API_CURL_EXAMPLES.md new file mode 100644 index 0000000..4219899 --- /dev/null +++ b/docs/API_CURL_EXAMPLES.md @@ -0,0 +1,492 @@ +# Momentry API 使用說明 (curl 範例) + +| 項目 | 內容 | +|------|------| +| 版本 | V1.2 | +| 日期 | 2026-03-23 | +| Base URL | `http://localhost:3002` | + +--- + +> **狀態說明**: +> - ✅ **已實作**: 健康檢查、搜尋、影片管理端點 +> - ⚠️ **規劃中**: API Key 管理功能 +> - 🔧 **CLI**: 部分功能需使用命令列工具 + +--- + +## 目錄 + +1. [已實作端點](#1-已實作端點) +2. [API Key 管理](#2-api-key-管理-規劃中) +3. [影片管理](#3-影片管理) +4. [查詢與搜索](#4-查詢與搜索) +5. [系統狀態](#5-系統狀態) + +--- + +## URL 選擇指南 + +### 兩種 URL 的使用情境 + +| 環境 | URL | 說明 | +|------|-----|------| +| **本地開發** | `http://localhost:3002` | 直接訪問 API,繞過反向代理 | +| **外部訪問** | `https://api.momentry.ddns.net` | 通過 Caddy 反向代理訪問,需網路可達 | + +### 何時使用 localhost:3002 + +- ✅ 開發/測試環境 +- ✅ 直接在伺服器上操作 +- ✅ 當 Caddy/反向代理有問題時 +- ✅ 需要快速除錯時 + +### 何時使用 api.momentry.ddns.net + +- ✅ n8n workflow 中呼叫 API +- ✅ 外部系統整合 +- ✅ 生產環境 +- ✅ 從其他機器訪問 + +### 快速切換範例 + +```bash +# 本地測試 +curl http://localhost:3002/health + +# 外部測試(功能相同) +curl https://api.momentry.ddns.net/health +``` + +### 常見問題 + +**Q: 為什麼有兩個 URL?** +A: `localhost:3002` 是直接訪問,`api.momentry.ddns.net` 通過 Caddy 反向代理。 + +**Q: 兩者功能相同嗎?** +A: 是的,所有端點和功能完全相同。 + +**Q: 502 錯誤時怎麼辦?** +A: 如果 `api.momentry.ddns.net` 返回 502,檢查 Momentry API 服務是否運行: +```bash +launchctl list | grep momentry.api +# 如果未運行 +sudo launchctl load /Library/LaunchDaemons/com.momentry.api.plist +``` + +--- + +## 1. 已實作端點 + +### 健康檢查 + +```bash +curl http://localhost:3002/health +``` + +**回應**: +```json +{"status":"ok","version":"0.1.0","uptime_ms":123456} +``` + +### 詳細健康檢查 + +```bash +curl http://localhost:3002/health/detailed +``` + +--- + +## 2. API Key 管理 *(規劃中)* + +> ⚠️ **此功能尚未實作**。以下為規劃中的 API 說明,僅供參考。 + +### 2.1 建立 API Key + +```bash +curl -X POST http://localhost:3002/api/v1/api-keys \ + -H "Content-Type: application/json" \ + -H "X-API-Key: your-admin-key" \ + -d '{ + "name": "my-service-key", + "key_type": "service", + "permissions": ["read", "write"], + "ttl_days": 90 + }' +``` + +### 2.2 列出所有 API Keys + +```bash +curl -X GET http://localhost:3002/api/v1/api-keys \ + -H "X-API-Key: your-admin-key" +``` + +### 2.3 驗證 API Key + +```bash +curl -X GET http://localhost:3002/api/v1/api-keys/validate \ + -H "X-API-Key: key-to-validate" +``` + +### 2.4 撤銷 API Key + +```bash +curl -X DELETE http://localhost:3002/api/v1/api-keys/msvc_a1b2c3d4_... \ + -H "X-API-Key: your-admin-key" +``` + +### 2.5 請求 Key 輪換 + +```bash +curl -X POST http://localhost:3002/api/v1/api-keys/msvc_a1b2c3d4_.../rotate \ + -H "X-API-Key: your-admin-key" \ + -H "Content-Type: application/json" \ + -d '{"reason": "scheduled_rotation"}' +``` + +### 2.6 取得統計資訊 + +```bash +curl -X GET http://localhost:3002/api/v1/api-keys/stats \ + -H "X-API-Key: your-admin-key" +``` + +--- + +## 3. 影片管理 + +### 3.1 註冊影片 ✅ + +```bash +curl -X POST http://localhost:3002/api/v1/register \ + -H "Content-Type: application/json" \ + -d '{"path": "/path/to/video.mp4"}' +``` + +**回應範例**: + +```json +{ + "id": 1, + "uuid": "a1b2c3d4e5f6g7h8", + "file_path": "/path/to/video.mp4", + "file_name": "video.mp4", + "duration": 120.5, + "width": 1920, + "height": 1080 +} +``` + +### 3.2 列出所有影片 ✅ + +```bash +curl http://localhost:3002/api/v1/videos +``` + +### 3.3 查詢影片 ✅ + +```bash +# 依 UUID 查詢 +curl "http://localhost:3002/api/v1/lookup?uuid=a1b2c3d4e5f6g7h8" + +# 依路徑查詢 +curl "http://localhost:3002/api/v1/lookup?path=/path/to/video.mp4" +``` + +### 3.4 處理影片 🔧 *(CLI - 非 API)* + +影片處理需要使用 CLI 命令: + +```bash +# 處理影片(生成 ASR, CUT, YOLO, OCR, Face, Pose 資料) +cargo run --bin momentry -- process + +# 或處理多個影片 +cargo run --bin momentry -- process +``` + +### 3.5 取得處理進度 ✅ + +```bash +curl http://localhost:3002/api/v1/progress/ +``` + +**回應範例**: + +```json +{ + "uuid": "a1b2c3d4e5f6g7h8", + "overall_progress": 75, + "processors": [ + { + "name": "asr", + "status": "complete", + "current": 100, + "total": 100, + "progress": 100, + "message": "7 segments" + }, + { + "name": "cut", + "status": "complete", + "current": 134, + "total": 134, + "progress": 100, + "message": "134 scenes" + }, + { + "name": "yolo", + "status": "progress", + "current": 5000, + "total": 14315, + "progress": 35, + "message": "frame 5000" + } + ] +} +``` + +--- + +## 4. 查詢與搜索 + +### 4.1 語意搜尋 ✅ + +```bash +curl -X POST http://localhost:3002/api/v1/search \ + -H "Content-Type: application/json" \ + -d '{ + "query": "測試關鍵字", + "limit": 5 + }' +``` + +**回應範例**: + +```json +{ + "results": [ + { + "uuid": "a1b2c3d4e5f6g7h8", + "chunk_id": "sentence_0006", + "chunk_type": "sentence", + "start_time": 48.8, + "end_time": 55.44, + "text": "fun plot twists...", + "score": 0.526 + } + ], + "query": "測試關鍵字" +} +``` + +### 4.2 n8n 格式搜尋 ✅ + +```bash +curl -X POST http://localhost:3002/api/v1/n8n/search \ + -H "Content-Type: application/json" \ + -d '{ + "query": "測試關鍵字", + "limit": 5 + }' +``` + +**回應範例**: + +```json +{ + "query": "測試關鍵字", + "count": 2, + "hits": [ + { + "id": "c_001", + "vid": "a1b2c3d4e5f6g7h8", + "start": 48.8, + "end": 55.44, + "title": "Chunk sentence_0006", + "text": "fun plot twists...", + "score": 0.92, + "media_url": "https://wp.momentry.ddns.net/video.mp4" + } + ] +} +``` + +### 4.3 混合搜尋 ✅ + +```bash +curl -X POST http://localhost:3002/api/v1/search/hybrid \ + -H "Content-Type: application/json" \ + -d '{ + "query": "測試關鍵字", + "limit": 5 + }' +``` + +--- + +## 5. 系統狀態 + +### 5.1 健康檢查 ✅ + +```bash +curl http://localhost:3002/health +``` + +**回應**: +```json +{"status":"ok","version":"0.1.0","uptime_ms":123456} +``` + +### 5.2 詳細健康檢查 ✅ + +```bash +curl http://localhost:3002/health/detailed +``` + +**回應範例**: + +```json +{ + "status":"ok", + "version":"0.1.0", + "uptime_ms":123456, + "services":{ + "postgres":{"status":"ok","latency_ms":42,"error":null}, + "redis":{"status":"ok","latency_ms":0,"error":null}, + "qdrant":{"status":"ok","latency_ms":15,"error":null} + } +} +``` + +--- + +## 6. n8n Webhook 測試 + +### 測試 n8n Workflow + +**重要**: 測試前請先在 n8n UI 中點擊 "Execute workflow" 按鈕 + +```bash +# 測試 Video RAG Workflow (Test Mode) +curl -X POST http://localhost:5678/webhook-test/video-rag-mcp \ + -H "Content-Type: application/json" \ + -d '{"query":"charade","limit":3}' + +# 帶有 UUID 過濾的搜尋 +curl -X POST http://localhost:5678/webhook-test/video-rag-mcp \ + -H "Content-Type: application/json" \ + -d '{"query":"woody","limit":5,"uuid":"a1b10138a6bbb0cd"}' +``` + +### 生產環境 Webhook + +**注意**: 工作流程必須處於 Active 狀態 + +```bash +curl -X POST http://localhost:5678/webhook/video-rag-mcp \ + -H "Content-Type: application/json" \ + -d '{"query":"charade","limit":3}' +``` + +### n8n Webhook 常見問題 + +**Q: webhook-test 返回 404** +A: 需要在 n8n UI 中點擊 "Execute workflow" 按鈕後才能使用 test webhook + +**Q: webhook (生產環境) 返回 404** +A: 需要將工作流程切換為 Active 狀態 (右上角開關) + +--- + +## 附錄 + +### A. 服務 URL 列表 + +| 服務 | URL | +|------|-----| +| Momentry API (本地) | `http://localhost:3002` | +| Momentry API (外部) | `https://api.momentry.ddns.net` | +| n8n Web UI | `https://n8n.momentry.ddns.net` | +| n8n Webhook Test | `http://localhost:5678/webhook-test/{workflow-name}` | +| n8n Webhook Prod | `http://localhost:5678/webhook/{workflow-name}` | + +### B. 所有可用端點 + +| 端點 | 方法 | 狀態 | 說明 | +|------|------|------|------| +| `/health` | GET | ✅ | 健康檢查 | +| `/health/detailed` | GET | ✅ | 詳細健康檢查 | +| `/api/v1/register` | POST | ✅ | 註冊影片 | +| `/api/v1/search` | POST | ✅ | 語意搜尋 | +| `/api/v1/n8n/search` | POST | ✅ | n8n 格式搜尋 | +| `/api/v1/search/hybrid` | POST | ✅ | 混合搜尋 | +| `/api/v1/lookup` | GET | ✅ | 查詢影片 | +| `/api/v1/videos` | GET | ✅ | 列出所有影片 | +| `/api/v1/progress/:uuid` | GET | ✅ | 處理進度 | +| `/api/v1/api-keys` | * | ⚠️ | API Key 管理 (規劃中) | + +### C. 常見錯誤 + +| HTTP 狀態 | 說明 | 解決方式 | +|-----------|------|----------| +| 200 | 成功 | - | +| 400 | 請求格式錯誤 | 檢查 JSON 格式 | +| 404 | 端點不存在或資源未找到 | 確認端點 URL 正確 | +| 500 | 伺服器內部錯誤 | 檢查 API 服務日誌 | +| **502** | **Bad Gateway** | **API 服務未啟動,見下方說明** | + +#### 502 Bad Gateway 錯誤 + +**問題**: 外部 URL `https://api.momentry.ddns.net` 返回 502 + +**原因**: Momentry Core API 服務未啟動 + +**解決方式**: + +```bash +# 1. 檢查服務狀態 +launchctl list | grep momentry.api + +# 2. 如果未啟動,手動啟動 +sudo launchctl load /Library/LaunchDaemons/com.momentry.api.plist + +# 3. 或使用本地測試(繞過反向代理) +curl http://localhost:3002/health + +# 4. 檢查日誌 +tail -50 /Users/accusys/momentry/log/momentry_api.error.log +``` + +### D. 範例腳本 + +```bash +#!/bin/bash +# api_test.sh - API 測試腳本 + +API_URL="http://localhost:3002" + +# 健康檢查 +echo "=== Health Check ===" +curl -s "$API_URL/health" | jq . + +# 搜尋 +echo -e "\n=== Search ===" +curl -s -X POST "$API_URL/api/v1/search" \ + -H "Content-Type: application/json" \ + -d '{"query": "test", "limit": 3}' | jq . + +# 列出影片 +echo -e "\n=== Videos ===" +curl -s "$API_URL/api/v1/videos" | jq '.videos | length' +``` + +--- + +## 相關文件 + +- [API_INDEX.md](./API_INDEX.md) - 文件總覽(起點) +- [API_ENDPOINTS.md](./API_ENDPOINTS.md) - 端點完整說明 +- [API_N8N_GUIDE.md](./API_N8N_GUIDE.md) - n8n 使用範例 +- [API_WORDPRESS_GUIDE.md](./API_WORDPRESS_GUIDE.md) - WordPress 使用範例 diff --git a/docs/API_ENDPOINTS.md b/docs/API_ENDPOINTS.md index 3989dfc..1773b8a 100644 --- a/docs/API_ENDPOINTS.md +++ b/docs/API_ENDPOINTS.md @@ -16,9 +16,34 @@ --- +## 認證 + +除健康檢查端點外,所有 API 端點都需要 API Key。 + +### Header 方式 + +```bash +curl -H "X-API-Key: your-api-key" http://localhost:3002/api/v1/videos +``` + +### 響應 + +- `401 Unauthorized` - 缺少或無效的 API Key +- `200 OK` - 認證成功 + +### 取得 API Key + +使用 CLI 建立: + +```bash +./target/release/momentry api-key create "My API Key" --key-type user +``` + +--- + ## 端點列表 -### 健康檢查 +### 健康檢查(公開) | 方法 | 端點 | 說明 | |------|------|------| @@ -213,5 +238,7 @@ sudo launchctl load /Library/LaunchDaemons/com.momentry.api.plist ## 相關文件 - [API_INDEX.md](./API_INDEX.md) - 文件總覽(起點) -- [API_N8N_GUIDE.md](./API_N8N_GUIDE.md) - n8n 使用範例 -- [API_WORDPRESS_GUIDE.md](./API_WORDPRESS_GUIDE.md) - WordPress 使用範例 +- [API_EXAMPLES.md](./API_EXAMPLES.md) - **完整範例總覽(curl / n8n / WordPress)** +- [API_N8N_GUIDE.md](./API_N8N_GUIDE.md) - n8n 詳細指南 +- [API_WORDPRESS_GUIDE.md](./API_WORDPRESS_GUIDE.md) - WordPress 詳細指南 +- [API_CURL_EXAMPLES.md](./API_CURL_EXAMPLES.md) - curl 範例 diff --git a/docs/API_EXAMPLES.md b/docs/API_EXAMPLES.md new file mode 100644 index 0000000..1abb193 --- /dev/null +++ b/docs/API_EXAMPLES.md @@ -0,0 +1,726 @@ +# Momentry Core API 使用範例總覽 + +| 項目 | 內容 | +|------|------| +| 版本 | V2.0 | +| 日期 | 2026-03-25 | +| Base URL (本地) | `http://localhost:3002` | +| Base URL (外部) | `https://api.momentry.ddns.net` | + +--- + +## 快速參考 + +### 環境 URL 選擇 + +| 環境 | URL | 用途 | +|------|-----|------| +| **本地開發** | `http://localhost:3002` | 開發/測試,直接訪問 API | +| **外部訪問** | `https://api.momentry.ddns.net` | n8n、WordPress、curl 生產環境 | + +### 所有可用端點 + +| 方法 | 端點 | 說明 | +|------|------|------| +| GET | `/health` | 健康檢查 | +| GET | `/health/detailed` | 詳細健康檢查 | +| POST | `/api/v1/search` | 語意搜尋(標準格式) | +| POST | `/api/v1/n8n/search` | 語意搜尋(n8n 格式) | +| POST | `/api/v1/search/hybrid` | 混合搜尋 | +| POST | `/api/v1/register` | 註冊影片 | +| POST | `/api/v1/probe` | 探測影片資訊 | +| GET | `/api/v1/videos` | 列出所有影片 | +| GET | `/api/v1/lookup` | 查詢影片 | +| GET | `/api/v1/progress/:uuid` | 處理進度 | +| GET | `/api/v1/jobs` | 任務列表 | +| GET | `/api/v1/jobs/:uuid` | 任務詳情 | + +--- + +## 認證 + +### API Key + +所有 `/api/v1/*` 端點需要 API Key 認證。 + +```bash +# 添加 API Key Header +curl -H "X-API-Key: your-api-key" http://localhost:3002/api/v1/videos + +# 範例 +curl -H "X-API-Key: muser_f08e13ba967e4d8ea8fc542ad9f99ac8_1774416728_90472a35" \ + http://localhost:3002/api/v1/videos +``` + +### 響應狀態 + +| 狀態碼 | 說明 | +|--------|------| +| 200 | 成功 | +| 401 | 未授權(缺少或無效 API Key) | +| 500 | 伺服器錯誤 | + +### 建立 API Key + +```bash +./target/release/momentry api-key create "My Key" --key-type user +``` + +--- + +## 1. curl 範例 + +### 基本語法 + +```bash +# 格式 +curl [OPTIONS] URL + +# 常用選項 +-X METHOD # HTTP 方法 (GET, POST, etc.) +-H HEADER # 添加 HTTP 標頭 +-d DATA # POST 請求體 +-s # 靜默模式 +-w FORMAT # 輸出額外信息 +``` + +### 1.1 健康檢查 + +```bash +# 基本健康檢查 +curl http://localhost:3002/health + +# 詳細健康檢查 +curl http://localhost:3002/health/detailed +``` + +**回應**: +```json +{"status":"ok","version":"0.1.0","uptime_ms":123456} +``` + +### 1.2 語意搜尋 + +```bash +# 標準格式搜尋 +curl -X POST http://localhost:3002/api/v1/search \ + -H "Content-Type: application/json" \ + -d '{"query": "charade", "limit": 5}' + +# n8n 格式搜尋(推薦) +curl -X POST http://localhost:3002/api/v1/n8n/search \ + -H "Content-Type: application/json" \ + -d '{"query": "charade", "limit": 5}' + +# 混合搜尋 +curl -X POST http://localhost:3002/api/v1/search/hybrid \ + -H "Content-Type: application/json" \ + -d '{"query": "charade", "limit": 5}' +``` + +**標準格式回應**: +```json +{ + "results": [ + { + "uuid": "a1b10138a6bbb0cd", + "chunk_id": "sentence_0001", + "chunk_type": "sentence", + "start_time": 48.8, + "end_time": 55.44, + "text": "fun plot twists...", + "score": 0.92 + } + ], + "query": "charade" +} +``` + +**n8n 格式回應**: +```json +{ + "query": "charade", + "count": 1, + "hits": [ + { + "id": "sentence_0001", + "vid": "a1b10138a6bbb0cd", + "start": 48.8, + "end": 55.44, + "title": "Chunk sentence_0001", + "text": "fun plot twists...", + "score": 0.92, + "media_url": "https://wp.momentry.ddns.net/video.mp4" + } + ] +} +``` + +### 1.3 影片管理 + +```bash +# 列出所有影片 +curl http://localhost:3002/api/v1/videos + +# 查詢特定影片(依 UUID) +curl "http://localhost:3002/api/v1/lookup?uuid=a1b10138a6bbb0cd" + +# 查詢特定影片(依路徑) +curl "http://localhost:3002/api/v1/lookup?path=/path/to/video.mp4" + +# 取得處理進度 +curl http://localhost:3002/api/v1/progress/a1b10138a6bbb0cd + +# 探測影片(不註冊) +curl -X POST http://localhost:3002/api/v1/probe \ + -H "Content-Type: application/json" \ + -d '{"path": "/path/to/video.mp4"}' + +# 註冊影片 +curl -X POST http://localhost:3002/api/v1/register \ + -H "Content-Type: application/json" \ + -d '{"path": "/path/to/video.mp4", "file_name": "video.mp4"}' +``` + +### 1.4 批次測試腳本 + +```bash +#!/bin/bash +# api_test.sh - API 測試腳本 + +API_URL="http://localhost:3002" + +echo "=== 健康檢查 ===" +curl -s "$API_URL/health" | jq . + +echo -e "\n=== 語意搜尋 ===" +curl -s -X POST "$API_URL/api/v1/search" \ + -H "Content-Type: application/json" \ + -d '{"query": "charade", "limit": 3}' | jq . + +echo -e "\n=== 影片列表 ===" +curl -s "$API_URL/api/v1/videos" | jq '.videos | length' +``` + +### 1.5 外部 URL 範例 + +```bash +# 使用外部 URL(需網路可達) +curl https://api.momentry.ddns.net/health + +# 外部搜尋 +curl -X POST https://api.momentry.ddns.net/api/v1/n8n/search \ + -H "Content-Type: application/json" \ + -d '{"query": "charade", "limit": 5}' +``` + +--- + +## 2. n8n 範例 + +### 2.1 HTTP Request Node 設定 + +``` +Node: HTTP Request +├── URL: https://api.momentry.ddns.net/api/v1/n8n/search +├── Method: POST +├── Authentication: None +├── Send Body: ✓ (checked) +├── Content Type: JSON +└── Body: + { + "query": "={{ $json.query }}", + "limit": "={{ $json.limit || 10 }}" + } +``` + +### 2.2 基本搜尋 Workflow + +```json +{ + "name": "Momentry Video Search", + "nodes": [ + { + "parameters": {}, + "name": "Manual Trigger", + "type": "n8n-nodes-base.manualTrigger", + "position": [250, 300] + }, + { + "parameters": { + "url": "https://api.momentry.ddns.net/api/v1/n8n/search", + "method": "POST", + "sendBody": true, + "contentType": "json", + "body": { + "query": "charade", + "limit": 3 + } + }, + "name": "Search Video API", + "type": "n8n-nodes-base.httpRequest", + "position": [450, 300] + } + ], + "connections": { + "Manual Trigger": { + "main": [[{"node": "Search Video API"}]] + } + } +} +``` + +### 2.3 Webhook 動態搜尋 + +```json +{ + "name": "Momentry Dynamic Search", + "nodes": [ + { + "parameters": { + "httpMethod": "POST", + "path": "search", + "responseMode": "lastNode" + }, + "name": "Webhook", + "type": "n8n-nodes-base.webhook", + "position": [250, 300] + }, + { + "parameters": { + "url": "https://api.momentry.ddns.net/api/v1/n8n/search", + "method": "POST", + "sendBody": true, + "contentType": "json", + "body": { + "query": "={{ JSON.stringify($json.body.query) }}", + "limit": "={{ $json.body.limit || 5 }}" + } + }, + "name": "Search API", + "type": "n8n-nodes-base.httpRequest", + "position": [450, 300] + } + ], + "connections": { + "Webhook": { + "main": [[{"node": "Search API"}]] + } + } +} +``` + +### 2.4 測試 Webhook + +```bash +# 測試模式(需先在 n8n UI 點擊 Execute) +curl -X POST http://localhost:5678/webhook-test/video-rag-mcp \ + -H "Content-Type: application/json" \ + -d '{"query":"charade","limit":3}' + +# 生產環境(需 workflow 為 Active 狀態) +curl -X POST http://localhost:5678/webhook/video-rag-mcp \ + -H "Content-Type: application/json" \ + -d '{"query":"charade","limit":3}' +``` + +### 2.5 健康檢查 Workflow + +```json +{ + "name": "Momentry Health Check", + "nodes": [ + { + "parameters": {}, + "name": "Manual Trigger", + "type": "n8n-nodes-base.manualTrigger", + "position": [250, 300] + }, + { + "parameters": { + "url": "https://api.momentry.ddns.net/health", + "method": "GET" + }, + "name": "Health Check", + "type": "n8n-nodes-base.httpRequest", + "position": [450, 300] + } + ], + "connections": { + "Manual Trigger": { + "main": [[{"node": "Health Check"}]] + } + } +} +``` + +### 2.6 錯誤處理 + +| 錯誤 | 原因 | 解決 | +|------|------|------| +| 502 Bad Gateway | API 服務未啟動 | `sudo launchctl load /Library/LaunchDaemons/com.momentry.api.plist` | +| "Your request is invalid" | Body 格式設定錯誤 | 確認 Content Type: JSON,Body 為有效 JSON | +| 404 on webhook-test | 未執行 workflow | 在 n8n UI 點擊 "Execute workflow" | + +--- + +## 3. WordPress 範例 + +### 3.1 PHP 基本用法 + +```php + 'charade', + 'limit' => 10 +]; + +$response = wp_remote_post($api_url, [ + 'headers' => ['Content-Type' => 'application/json'], + 'body' => json_encode($data), + 'timeout' => 30 +]); + +if (is_wp_error($response)) { + echo '錯誤: ' . $response->get_error_message(); +} else { + $body = json_decode(wp_remote_retrieve_body($response), true); + print_r($body['hits']); +} +?> +``` + +### 3.2 列出影片 + +```php + 30]); + +if (!is_wp_error($response)) { + $body = json_decode(wp_remote_retrieve_body($response), true); + foreach ($body['videos'] as $video) { + echo $video['file_name'] . "\n"; + } +} +?> +``` + +### 3.3 查詢特定影片 + +```php + 30]); + +if (!is_wp_error($response)) { + $video = json_decode(wp_remote_retrieve_body($response), true); + echo '檔案: ' . $video['file_name'] . "\n"; + echo '時長: ' . $video['duration'] . ' 秒'; +} +?> +``` + +### 3.4 JavaScript fetch + +```javascript +// 搜尋影片 +async function searchVideos(query, limit = 10) { + const response = await fetch('https://api.momentry.ddns.net/api/v1/n8n/search', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ query, limit }) + }); + + if (!response.ok) { + throw new Error('API 請求失敗'); + } + + return await response.json(); +} + +// 使用範例 +searchVideos('charade', 5) + .then(data => { + data.hits.forEach(hit => { + console.log(`${hit.text} (score: ${hit.score})`); + }); + }); +``` + +### 3.5 WordPress Shortcode + +在 `functions.php` 中註冊短碼: + +```php + '', + 'limit' => '10' + ], $atts); + + if (empty($atts['query'])) { + return '

請提供搜尋關鍵字

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

搜尋服務暫時無法使用

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

找不到相關結果

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

請提供搜尋關鍵字

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

搜尋服務暫時無法使用

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

找不到相關結果

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

影片列表

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

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

請提供搜尋關鍵字

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

」搜尋結果

+ +

沒有找到相關結果

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

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