feat: AI 治理完備 V10.3 — 技術債清零 + DB 備份機制 + 備份 AI 監控
Some checks are pending
CD Pipeline / deploy (push) Waiting to run
Some checks are pending
CD Pipeline / deploy (push) Waiting to run
技術債清零 (2026-04-19): - migrations/010: ai_insights 補 decay_exempt/avg_quality/status/ai_model/feedback 欄位 - migrations/011: embedding_retry_queue 持久化表 (ADR-009) - migrations/012: backup_log 備份記錄表 - services/openclaw_learning_service: 記憶體 Queue → DB retry queue,時間衰減 RAG - services/nemoton_dispatcher_service: 三個 tool 強制雙寫 ai_insights (_sink_insight_to_km) - services/import_service: Excel 前置欄位防禦(商品名稱類 + 業績金額類) - services/ollama_service: generate_embedding 新增 EMBEDDING_HOST env,embedding 永遠走 192.168.0.111 - SYSTEM_VERSION: V9.4 → V10.3 DB 備份機制: - scripts/pg_backup.sh: host-level pg_dump 備份腳本,cron 每日 02:00,保留 7 天,Telegram 通知 - services/db_backup_service.py: Python 備份 service,寫入 backup_log - scheduler: run_db_backup_task (02:00) + run_backup_monitor_task (每 6h AI Agent 監控) - Dockerfile: 加入 postgresql-client 文件: - CLAUDE.md: 環境架構依 ADR-008 實地重寫,含完整 SSH/Docker 部署 SOP - PROJECT_CONSTITUTION.md: 內容已整合入 CLAUDE.md,刪除重複檔案 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -2,8 +2,19 @@
|
||||
|
||||
> 本文件定義專案開發的核心準則與不可違反的規範
|
||||
> **建立日期**: 2026-01-12
|
||||
> **當前版本**: V10.1
|
||||
> **最後更新**: 2026-04-18(加入第十三、十四章 AI 架構與 Claude Code 官方規範)
|
||||
> **當前版本**: V10.2 (治理與安全重疊整合版)
|
||||
> **最後更新**: 2026-04-18
|
||||
|
||||
---
|
||||
|
||||
## 🗣️ 第零章:溝通與語法原則
|
||||
### 第 0.1 條:語言使用
|
||||
- **所有溝通一律使用繁體中文**。包含:程式碼註解、文檔說明、Commit 訊息、錯誤訊息、日誌輸出、使用者介面文字。
|
||||
|
||||
### 第 0.2 條:文檔規範
|
||||
- 所有文檔使用 Markdown 格式。
|
||||
- 檔案名稱優先使用英文大寫加底線(例:`CONSTITUTION.md`)。
|
||||
- 重要變更需記錄在 `CLAUDE.md` 或 `walkthrough.md` 中。
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ RUN apt-get update && apt-get install -y \
|
||||
g++ \
|
||||
curl \
|
||||
libpq-dev \
|
||||
postgresql-client \
|
||||
# Chrome/Selenium 依賴
|
||||
wget \
|
||||
gnupg \
|
||||
|
||||
@@ -1,599 +0,0 @@
|
||||
# MOMO 監控系統 - 專案憲法
|
||||
|
||||
**版本:** 1.3
|
||||
**制定日期:** 2026-01-12
|
||||
**最後更新:** 2026-01-14
|
||||
|
||||
---
|
||||
|
||||
## 📜 專案基本原則
|
||||
|
||||
本憲法定義了 MOMO 監控系統的開發規範、溝通原則、安全政策及技術標準。所有參與者(開發人員、AI 助手、維護人員)必須遵守以下規範。
|
||||
|
||||
---
|
||||
|
||||
## 🗣️ 第一章:溝通規範
|
||||
|
||||
### 第 1 條:語言使用
|
||||
- **所有溝通一律使用繁體中文**
|
||||
- 包含但不限於:
|
||||
- 程式碼註解
|
||||
- 文檔說明
|
||||
- Commit 訊息
|
||||
- 錯誤訊息
|
||||
- 日誌輸出
|
||||
- 使用者介面文字
|
||||
- README 和文檔
|
||||
|
||||
### 第 2 條:文檔規範
|
||||
- 所有文檔檔案使用 Markdown 格式(`.md`)
|
||||
- 檔案名稱使用英文大寫加底線(例:`PROJECT_CONSTITUTION.md`)
|
||||
- 文檔內容必須包含版本號和最後更新日期
|
||||
- 重要變更需記錄在 CHANGELOG 中
|
||||
|
||||
### 第 3 條:註解規範
|
||||
- Python 函數必須包含繁體中文 docstring
|
||||
- 複雜邏輯必須添加行內註解說明
|
||||
- 註解應說明「為什麼」而非「做什麼」
|
||||
|
||||
---
|
||||
|
||||
## 🔒 第二章:安全政策
|
||||
|
||||
### 第 4 條:敏感資訊管理
|
||||
- **禁止在程式碼中硬編碼任何敏感資訊**
|
||||
- 所有憑證、API 金鑰、密碼必須使用環境變數(`.env`)
|
||||
- `.env` 檔案必須列入 `.gitignore`
|
||||
- 提供 `.env.example` 作為範本
|
||||
|
||||
### 第 5 條:密碼安全
|
||||
- 所有密碼必須使用 `pbkdf2:sha256` 雜湊儲存
|
||||
- 禁止使用明文密碼(僅過渡期允許,需發出警告)
|
||||
- 密碼長度至少 8 個字元,包含英文字母和數字
|
||||
- 登入失敗 5 次後鎖定帳號 5 分鐘
|
||||
|
||||
### 第 6 條:輸入驗證
|
||||
- **所有使用者輸入必須經過驗證**
|
||||
- SQL 查詢必須使用參數化查詢或白名單驗證
|
||||
- 檔案上傳必須驗證副檔名和檔案大小
|
||||
- 路徑操作必須使用 `safe_join()` 防止路徑遍歷
|
||||
|
||||
### 第 7 條:CSRF 防護
|
||||
- 所有 POST/PUT/DELETE/PATCH 請求必須包含 CSRF token
|
||||
- HTML 表單使用 hidden input: `<input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/>`
|
||||
- AJAX 請求使用 header: `'X-CSRFToken': getCSRFToken()`
|
||||
|
||||
### 第 8 條:Session 安全
|
||||
- Session cookie 必須設定 `HttpOnly=True`
|
||||
- Session cookie 必須設定 `SameSite=Lax`
|
||||
- 生產環境必須設定 `Secure=True`(HTTPS)
|
||||
- Session 有效期設定為 2 小時
|
||||
|
||||
---
|
||||
|
||||
## 💻 第三章:程式碼規範
|
||||
|
||||
### 第 9 條:檔案上傳
|
||||
- 僅允許上傳:`.xlsx`, `.xls`, `.csv`
|
||||
- 檔案大小限制:10 MB
|
||||
- 使用 `secure_filename_unicode()` 清理檔名(支援中文)
|
||||
- 檢查路徑遍歷攻擊(`..`, `/`, `\`)
|
||||
|
||||
### 第 10 條:SQL 安全
|
||||
- 表名驗證:僅允許英文字母、數字、底線
|
||||
- 欄位名驗證:允許中文、英文字母、數字、底線
|
||||
- 時間戳驗證:嚴格遵守 `YYYY-MM-DD HH:MM:SS` 格式
|
||||
- 使用 `safe_read_sql()` 進行安全的 SQL 查詢
|
||||
|
||||
### 第 11 條:路徑安全
|
||||
- 所有路徑拼接使用 `safe_join(base, *paths)`
|
||||
- 檢查 Windows 反斜線、連續點、雙點模式
|
||||
- 驗證最終路徑在基礎目錄內
|
||||
- 偵測到攻擊時記錄安全日誌
|
||||
|
||||
### 第 12 條:日誌規範
|
||||
- 使用結構化日誌格式:`[模組] [級別] 訊息 | 詳細資訊`
|
||||
- 安全事件使用 `[Security]` 標籤
|
||||
- 記錄所有失敗的驗證嘗試
|
||||
- 日誌級別:
|
||||
- `ERROR`: 系統錯誤
|
||||
- `WARNING`: 安全警告、失敗的攻擊嘗試
|
||||
- `INFO`: 重要操作成功
|
||||
- `DEBUG`: 詳細除錯資訊
|
||||
|
||||
---
|
||||
|
||||
## 🕷️ 第四章:數據爬取規範
|
||||
|
||||
### 第 13 條:爬蟲程式碼穩定性原則
|
||||
- **爬蟲程式碼屬於核心業務邏輯,修改時必須格外謹慎**
|
||||
- 任何修改必須經過完整測試,確認不影響現有爬取功能
|
||||
- 修改前必須備份現有可運作的版本
|
||||
- 修改後必須驗證所有爬蟲任務正常執行
|
||||
|
||||
### 第 14 條:爬蟲選擇器維護
|
||||
- **CSS 選擇器和 XPath 是脆弱的依賴**
|
||||
- 修改選擇器前必須:
|
||||
1. 記錄修改原因(網站改版、元素變更等)
|
||||
2. 測試新選擇器是否正確抓取目標資料
|
||||
3. 保留舊選擇器作為註解備份
|
||||
4. 記錄網站結構變更日期
|
||||
- 建議使用多層次選擇器備援(主選擇器 + 備用選擇器)
|
||||
|
||||
### 第 15 條:爬蟲錯誤處理
|
||||
- **所有爬蟲函數必須包含完整的錯誤處理**
|
||||
- 必須處理的情況:
|
||||
1. 網路連線失敗
|
||||
2. 頁面載入超時
|
||||
3. 元素找不到(選擇器失效)
|
||||
4. 資料格式異常
|
||||
5. 反爬蟲機制觸發
|
||||
- 錯誤發生時:
|
||||
- 記錄詳細錯誤日誌(包含 URL、選擇器、錯誤訊息)
|
||||
- 發送通知給管理員
|
||||
- 不中斷其他爬蟲任務
|
||||
- 保存最後成功的資料作為備援
|
||||
|
||||
### 第 16 條:爬蟲測試要求
|
||||
- **修改爬蟲程式碼後必須執行完整測試**
|
||||
- 測試項目:
|
||||
1. 單一商品資料爬取
|
||||
2. 列表頁面分頁爬取
|
||||
3. 多執行緒/並發爬取
|
||||
4. 錯誤處理機制
|
||||
5. 資料儲存完整性
|
||||
- 測試環境應模擬生產環境(網路延遲、並發請求)
|
||||
- 使用測試資料集驗證爬取結果準確性
|
||||
|
||||
### 第 17 條:爬蟲依賴管理
|
||||
- **爬蟲依賴的套件版本必須固定**
|
||||
- `requirements.txt` 中爬蟲相關套件必須指定版本號:
|
||||
- `selenium==4.x.x` (具體版本)
|
||||
- `requests==2.x.x`
|
||||
- `beautifulsoup4==4.x.x`
|
||||
- 升級套件前必須:
|
||||
1. 在測試環境驗證相容性
|
||||
2. 檢查 changelog 確認無破壞性變更
|
||||
3. 執行完整爬蟲測試套件
|
||||
4. 記錄升級原因和影響
|
||||
|
||||
### 第 18 條:網站結構變更應對
|
||||
- **定期檢查目標網站結構是否變更**
|
||||
- 建立網站結構監控機制:
|
||||
1. 記錄關鍵元素的 HTML 結構
|
||||
2. 定期比對結構變化
|
||||
3. 發現變更時立即通知
|
||||
4. 建立選擇器失效告警
|
||||
- 保存網站結構快照(HTML samples)供除錯使用
|
||||
|
||||
### 第 19 條:爬蟲效能與禮節
|
||||
- **遵守網站的 robots.txt 規範**
|
||||
- 設定合理的請求間隔(建議 1-3 秒)
|
||||
- 使用 User-Agent 識別身份
|
||||
- 避免在網站高峰時段進行大量爬取
|
||||
- 實作請求失敗的退避重試機制(Exponential Backoff)
|
||||
|
||||
### 第 20 條:資料驗證與清洗
|
||||
- **爬取的資料必須經過驗證**
|
||||
- 驗證項目:
|
||||
1. 價格範圍合理性(不可為 0 或異常大)
|
||||
2. 日期格式正確性
|
||||
3. 必填欄位完整性
|
||||
4. 資料型別正確性
|
||||
- 發現異常資料時:
|
||||
- 記錄到錯誤日誌
|
||||
- 標記為「需人工審核」
|
||||
- 不自動儲存到資料庫
|
||||
- 發送通知給管理員
|
||||
|
||||
### 第 21 條:爬蟲版本控制
|
||||
- **爬蟲程式碼每次修改必須建立 Git commit**
|
||||
- Commit 訊息格式:
|
||||
- `[Crawler] [網站名稱] 修改描述`
|
||||
- 例:`[Crawler] [MOMO] 修復商品價格選擇器失效問題`
|
||||
- 重大修改應建立分支,測試通過後才合併
|
||||
- 保留至少最近 3 個可運作版本的備份
|
||||
|
||||
### 第 22 條:爬蟲文檔要求
|
||||
- **每個爬蟲模組必須包含詳細文檔**
|
||||
- 必須記錄:
|
||||
1. 爬取目標(網站 URL、資料類型)
|
||||
2. 執行頻率(每小時/每日)
|
||||
3. 關鍵選擇器說明
|
||||
4. 已知問題和限制
|
||||
5. 最後修改日期和原因
|
||||
6. 聯絡人/負責人
|
||||
- 文檔應隨程式碼更新
|
||||
|
||||
---
|
||||
|
||||
## 🧪 第五章:測試與品質保證
|
||||
|
||||
### 第 23 條:測試覆蓋
|
||||
- 所有安全功能必須有對應的測試
|
||||
- 測試必須包含正常情況和攻擊情境
|
||||
- 使用 `test_*.py` 命名測試檔案
|
||||
- 執行 `./run_security_tests.sh` 必須全部通過
|
||||
|
||||
### 第 24 條:安全測試項目
|
||||
必須測試以下項目:
|
||||
1. 環境變數與憑證管理
|
||||
2. SQL 注入防護
|
||||
3. 路徑遍歷防護
|
||||
4. 檔案上傳驗證
|
||||
5. CSRF 防護
|
||||
6. 登入驗證強化
|
||||
7. Flask 安全配置
|
||||
|
||||
### 第 25 條:爬蟲測試項目
|
||||
必須測試以下項目:
|
||||
1. 選擇器有效性測試
|
||||
2. 資料完整性測試
|
||||
3. 錯誤處理測試
|
||||
4. 並發爬取測試
|
||||
5. 效能壓力測試
|
||||
|
||||
### 第 26 條:程式碼審查
|
||||
- 所有涉及安全的程式碼變更必須經過審查
|
||||
- 所有涉及爬蟲的程式碼變更必須經過審查
|
||||
- 檢查是否符合本憲法規範
|
||||
- 驗證是否通過完整測試
|
||||
- 確認日誌和錯誤處理完整
|
||||
|
||||
---
|
||||
|
||||
## 📦 第六章:部署與維運
|
||||
|
||||
### 第 27 條:環境管理規範
|
||||
|
||||
#### 27.1 環境分層
|
||||
本系統採用三層環境架構:
|
||||
|
||||
1. **開發環境 (Development)**
|
||||
- 位置:`/Users/ogt/momo_pro_system` (macOS Local)
|
||||
- 用途:程式碼開發、快速測試、UI/UX 調整
|
||||
- 運行方式:直接執行 `python app.py`
|
||||
- 特性:即時修改、快速迭代
|
||||
|
||||
2. **測試環境 (Testing)**
|
||||
- 位置:同開發環境
|
||||
- 用途:功能測試、安全測試、回歸測試
|
||||
- 運行方式:執行測試腳本
|
||||
|
||||
3. **正式環境 (Production)**
|
||||
- 位置:`/home/ogt/momo_pro_system` (GCP VM)
|
||||
- 用途:生產服務、24/7 運行
|
||||
- 運行方式:systemd service
|
||||
- 網址:`https://momo.wooo.work`
|
||||
|
||||
#### 27.2 環境同步原則
|
||||
|
||||
**嚴格禁止**:
|
||||
- ❌ 直接在正式環境修改程式碼
|
||||
- ❌ 跳過測試直接部署到正式環境
|
||||
- ❌ 混用不同環境的資料庫
|
||||
- ❌ 將 `.env` 檔案上傳到 Git
|
||||
|
||||
**必須遵守**:
|
||||
- ✅ 所有修改必須在開發環境完成
|
||||
- ✅ 完整測試通過後才能部署
|
||||
- ✅ 部署前必須備份正式環境
|
||||
- ✅ 部署後必須驗證功能正常
|
||||
- ✅ 監控 24 小時確保穩定
|
||||
|
||||
#### 27.3 標準部署流程
|
||||
|
||||
參照 `DEPLOYMENT_WORKFLOW.md` 文檔,嚴格遵守以下流程:
|
||||
|
||||
```
|
||||
開發 → 測試 → 備份 → 部署 → 驗證 → 監控
|
||||
```
|
||||
|
||||
**階段性檢查**:
|
||||
1. **開發階段**:程式碼符合規範、Git commit 完成
|
||||
2. **測試階段**:功能測試、安全測試、爬蟲測試通過
|
||||
3. **部署前**:備份正式環境、確認修改檔案清單
|
||||
4. **部署中**:使用標準部署腳本或手動部署
|
||||
5. **部署後**:服務狀態正常、功能驗證通過
|
||||
6. **監控期**:持續監控 24 小時
|
||||
|
||||
#### 27.4 部署方法選擇
|
||||
|
||||
**方法 A:完整部署**(推薦用於大改動)
|
||||
```bash
|
||||
cd /Users/ogt/momo_pro_system
|
||||
```
|
||||
適用於:
|
||||
- Python 程式碼修改
|
||||
- 依賴套件更新
|
||||
- 配置檔案變更
|
||||
- 資料庫結構變更
|
||||
|
||||
**方法 B:快速更新**(用於小改動)
|
||||
```bash
|
||||
gcloud compute scp --zone=asia-east1-a 修改的檔案 momo-server:~/momo_pro_system/
|
||||
```
|
||||
適用於:
|
||||
- HTML/CSS/JS 檔案修改
|
||||
- 模板檔案更新
|
||||
- 靜態資源更新
|
||||
|
||||
**重要**:
|
||||
- HTML/CSS/JS 修改:不需重啟服務(Flask 自動重載模板)
|
||||
- Python 檔案修改:必須重啟服務
|
||||
- 配置檔案修改:必須重啟服務
|
||||
|
||||
### 第 28 條:變更前強制備份原則 ⚠️
|
||||
|
||||
**重要性:最高優先級**
|
||||
|
||||
**核心原則**:
|
||||
- **所有涉及嚴重影響的變更、修改操作,變更前必須先進行完整備份**
|
||||
- **備份完成並確認無誤後,才可進行變更**
|
||||
- 違反此原則的變更操作視為嚴重違規
|
||||
|
||||
**適用範圍**:
|
||||
以下操作在執行前必須完整備份:
|
||||
|
||||
1. **資料庫相關**
|
||||
- 資料庫結構變更(ALTER TABLE, DROP, CREATE)
|
||||
- 大量資料修改或刪除(UPDATE, DELETE 影響 >100 筆)
|
||||
- 資料庫升級或遷移
|
||||
- 索引重建或優化
|
||||
|
||||
2. **系統配置**
|
||||
- 系統配置檔案修改(config.py, .env)
|
||||
- Nginx/Apache 配置變更
|
||||
- Systemd service 配置修改
|
||||
- 排程任務(crontab, scheduler)變更
|
||||
|
||||
3. **核心程式碼**
|
||||
- 爬蟲核心邏輯修改
|
||||
- 資料庫連線和 ORM 修改
|
||||
- 認證和安全模組修改
|
||||
- API 端點的破壞性變更
|
||||
|
||||
4. **部署操作**
|
||||
- 生產環境程式碼更新
|
||||
- Python 依賴套件升級
|
||||
- 系統套件升級(Python, Node.js 等)
|
||||
- 伺服器遷移或重啟
|
||||
|
||||
5. **資料處理**
|
||||
- Excel 匯入覆蓋現有資料
|
||||
- 批次資料清理或轉換
|
||||
- 歷史資料歸檔或刪除
|
||||
|
||||
**備份要求**:
|
||||
|
||||
**必須備份的內容**:
|
||||
- 完整資料庫檔案(momo.db 或 PostgreSQL dump)
|
||||
- 所有程式碼檔案(Git commit + 檔案副本)
|
||||
- 配置檔案(config.py, .env, nginx.conf 等)
|
||||
- 重要的資料檔案(Excel, CSV 等)
|
||||
|
||||
**備份驗證**:
|
||||
- 檢查備份檔案完整性(檔案大小、MD5 校驗)
|
||||
- 確認備份可讀取(嘗試開啟資料庫)
|
||||
- 記錄備份時間和檔案位置
|
||||
- 確保備份檔案有足夠的磁碟空間
|
||||
|
||||
**備份命名規範**:
|
||||
```
|
||||
資料庫:momo_backup_YYYYMMDD_HHMMSS.db
|
||||
程式碼:momo_code_backup_YYYYMMDD_HHMMSS.tar.gz
|
||||
配置:config_backup_YYYYMMDD_HHMMSS.tar.gz
|
||||
```
|
||||
|
||||
**復原計畫**:
|
||||
- 每次重大變更必須準備復原步驟文件
|
||||
- 測試復原流程的可行性
|
||||
- 記錄復原所需時間
|
||||
- 確保有回滾機制
|
||||
|
||||
**違規處理**:
|
||||
- 未備份就執行重大變更:視為一級違規
|
||||
- 備份不完整或無法復原:視為二級違規
|
||||
- 必須立即停止變更,進行損害評估
|
||||
- 記錄事件並更新操作規範
|
||||
|
||||
**例外情況**:
|
||||
僅以下情況可豁免備份要求:
|
||||
- 純前端 HTML/CSS/JS 修改(不影響資料)
|
||||
- 日誌檔案查看(唯讀操作)
|
||||
- 系統監控和狀態查詢
|
||||
- 測試環境的實驗性變更
|
||||
|
||||
### 第 29 條:環境配置
|
||||
- 開發環境使用 `.env`
|
||||
- 生產環境使用環境變數注入
|
||||
- 不同環境使用不同的 `SECRET_KEY`
|
||||
- 定期輪換敏感憑證
|
||||
|
||||
### 第 30 條:備份策略
|
||||
- 資料庫每日自動備份
|
||||
- 備份檔案加密保存
|
||||
- 保留最近 7 天備份
|
||||
- 使用 `safe_join()` 處理備份路徑
|
||||
|
||||
### 第 31 條:更新流程
|
||||
1. **評估變更影響**(是否需要備份,參照第 28 條)
|
||||
2. **完整備份**(若屬於重大變更,必須先備份)
|
||||
3. 更新程式碼
|
||||
4. 執行完整測試套件(安全測試 + 爬蟲測試)
|
||||
5. 檢查安全日誌
|
||||
6. 重啟服務
|
||||
7. 驗證功能正常(包含爬蟲任務)
|
||||
8. 監控 24 小時確保穩定
|
||||
9. 確認備份可刪除或歸檔
|
||||
|
||||
---
|
||||
|
||||
## 🚨 第七章:事件處理
|
||||
|
||||
### 第 32 條:安全事件
|
||||
發現安全漏洞時:
|
||||
1. 立即記錄詳細資訊
|
||||
2. 評估風險等級(Critical/High/Medium/Low)
|
||||
3. 優先處理 Critical 和 High 級別
|
||||
4. 修復後執行完整測試
|
||||
5. 更新 `SECURITY_FIX_SUMMARY.md`
|
||||
|
||||
### 第 33 條:爬蟲異常事件
|
||||
發現爬蟲異常時:
|
||||
1. 記錄詳細錯誤資訊(URL、選擇器、錯誤訊息)
|
||||
2. 檢查是否為網站結構變更
|
||||
3. 若為選擇器失效,立即修復並測試
|
||||
4. 發送通知給管理員
|
||||
5. 記錄在爬蟲維護日誌中
|
||||
|
||||
### 第 34 條:錯誤處理
|
||||
- 所有錯誤必須妥善處理,不得暴露敏感資訊
|
||||
- 使用者看到的錯誤訊息應簡潔明確
|
||||
- 詳細錯誤資訊記錄在日誌中
|
||||
- 開發環境可顯示詳細錯誤,生產環境僅顯示通用訊息
|
||||
|
||||
---
|
||||
|
||||
## 🔧 第八章:開發工具與依賴
|
||||
|
||||
### 第 35 條:Python 依賴
|
||||
核心依賴套件(見 `requirements.txt`):
|
||||
- Flask (Web 框架)
|
||||
- Flask-WTF (CSRF 防護)
|
||||
- SQLAlchemy (ORM)
|
||||
- pandas (資料處理)
|
||||
- selenium (網頁自動化)
|
||||
- werkzeug (安全工具)
|
||||
|
||||
### 第 36 條:版本控制
|
||||
- 使用 Git 進行版本控制
|
||||
- Commit 訊息使用繁體中文
|
||||
- Commit 格式:`[模組] 簡短描述`
|
||||
- 例:`[Security] 修復路徑遍歷漏洞`
|
||||
- 例:`[Crawler] [MOMO] 更新商品價格選擇器`
|
||||
|
||||
### 第 37 條:開發環境
|
||||
- Python 3.8+
|
||||
- 使用虛擬環境 (venv)
|
||||
- IDE 建議:VSCode, PyCharm
|
||||
- 測試環境與生產環境分離
|
||||
- 爬蟲測試使用獨立環境
|
||||
|
||||
---
|
||||
|
||||
## 📊 第九章:監控與維護
|
||||
|
||||
### 第 38 條:系統監控
|
||||
- 定期檢查安全日誌
|
||||
- 監控登入失敗次數
|
||||
- 追蹤異常 API 請求
|
||||
- 定期執行安全測試
|
||||
|
||||
### 第 39 條:爬蟲監控
|
||||
- 監控爬蟲執行成功率
|
||||
- 追蹤選擇器失效次數
|
||||
- 檢查資料品質(異常值、缺失值)
|
||||
- 監控爬取耗時變化
|
||||
- 定期檢查目標網站結構
|
||||
|
||||
### 第 40 條:效能監控
|
||||
- 資料庫查詢效能
|
||||
- API 回應時間
|
||||
- 記憶體使用量
|
||||
- 磁碟空間
|
||||
- 爬蟲執行效率
|
||||
|
||||
---
|
||||
|
||||
## 📋 第十章:憲法修訂
|
||||
|
||||
### 第 41 條:修訂流程
|
||||
- 本憲法可隨專案需求修訂
|
||||
- 修訂需說明原因和影響範圍
|
||||
- 更新版本號和修訂日期
|
||||
- 記錄在文檔歷史中
|
||||
|
||||
### 第 42 條:解釋權
|
||||
- 本憲法條款如有疑義,以最新版本為準
|
||||
- 技術決策以穩定性和安全性優先
|
||||
- 爬蟲修改以不影響現有功能為原則
|
||||
- 使用者體驗和效能次之
|
||||
|
||||
---
|
||||
|
||||
## 📚 附錄:快速檢查清單
|
||||
|
||||
### ✅ 新功能開發檢查
|
||||
- [ ] 程式碼和註解使用繁體中文
|
||||
- [ ] 無硬編碼敏感資訊
|
||||
- [ ] 所有輸入經過驗證
|
||||
- [ ] POST 請求包含 CSRF token
|
||||
- [ ] 路徑操作使用 `safe_join()`
|
||||
- [ ] 檔案上傳經過驗證
|
||||
- [ ] 錯誤處理完整
|
||||
- [ ] 日誌記錄完整
|
||||
- [ ] 通過安全測試
|
||||
- [ ] 更新相關文檔
|
||||
|
||||
### ⚠️ 重大變更前備份檢查
|
||||
- [ ] 評估變更影響範圍(資料庫/配置/核心程式碼/部署)
|
||||
- [ ] 確認符合第 28 條適用範圍
|
||||
- [ ] 完整備份資料庫檔案
|
||||
- [ ] 備份所有程式碼(Git commit + 檔案副本)
|
||||
- [ ] 備份配置檔案
|
||||
- [ ] 驗證備份檔案完整性(檔案大小、可讀取)
|
||||
- [ ] 記錄備份時間和位置
|
||||
- [ ] 準備復原計畫文件
|
||||
- [ ] 測試復原流程可行性
|
||||
- [ ] 確保有回滾機制
|
||||
|
||||
### 🔒 安全審查檢查
|
||||
- [ ] SQL 查詢使用參數化或白名單
|
||||
- [ ] 無明文密碼
|
||||
- [ ] Session 配置正確
|
||||
- [ ] CSRF 防護啟用
|
||||
- [ ] 路徑遍歷防護
|
||||
- [ ] 檔案上傳限制
|
||||
- [ ] 登入失敗鎖定
|
||||
- [ ] 敏感操作有日誌
|
||||
|
||||
### 🕷️ 爬蟲修改檢查
|
||||
- [ ] 備份現有可運作版本
|
||||
- [ ] 記錄修改原因和網站變更資訊
|
||||
- [ ] 保留舊選擇器作為註解
|
||||
- [ ] 測試新選擇器正確性
|
||||
- [ ] 執行單一商品爬取測試
|
||||
- [ ] 執行列表頁面爬取測試
|
||||
- [ ] 驗證資料完整性和格式
|
||||
- [ ] 檢查錯誤處理機制
|
||||
- [ ] 更新爬蟲文檔
|
||||
- [ ] 記錄在維護日誌中
|
||||
- [ ] 建立 Git commit
|
||||
- [ ] 監控 24 小時確保穩定
|
||||
|
||||
---
|
||||
|
||||
## 🎯 結語
|
||||
|
||||
本憲法旨在確保 MOMO 監控系統的安全性、穩定性、可維護性和一致性。所有參與者應:
|
||||
|
||||
1. **遵守規範**:嚴格遵守本憲法所有條款
|
||||
2. **持續改進**:隨著專案發展適時修訂
|
||||
3. **穩定優先**:爬蟲修改以不影響現有功能為原則
|
||||
4. **安全第一**:任何決策以安全為最高優先
|
||||
5. **謹慎測試**:修改後必須完整測試並監控
|
||||
6. **文檔完整**:保持文檔與程式碼同步更新
|
||||
|
||||
**核心原則:**
|
||||
- **安全不是功能,而是基礎**
|
||||
- **爬蟲是核心業務,修改需格外謹慎**
|
||||
- **測試是品質的保證,不可省略**
|
||||
|
||||
---
|
||||
|
||||
**版本歷史:**
|
||||
- v1.3 (2026-01-14): 新增第六章第 28 條「變更前強制備份原則」⚠️,明確規定所有涉及嚴重影響的變更操作前必須完整備份,定義適用範圍、備份要求、驗證流程、復原計畫及違規處理機制
|
||||
- v1.2 (2026-01-13): 擴充第六章第 27 條「環境管理規範」,明確定義開發/測試/正式三層環境架構、環境同步原則、標準部署流程,並新增 `DEPLOYMENT_WORKFLOW.md` 完整部署文檔
|
||||
- v1.1 (2026-01-12): 新增第四章「數據爬取規範」(第 13-22 條),定義爬蟲程式碼穩定性、選擇器維護、錯誤處理、測試要求等 10 項規範
|
||||
- v1.0 (2026-01-12): 初版發布,定義核心規範
|
||||
19
migrations/012_backup_log.sql
Normal file
19
migrations/012_backup_log.sql
Normal file
@@ -0,0 +1,19 @@
|
||||
-- Migration 012: backup_log 備份記錄表
|
||||
-- 用於 AI Agent 監控資料庫備份執行狀況
|
||||
|
||||
CREATE TABLE IF NOT EXISTS backup_log (
|
||||
id SERIAL PRIMARY KEY,
|
||||
backup_type VARCHAR(20) DEFAULT 'full', -- full / incremental
|
||||
filename VARCHAR(255) NOT NULL,
|
||||
file_size_bytes BIGINT DEFAULT 0,
|
||||
duration_seconds FLOAT DEFAULT 0,
|
||||
status VARCHAR(20) DEFAULT 'pending', -- pending / success / failed
|
||||
error_message TEXT,
|
||||
host VARCHAR(100),
|
||||
storage_path VARCHAR(500),
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
completed_at TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_backup_log_status ON backup_log(status);
|
||||
CREATE INDEX IF NOT EXISTS idx_backup_log_created ON backup_log(created_at DESC);
|
||||
@@ -46,6 +46,8 @@ def main():
|
||||
run_competitor_price_feeder_task,
|
||||
run_icaim_analysis_task,
|
||||
run_weekly_strategy_task,
|
||||
run_db_backup_task,
|
||||
run_backup_monitor_task,
|
||||
)
|
||||
logger.info("✅ 排程任務模組載入成功")
|
||||
except ImportError as e:
|
||||
@@ -83,6 +85,12 @@ def main():
|
||||
schedule.every().monday.at("07:00").do(run_weekly_strategy_task)
|
||||
logger.info("📅 已設定:每週一 07:00 執行 Gemini 策略師週報任務")
|
||||
|
||||
schedule.every().day.at("02:00").do(run_db_backup_task)
|
||||
logger.info("📅 已設定:每日 02:00 執行 PostgreSQL 資料庫備份")
|
||||
|
||||
schedule.every(6).hours.do(run_backup_monitor_task)
|
||||
logger.info("📅 已設定:每 6 小時執行備份健康監控(AI Agent 跟進)")
|
||||
|
||||
logger.info("=" * 60)
|
||||
logger.info("✅ 排程器已啟動,等待任務執行...")
|
||||
logger.info("=" * 60)
|
||||
|
||||
153
scheduler.py
153
scheduler.py
@@ -1652,6 +1652,159 @@ def run_weekly_strategy_task():
|
||||
_save_stats('weekly_strategy', {"status": "Failed", "error": str(e)})
|
||||
|
||||
|
||||
def run_db_backup_task():
|
||||
"""
|
||||
每日凌晨 02:00 執行 pg_dump 備份 momo_analytics,
|
||||
並清理超過 7 天的舊備份。完成後透過 Telegram 通知統帥。
|
||||
失敗同樣發出告警,並沉澱到 ai_insights(供 RAG 查詢)。
|
||||
"""
|
||||
logging.info("[Scheduler] [Backup] 🚀 啟動資料庫備份任務...")
|
||||
try:
|
||||
from services.db_backup_service import run_backup, cleanup_old_backups
|
||||
from services.notification_manager import NotificationManager
|
||||
|
||||
result = run_backup()
|
||||
deleted_count = cleanup_old_backups()
|
||||
now_str = datetime.now(TAIPEI_TZ).strftime('%Y-%m-%d %H:%M')
|
||||
|
||||
notifier = NotificationManager()
|
||||
|
||||
if result["success"]:
|
||||
size_kb = result["file_size"] // 1024
|
||||
msg = (
|
||||
f"💾 資料庫備份完成 ({now_str})\n"
|
||||
f"{'='*30}\n"
|
||||
f"✅ 狀態:成功\n"
|
||||
f"📁 檔案:{result['filename']}\n"
|
||||
f"📦 大小:{size_kb} KB\n"
|
||||
f"⏱ 耗時:{result['duration']:.1f} 秒\n"
|
||||
f"🗑 清理舊備份:{deleted_count} 個"
|
||||
)
|
||||
logging.info(f"[Scheduler] [Backup] ✅ 備份成功 | {result['filename']} ({size_kb}KB)")
|
||||
_save_stats('db_backup', {"status": "Success", "filename": result["filename"], "size_kb": size_kb})
|
||||
|
||||
# 沉澱到 ai_insights(RAG 可查詢備份歷史)
|
||||
try:
|
||||
from services.openclaw_learning_service import store_insight
|
||||
store_insight(
|
||||
insight_type='backup_status',
|
||||
content=f"資料庫備份成功:{result['filename']},大小 {size_kb}KB,耗時 {result['duration']:.1f}s",
|
||||
period=datetime.now(TAIPEI_TZ).strftime('%Y-%m-%d'),
|
||||
metadata={"status": "success", "size_kb": size_kb, "deleted_old": deleted_count},
|
||||
ai_model="scheduler",
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
else:
|
||||
msg = (
|
||||
f"🚨 資料庫備份失敗 ({now_str})\n"
|
||||
f"{'='*30}\n"
|
||||
f"❌ 狀態:失敗\n"
|
||||
f"🔍 原因:{result.get('error', '未知錯誤')}\n"
|
||||
f"⚠️ 請立即檢查 momo-db 容器狀態!"
|
||||
)
|
||||
logging.error(f"[Scheduler] [Backup] ❌ 備份失敗: {result.get('error')}")
|
||||
_save_stats('db_backup', {"status": "Failed", "error": result.get("error")})
|
||||
|
||||
try:
|
||||
from services.openclaw_learning_service import store_insight
|
||||
store_insight(
|
||||
insight_type='backup_status',
|
||||
content=f"資料庫備份失敗:{result.get('error', '未知')}",
|
||||
period=datetime.now(TAIPEI_TZ).strftime('%Y-%m-%d'),
|
||||
metadata={"status": "failed", "error": result.get("error")},
|
||||
ai_model="scheduler",
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
notifier._send_telegram_messages([msg])
|
||||
|
||||
except Exception as e:
|
||||
logging.error(f"[Scheduler] [Backup] 🚨 任務異常 | Error: {e}")
|
||||
_save_stats('db_backup', {"status": "Error", "error": str(e)})
|
||||
try:
|
||||
from services.notification_manager import NotificationManager
|
||||
NotificationManager()._send_telegram_messages([
|
||||
f"🚨 DB 備份排程異常\n錯誤:{e}"
|
||||
])
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def run_backup_monitor_task():
|
||||
"""
|
||||
每 6 小時檢查最近一次備份是否在 25 小時內(允許一點偏差)。
|
||||
若發現備份過期或從未備份,立即發出 Telegram 告警,
|
||||
並透過 store_insight 讓 NemoTron/OpenClaw 可感知備份健康狀態。
|
||||
"""
|
||||
logging.info("[Scheduler] [BackupMonitor] 🔍 檢查備份健康狀態...")
|
||||
try:
|
||||
from services.db_backup_service import get_latest_backup_info
|
||||
from services.notification_manager import NotificationManager
|
||||
|
||||
info = get_latest_backup_info()
|
||||
now = datetime.now(TAIPEI_TZ)
|
||||
alert_needed = False
|
||||
alert_reason = ""
|
||||
|
||||
if info["status"] == "no_backup" or info["filename"] is None:
|
||||
alert_needed = True
|
||||
alert_reason = "從未執行過備份,backup_log 與備份目錄均為空"
|
||||
elif info["status"] == "failed":
|
||||
alert_needed = True
|
||||
alert_reason = f"最近一次備份失敗:{info.get('error', '未知')}"
|
||||
else:
|
||||
created_at = info["created_at"]
|
||||
if created_at is not None:
|
||||
# 統一為 naive datetime 比較
|
||||
if hasattr(created_at, 'tzinfo') and created_at.tzinfo is not None:
|
||||
created_at = created_at.replace(tzinfo=None)
|
||||
now_naive = now.replace(tzinfo=None)
|
||||
hours_ago = (now_naive - created_at).total_seconds() / 3600
|
||||
if hours_ago > 25:
|
||||
alert_needed = True
|
||||
alert_reason = f"最近備份距今 {hours_ago:.1f} 小時(超過 25h 閾值),可能備份任務未執行"
|
||||
|
||||
if alert_needed:
|
||||
now_str = now.strftime('%Y-%m-%d %H:%M')
|
||||
msg = (
|
||||
f"⚠️ 資料庫備份異常告警 ({now_str})\n"
|
||||
f"{'='*30}\n"
|
||||
f"🔴 原因:{alert_reason}\n"
|
||||
f"📋 最新備份:{info.get('filename', '無')}\n"
|
||||
f"🕐 備份時間:{info.get('created_at', '無')}\n"
|
||||
f"💡 請確認 momo-scheduler 備份排程是否正常執行!"
|
||||
)
|
||||
logging.warning(f"[Scheduler] [BackupMonitor] ⚠️ 備份告警: {alert_reason}")
|
||||
NotificationManager()._send_telegram_messages([msg])
|
||||
|
||||
try:
|
||||
from services.openclaw_learning_service import store_insight
|
||||
store_insight(
|
||||
insight_type='backup_status',
|
||||
content=f"備份監控告警:{alert_reason}",
|
||||
period=now.strftime('%Y-%m-%d'),
|
||||
metadata={"alert": True, "reason": alert_reason, "latest_file": info.get("filename")},
|
||||
ai_model="scheduler",
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
else:
|
||||
created_at = info.get("created_at")
|
||||
logging.info(f"[Scheduler] [BackupMonitor] ✅ 備份狀態正常 | 最新: {info.get('filename')} @ {created_at}")
|
||||
|
||||
_save_stats('backup_monitor', {
|
||||
"status": "Alert" if alert_needed else "OK",
|
||||
"latest_file": info.get("filename"),
|
||||
"alert_reason": alert_reason if alert_needed else None,
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
logging.error(f"[Scheduler] [BackupMonitor] 🚨 監控任務異常 | Error: {e}")
|
||||
_save_stats('backup_monitor', {"status": "Error", "error": str(e)})
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# 此檔案現在由 app.py 導入並由其主執行緒管理排程。
|
||||
# 若需獨立測試,可在此處臨時加入調用程式碼。
|
||||
|
||||
72
scripts/pg_backup.sh
Normal file
72
scripts/pg_backup.sh
Normal file
@@ -0,0 +1,72 @@
|
||||
#!/bin/bash
|
||||
# EwoooC PostgreSQL 備份腳本 (Host-Level)
|
||||
# 執行環境:192.168.0.188 host,每日 02:00 cron 觸發
|
||||
# pg_dump 在 momo-db container 內執行
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
BACKUP_DIR="/home/ollama/momo_backups"
|
||||
DB_CONTAINER="momo-db"
|
||||
DB_USER="momo"
|
||||
DB_NAME="momo_analytics"
|
||||
DB_PASS="wooo_pg_2026"
|
||||
KEEP_DAYS=7
|
||||
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
|
||||
FILENAME="momo_analytics_${TIMESTAMP}.sql.gz"
|
||||
FILEPATH="${BACKUP_DIR}/${FILENAME}"
|
||||
LOG_FILE="${BACKUP_DIR}/backup.log"
|
||||
|
||||
TELEGRAM_TOKEN="8075645931:AAH-EGKMo8ZC4QJs-Nc1_0s92xHrGdQvdpg"
|
||||
TELEGRAM_CHAT="5619078117"
|
||||
|
||||
log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" | tee -a "$LOG_FILE"; }
|
||||
send_tg() {
|
||||
curl -s -X POST "https://api.telegram.org/bot${TELEGRAM_TOKEN}/sendMessage" \
|
||||
-d "chat_id=${TELEGRAM_CHAT}&text=$1&parse_mode=HTML" > /dev/null 2>&1 || true
|
||||
}
|
||||
|
||||
mkdir -p "$BACKUP_DIR"
|
||||
log "===== EwoooC DB Backup 開始 ====="
|
||||
|
||||
START=$(date +%s)
|
||||
|
||||
# 執行 pg_dump(在 momo-db container 內,透過 docker exec)
|
||||
if PGPASSWORD="$DB_PASS" docker exec -e PGPASSWORD="$DB_PASS" \
|
||||
"$DB_CONTAINER" pg_dump -U "$DB_USER" -d "$DB_NAME" --no-password | \
|
||||
gzip > "$FILEPATH"; then
|
||||
|
||||
END=$(date +%s)
|
||||
DURATION=$((END - START))
|
||||
SIZE=$(du -h "$FILEPATH" | cut -f1)
|
||||
SIZE_BYTES=$(stat -c%s "$FILEPATH" 2>/dev/null || stat -f%z "$FILEPATH" 2>/dev/null || echo 0)
|
||||
|
||||
log "✅ 備份成功: $FILENAME ($SIZE, ${DURATION}s)"
|
||||
|
||||
# 寫入 backup_log(PostgreSQL)
|
||||
docker exec -e PGPASSWORD="$DB_PASS" "$DB_CONTAINER" psql -U "$DB_USER" -d "$DB_NAME" -c \
|
||||
"INSERT INTO backup_log (filename, file_size_bytes, duration_seconds, status, host, storage_path, completed_at)
|
||||
VALUES ('$FILENAME', $SIZE_BYTES, $DURATION, 'success', '$(hostname)', '$FILEPATH', CURRENT_TIMESTAMP);" \
|
||||
> /dev/null 2>&1 || log "⚠️ backup_log 寫入失敗(不影響備份本體)"
|
||||
|
||||
# 清理舊備份
|
||||
DELETED=$(find "$BACKUP_DIR" -name "momo_analytics_*.sql.gz" -mtime +${KEEP_DAYS} -print -delete | wc -l)
|
||||
log "🗑 清理舊備份:${DELETED} 個"
|
||||
|
||||
MSG="💾 EwoooC DB 備份完成%0A✅ 狀態:成功%0A📁 ${FILENAME}%0A📦 大小:${SIZE}%0A⏱ 耗時:${DURATION}秒%0A🗑 清理:${DELETED} 個舊備份"
|
||||
send_tg "$MSG"
|
||||
else
|
||||
END=$(date +%s)
|
||||
DURATION=$((END - START))
|
||||
log "❌ 備份失敗!"
|
||||
|
||||
docker exec -e PGPASSWORD="$DB_PASS" "$DB_CONTAINER" psql -U "$DB_USER" -d "$DB_NAME" -c \
|
||||
"INSERT INTO backup_log (filename, file_size_bytes, duration_seconds, status, host, storage_path, error_message, completed_at)
|
||||
VALUES ('$FILENAME', 0, $DURATION, 'failed', '$(hostname)', '$BACKUP_DIR', 'pg_dump 執行失敗', CURRENT_TIMESTAMP);" \
|
||||
> /dev/null 2>&1 || true
|
||||
|
||||
MSG="🚨 EwoooC DB 備份失敗%0A❌ 時間:$(date '+%Y-%m-%d %H:%M')%0A⚠️ 請立即檢查 momo-db 容器!"
|
||||
send_tg "$MSG"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
log "===== Backup 完成 ====="
|
||||
187
services/db_backup_service.py
Normal file
187
services/db_backup_service.py
Normal file
@@ -0,0 +1,187 @@
|
||||
"""
|
||||
DB Backup Service — EwoooC V10.3
|
||||
負責執行 pg_dump 備份、保留策略、以及備份狀態寫入 backup_log
|
||||
"""
|
||||
import os
|
||||
import subprocess
|
||||
import logging
|
||||
import glob
|
||||
from datetime import datetime, timedelta, timezone
|
||||
|
||||
TAIPEI_TZ = timezone(timedelta(hours=8))
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# 備份目錄:container 內掛載點
|
||||
BACKUP_DIR = os.environ.get("BACKUP_DIR", "/app/data/db_backups")
|
||||
# pg_dump 目標:在 momo-db container 內執行(docker exec)
|
||||
DB_CONTAINER = os.environ.get("DB_CONTAINER", "momo-db")
|
||||
DB_USER = os.environ.get("POSTGRES_USER", "momo")
|
||||
DB_NAME = os.environ.get("POSTGRES_DB", "momo_analytics")
|
||||
# 保留天數
|
||||
RETENTION_DAYS = int(os.environ.get("BACKUP_RETENTION_DAYS", "7"))
|
||||
|
||||
|
||||
def _ensure_backup_dir():
|
||||
os.makedirs(BACKUP_DIR, exist_ok=True)
|
||||
|
||||
|
||||
def _log_backup(filename, file_size, duration, status, error=None, storage_path=None):
|
||||
"""寫入 backup_log 表,失敗不阻斷主流程"""
|
||||
try:
|
||||
from database.manager import DatabaseManager
|
||||
db = DatabaseManager()
|
||||
with db.get_session() as session:
|
||||
from sqlalchemy import text
|
||||
session.execute(text("""
|
||||
INSERT INTO backup_log
|
||||
(filename, file_size_bytes, duration_seconds, status, error_message,
|
||||
host, storage_path, completed_at)
|
||||
VALUES
|
||||
(:filename, :size, :dur, :status, :error,
|
||||
:host, :path, CURRENT_TIMESTAMP)
|
||||
"""), {
|
||||
"filename": filename,
|
||||
"size": file_size,
|
||||
"dur": duration,
|
||||
"status": status,
|
||||
"error": error,
|
||||
"host": os.uname().nodename if hasattr(os, 'uname') else "unknown",
|
||||
"path": storage_path or BACKUP_DIR,
|
||||
})
|
||||
session.commit()
|
||||
except Exception as e:
|
||||
logger.warning(f"[Backup] backup_log 寫入失敗(不影響備份本體): {e}")
|
||||
|
||||
|
||||
def run_backup() -> dict:
|
||||
"""
|
||||
執行 pg_dump 備份。
|
||||
因 scheduler 在 momo-scheduler container 內,pg_dump 直連 momo-db service。
|
||||
回傳 dict: {success, filename, file_size, duration, error}
|
||||
"""
|
||||
_ensure_backup_dir()
|
||||
now = datetime.now(TAIPEI_TZ)
|
||||
filename = f"momo_analytics_{now.strftime('%Y%m%d_%H%M%S')}.sql.gz"
|
||||
filepath = os.path.join(BACKUP_DIR, filename)
|
||||
start = datetime.now()
|
||||
|
||||
db_host = os.environ.get("POSTGRES_HOST", "momo-db")
|
||||
db_port = os.environ.get("POSTGRES_PORT", "5432")
|
||||
|
||||
# 若 pg_dump 不存在則嘗試安裝(容器重建後需重裝;Dockerfile 已加入 postgresql-client)
|
||||
if not os.path.exists("/usr/bin/pg_dump"):
|
||||
logger.info("[Backup] pg_dump 不存在,嘗試安裝 postgresql-client...")
|
||||
subprocess.run(
|
||||
["apt-get", "install", "-y", "-qq", "postgresql-client"],
|
||||
capture_output=True
|
||||
)
|
||||
|
||||
cmd = [
|
||||
"sh", "-c",
|
||||
f"PGPASSWORD={os.environ.get('POSTGRES_PASSWORD', 'wooo_pg_2026')} "
|
||||
f"pg_dump -h {db_host} -p {db_port} -U {DB_USER} -d {DB_NAME} "
|
||||
f"--no-password -Fp | gzip > {filepath}"
|
||||
]
|
||||
|
||||
logger.info(f"[Backup] 開始備份 → {filepath}")
|
||||
result = {"success": False, "filename": filename, "file_size": 0, "duration": 0, "error": None}
|
||||
|
||||
try:
|
||||
proc = subprocess.run(cmd, capture_output=True, text=True, timeout=300)
|
||||
duration = (datetime.now() - start).total_seconds()
|
||||
|
||||
if proc.returncode != 0:
|
||||
error_msg = proc.stderr.strip() or "pg_dump 非零退出碼"
|
||||
logger.error(f"[Backup] 備份失敗: {error_msg}")
|
||||
result["error"] = error_msg
|
||||
result["duration"] = duration
|
||||
_log_backup(filename, 0, duration, "failed", error=error_msg)
|
||||
else:
|
||||
file_size = os.path.getsize(filepath) if os.path.exists(filepath) else 0
|
||||
logger.info(f"[Backup] 備份成功 | 大小={file_size//1024}KB | 耗時={duration:.1f}s")
|
||||
result.update({"success": True, "file_size": file_size, "duration": duration})
|
||||
_log_backup(filename, file_size, duration, "success", storage_path=filepath)
|
||||
|
||||
except subprocess.TimeoutExpired:
|
||||
duration = (datetime.now() - start).total_seconds()
|
||||
error_msg = "pg_dump 超時(300s)"
|
||||
logger.error(f"[Backup] {error_msg}")
|
||||
result["error"] = error_msg
|
||||
result["duration"] = duration
|
||||
_log_backup(filename, 0, duration, "failed", error=error_msg)
|
||||
except Exception as e:
|
||||
duration = (datetime.now() - start).total_seconds()
|
||||
error_msg = str(e)
|
||||
logger.error(f"[Backup] 備份異常: {e}")
|
||||
result["error"] = error_msg
|
||||
result["duration"] = duration
|
||||
_log_backup(filename, 0, duration, "failed", error=error_msg)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def cleanup_old_backups() -> int:
|
||||
"""刪除超過保留期限的備份檔,回傳刪除數量"""
|
||||
_ensure_backup_dir()
|
||||
cutoff = datetime.now() - timedelta(days=RETENTION_DAYS)
|
||||
deleted = 0
|
||||
for f in glob.glob(os.path.join(BACKUP_DIR, "momo_analytics_*.sql.gz")):
|
||||
try:
|
||||
mtime = datetime.fromtimestamp(os.path.getmtime(f))
|
||||
if mtime < cutoff:
|
||||
os.remove(f)
|
||||
deleted += 1
|
||||
logger.info(f"[Backup] 已刪除舊備份: {os.path.basename(f)}")
|
||||
except Exception as e:
|
||||
logger.warning(f"[Backup] 刪除舊備份失敗 {f}: {e}")
|
||||
return deleted
|
||||
|
||||
|
||||
def get_latest_backup_info() -> dict:
|
||||
"""
|
||||
回傳最新備份的資訊(供監控用)。
|
||||
優先從 backup_log 讀取,fallback 掃描檔案系統。
|
||||
"""
|
||||
try:
|
||||
from database.manager import DatabaseManager
|
||||
db = DatabaseManager()
|
||||
with db.get_session() as session:
|
||||
from sqlalchemy import text
|
||||
row = session.execute(text("""
|
||||
SELECT filename, file_size_bytes, duration_seconds, status, created_at, error_message
|
||||
FROM backup_log
|
||||
ORDER BY created_at DESC
|
||||
LIMIT 1
|
||||
""")).fetchone()
|
||||
if row:
|
||||
return {
|
||||
"filename": row[0],
|
||||
"file_size": row[1],
|
||||
"duration": row[2],
|
||||
"status": row[3],
|
||||
"created_at": row[4],
|
||||
"error": row[5],
|
||||
"source": "db",
|
||||
}
|
||||
except Exception as e:
|
||||
logger.warning(f"[Backup] 無法從 DB 讀取最新備份資訊: {e}")
|
||||
|
||||
# fallback: 掃描檔案
|
||||
_ensure_backup_dir()
|
||||
files = sorted(
|
||||
glob.glob(os.path.join(BACKUP_DIR, "momo_analytics_*.sql.gz")),
|
||||
key=os.path.getmtime, reverse=True
|
||||
)
|
||||
if files:
|
||||
f = files[0]
|
||||
mtime = datetime.fromtimestamp(os.path.getmtime(f))
|
||||
return {
|
||||
"filename": os.path.basename(f),
|
||||
"file_size": os.path.getsize(f),
|
||||
"duration": None,
|
||||
"status": "success",
|
||||
"created_at": mtime,
|
||||
"error": None,
|
||||
"source": "filesystem",
|
||||
}
|
||||
return {"filename": None, "status": "no_backup", "created_at": None, "source": "none"}
|
||||
@@ -505,29 +505,36 @@ class OllamaService:
|
||||
|
||||
return self.generate(prompt, system_prompt=system_prompt, temperature=0.5, timeout=120)
|
||||
|
||||
def generate_embedding(self, text: str, model: str = "bge-m3:latest") -> List[float]:
|
||||
def generate_embedding(self, text: str, model: str = "bge-m3:latest",
|
||||
host: str = None) -> List[float]:
|
||||
"""
|
||||
[ADR-007, Step 3] 呼叫 Ollama API 將文字轉換為向量 Embedding
|
||||
|
||||
2026-04-19 更新(ADR-003 對齊):
|
||||
embedding 預設走 Hermes 主機 `EMBEDDING_HOST`(env: EMBEDDING_HOST
|
||||
→ fallback http://192.168.0.111:11434,內網免認證),
|
||||
避免 self.host 若指向公開 ollama.wooo.work 時回 401。
|
||||
可透過 host 參數 override。
|
||||
"""
|
||||
import os
|
||||
target_host = host or os.getenv("EMBEDDING_HOST", "http://192.168.0.111:11434")
|
||||
try:
|
||||
# V-Opt: 發送 embedding 請求
|
||||
payload = {
|
||||
"model": model,
|
||||
"prompt": text
|
||||
}
|
||||
payload = {"model": model, "prompt": text}
|
||||
response = requests.post(
|
||||
f"{self.host}/api/embeddings",
|
||||
f"{target_host}/api/embeddings",
|
||||
json=payload,
|
||||
timeout=60
|
||||
timeout=60,
|
||||
)
|
||||
if response.status_code == 200:
|
||||
data = response.json()
|
||||
return data.get("embedding", [])
|
||||
else:
|
||||
logger.error(f"Ollama Embed Error HTTP {response.status_code}: {response.text}")
|
||||
logger.error(
|
||||
f"Ollama Embed Error HTTP {response.status_code} @ {target_host}: {response.text[:200]}"
|
||||
)
|
||||
return []
|
||||
except Exception as e:
|
||||
logger.error(f"Ollama Embed Exception: {e}")
|
||||
logger.error(f"Ollama Embed Exception @ {target_host}: {e}")
|
||||
return []
|
||||
|
||||
# 建立全域服務實例
|
||||
|
||||
Reference in New Issue
Block a user