diff --git a/CLAUDE.md b/CLAUDE.md index 5548dbf..ec160af 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -69,6 +69,7 @@ ssh wooo@192.168.0.110 "ssh ollama@192.168.0.188 \"\ * **ADR-014: Autonomous Code Repair**(結合 Elephant Alpha 進行系統錯誤日誌Traceback掃描,自動觸發 110 主機上 Aider 自動化執行程式碼修復,並依賴 Git + Gitea CI/CD 回滾防線進行安全發布) * **ADR-015: Telegram Bot Menu 復活**(OpenClawAwoooI_Bot 三專案共用、cmd:/menu:/await: callback prefix) * **ADR-016: daily_sales cache fingerprint**(gunicorn 多 worker cache 失效改 DB MAX/COUNT 指紋;棄用 N-POST 9.4% 命中 hack;3971fd4 已落地) | +* **ADR-017: Phase 3f 模組化收尾**(DB metadata、路由雙註冊、cache、scheduler、模板與死碼清理收斂路線圖) | | **PPT 簡報系統 V2(ADR-014)** | [docs/adr/ADR-014-ppt-report-system-v2.md](docs/adr/ADR-014-ppt-report-system-v2.md) | ## PPT 簡報系統(9 種,V2) diff --git a/app.py b/app.py index 0dcc7f0..2820bea 100644 --- a/app.py +++ b/app.py @@ -56,7 +56,7 @@ try: try: from scheduler import run_momo_task, run_edm_task, run_festival_task, run_auto_import_task, run_whitepage_check, run_competitor_price_feeder_task from database.manager import DatabaseManager - from database.models import Product, PriceRecord, MonthlySummaryAnalysis + from database.models import Base, Product, PriceRecord, MonthlySummaryAnalysis from database.edm_models import PromoProduct except ImportError as e: print(f"❌ 專案內部檔案缺失: {e}\n請檢查 database/ 或 services/ 目錄下的 .py 檔案是否存在。") @@ -396,6 +396,28 @@ public_url = "服務啟動中..." # 🚩 時區設定:台北時間 (UTC+8) TAIPEI_TZ = timezone(timedelta(hours=8)) +EXPECTED_METADATA_TABLES = { + 'categories', 'products', 'price_records', 'monthly_summary_analysis', + 'users', 'login_history', 'permissions', 'user_permissions', + 'promo_products', 'trend_records', 'trend_keywords', 'trend_analysis', + 'web_search_cache', 'telegram_users', + 'ai_generation_history', 'ai_prompt_templates', 'ai_usage_tracking', 'ai_insights', + 'agent_context', 'action_plans', 'action_outcomes', 'agent_strategy_weights', + 'incidents', 'playbooks', 'heal_logs', + 'import_jobs', 'import_config', 'notification_templates', 'ppt_reports', + 'vendor_stockout', 'vendor_list', 'vendor_emails', 'email_send_log', + 'realtime_sales_monthly', +} + + +def verify_metadata_tables(): + missing = EXPECTED_METADATA_TABLES - set(Base.metadata.tables.keys()) + if missing: + raise SystemExit(f"Base.metadata 漏表: {sorted(missing)}") + + +verify_metadata_tables() + # ========================================== # 🔧 全域模板變數注入 (Context Processor) # ========================================== diff --git a/database/ai_models.py b/database/ai_models.py index 36364b1..ea3960d 100644 --- a/database/ai_models.py +++ b/database/ai_models.py @@ -1,14 +1,3 @@ -# database/ai_models.py -# ⚠️ 這四個 class 的原始定義已移至 autoheal_models.py(ADR-013 統一管理)。 -# 此檔僅作向後相容 re-export shim,不再重複定義 SQLAlchemy Table, -# 以避免 "Table already defined for this MetaData instance" 衝突。 -from .autoheal_models import ( # noqa: F401 - AgentContext, - ActionPlan, - ActionOutcome, - AgentStrategyWeights, -) - # AI history and template models from sqlalchemy import Column, Integer, String, DateTime, Text, Boolean, Float from database.models import Base @@ -163,6 +152,5 @@ class AIInsight(Base): __all__ = [ - "AgentContext", "ActionPlan", "ActionOutcome", "AgentStrategyWeights", "AIGenerationHistory", "AIPromptTemplate", "AIUsageTracking", "AIInsight", ] diff --git a/database/manager.py b/database/manager.py index 4d17e80..84ff9ef 100644 --- a/database/manager.py +++ b/database/manager.py @@ -1,5 +1,6 @@ import os import re +import threading from sqlalchemy import create_engine, desc, select, text, literal from sqlalchemy.orm import sessionmaker from datetime import datetime @@ -7,12 +8,22 @@ from .models import Base, Category, Product, PriceRecord, MonthlySummaryAnalysis from .user_models import User, LoginHistory # noqa: F401 - 必須在 trend_models 之前導入,解決 ForeignKey 依賴問題 from .edm_models import PromoProduct # V-Fix: 確保 EDM 模型被註冊,以便自動建表 from .trend_models import TrendRecord, TrendKeyword, TrendAnalysis, WebSearchCache, TelegramUser # noqa: F401 - 趨勢資料表 -from .ai_models import AgentContext, ActionPlan, ActionOutcome, AgentStrategyWeights # noqa: F401 - AI agent 模型 -from .autoheal_models import Incident, Playbook, HealLog # noqa: F401 - ADR-013 AIOps 自動修復表 -from .import_models import ImportJob # noqa: F401 - 確保 import_jobs 表被 Base.metadata 管理 +from .permission_models import Permission, UserPermission # noqa: F401 - 確保權限表被 Base.metadata 管理 +from .ai_models import AIGenerationHistory, AIPromptTemplate, AIUsageTracking, AIInsight # noqa: F401 - AI history/template 表 +from .autoheal_models import ( # noqa: F401 - ADR-013 AIOps 自動修復表 + AgentContext, + ActionPlan, + ActionOutcome, + AgentStrategyWeights, + Incident, + Playbook, + HealLog, +) +from .import_models import ImportJob, ImportConfig # noqa: F401 - 確保 import_jobs/import_config 被 Base.metadata 管理 from .notification_models import NotificationTemplate # noqa: F401 - 確保 notification_templates 表被 Base.metadata 管理 from .ppt_reports import PPTReport # noqa: F401 - 確保 ppt_reports 表被 Base.metadata 管理 -from .vendor_models import VendorStockout # noqa: F401 - 確保 vendor_stockout 表被 Base.metadata 管理 +from .vendor_models import VendorStockout, VendorList, VendorEmail, EmailSendLog # noqa: F401 - 確保 vendor 表被 Base.metadata 管理 +from .realtime_sales_models import RealtimeSalesMonthly # noqa: F401 - 確保 realtime_sales_monthly 被 Base.metadata 管理 # 🚩 導入優化後的日誌管理模組 from utils.logger_manager import SystemLogger @@ -20,6 +31,33 @@ from utils.logger_manager import SystemLogger # 初始化資料庫模組專用 Logger sys_log = SystemLogger("Database").get_logger() +_metadata_init_lock = threading.Lock() +_metadata_initialized = False +_POSTGRES_METADATA_LOCK_ID = 170017 + + +def ensure_metadata_initialized(engine, use_postgres_lock=False): + """冪等初始化 SQLAlchemy metadata,避免一般流程重複碰 DDL。""" + global _metadata_initialized + if _metadata_initialized: + return + + with _metadata_init_lock: + if _metadata_initialized: + return + + if use_postgres_lock: + with engine.begin() as conn: + conn.execute(text("SELECT pg_advisory_lock(:lock_id)"), {"lock_id": _POSTGRES_METADATA_LOCK_ID}) + try: + Base.metadata.create_all(conn) + finally: + conn.execute(text("SELECT pg_advisory_unlock(:lock_id)"), {"lock_id": _POSTGRES_METADATA_LOCK_ID}) + else: + Base.metadata.create_all(engine) + + _metadata_initialized = True + def sanitize_timestamp(timestamp_str): """ 驗證並清理時間戳字串,防止 SQL Injection @@ -63,6 +101,7 @@ class DatabaseManager: 'options': '-c statement_timeout=60000' # SQL 超時 60 秒 } ) + ensure_metadata_initialized(self.engine, use_postgres_lock=True) self.Session = sessionmaker(bind=self.engine) sys_log.info(f"[Database] ✅ 使用 PostgreSQL 資料庫 (連線池已優化)") # ADR-013: 確保 AIOps 自動修復表存在並植入種子 PlayBook @@ -422,4 +461,4 @@ def get_session(): finally: session.close() """ - return get_db_manager().get_session() \ No newline at end of file + return get_db_manager().get_session() diff --git a/database/realtime_sales_models.py b/database/realtime_sales_models.py new file mode 100644 index 0000000..fe0b35a --- /dev/null +++ b/database/realtime_sales_models.py @@ -0,0 +1,41 @@ +from sqlalchemy import Column, Date, DateTime, Integer, Numeric, String, Text + +from database.models import Base + + +class RealtimeSalesMonthly(Base): + """ + 即時業績月報 ORM。 + + 這張表先由 PostgreSQL init.sql 建出,之後又被程式碼與匯入流程持續擴充。 + 這裡先把目前程式碼直接依賴的核心欄位納入 metadata,讓 create_all、 + metrics 與啟動自檢有一致的表定義。 + """ + + __tablename__ = "realtime_sales_monthly" + __table_args__ = {"extend_existing": True} + + id = Column(Integer, primary_key=True) + 日期 = Column(Date, index=True) + 訂單編號 = Column(String(50), index=True) + 商品ID = Column(String(100), index=True) + 商品編號 = Column(String(100), index=True) + 商品名稱 = Column(Text) + 數量 = Column(Integer) + 總業績 = Column(Numeric(15, 2)) + 總成本 = Column(Numeric(15, 2)) + 毛利 = Column(Numeric(15, 2)) + 退貨數量 = Column(Integer) + 商品單位售價 = Column(Numeric(15, 2)) + 廠商名稱 = Column(String(255), index=True) + 分類名稱 = Column(String(255), index=True) + 商品館 = Column(String(255), index=True) + 品牌名稱 = Column(String(255), index=True) + 時間 = Column(String(50)) + 付款方式 = Column(String(100)) + 折扣活動名稱 = Column(String(255)) + 折價券折扣金額 = Column(Numeric(15, 2)) + 折扣金額 = Column(Numeric(15, 2)) + 滿額再折扣金額 = Column(Numeric(15, 2)) + 分期手續費 = Column(Numeric(15, 2)) + created_at = Column(DateTime) diff --git a/docker/postgres/init/01-init.sql b/docker/postgres/init/01-init.sql index c272cac..2767cfc 100644 --- a/docker/postgres/init/01-init.sql +++ b/docker/postgres/init/01-init.sql @@ -14,22 +14,38 @@ CREATE TABLE IF NOT EXISTS realtime_sales_monthly ( id SERIAL PRIMARY KEY, 日期 DATE, 訂單編號 VARCHAR(50), + 商品ID VARCHAR(100), + 商品編號 VARCHAR(100), 商品名稱 TEXT, - 商品編號 VARCHAR(50), 數量 INTEGER, 總業績 DECIMAL(15, 2), 總成本 DECIMAL(15, 2), - 廠商名稱 VARCHAR(200), - 分類名稱 VARCHAR(200), - 品牌名稱 VARCHAR(200), + 毛利 DECIMAL(15, 2), + 退貨數量 INTEGER, + 商品單位售價 DECIMAL(15, 2), + 廠商名稱 VARCHAR(255), + 分類名稱 VARCHAR(255), + 商品館 VARCHAR(255), + 品牌名稱 VARCHAR(255), + 時間 VARCHAR(50), + 付款方式 VARCHAR(100), + 折扣活動名稱 VARCHAR(255), + 折價券折扣金額 DECIMAL(15, 2), + 折扣金額 DECIMAL(15, 2), + 滿額再折扣金額 DECIMAL(15, 2), + 分期手續費 DECIMAL(15, 2), created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); -- 建立索引以加速查詢 -CREATE INDEX idx_sales_date ON realtime_sales_monthly(日期); -CREATE INDEX idx_sales_vendor ON realtime_sales_monthly(廠商名稱); -CREATE INDEX idx_sales_category ON realtime_sales_monthly(分類名稱); -CREATE INDEX idx_sales_brand ON realtime_sales_monthly(品牌名稱); +CREATE INDEX IF NOT EXISTS idx_sales_date ON realtime_sales_monthly(日期); +CREATE INDEX IF NOT EXISTS idx_sales_order ON realtime_sales_monthly(訂單編號); +CREATE INDEX IF NOT EXISTS idx_sales_product_id ON realtime_sales_monthly(商品ID); +CREATE INDEX IF NOT EXISTS idx_sales_product_code ON realtime_sales_monthly(商品編號); +CREATE INDEX IF NOT EXISTS idx_sales_vendor ON realtime_sales_monthly(廠商名稱); +CREATE INDEX IF NOT EXISTS idx_sales_category ON realtime_sales_monthly(分類名稱); +CREATE INDEX IF NOT EXISTS idx_sales_hall ON realtime_sales_monthly(商品館); +CREATE INDEX IF NOT EXISTS idx_sales_brand ON realtime_sales_monthly(品牌名稱); -- EDM 資料表 CREATE TABLE IF NOT EXISTS edm_data ( diff --git a/docs/adr/ADR-017-modularization-cleanup-roadmap.md b/docs/adr/ADR-017-modularization-cleanup-roadmap.md new file mode 100644 index 0000000..3f469ce --- /dev/null +++ b/docs/adr/ADR-017-modularization-cleanup-roadmap.md @@ -0,0 +1,114 @@ +# ADR-017: 模組化收尾路線圖(Phase 3f) + +- **狀態**: Accepted +- **日期**: 2026-04-29 +- **觸發**: 12-Agent 全景盤點(debugger / refactor-specialist / critic / db-expert / Explore) +- **相關 ADR**: ADR-008(188 拓撲)、ADR-011(資源隔離)、ADR-016(cache fingerprint) +- **相關 Memory**: feedback_flask_blueprint_shadow、project_phase3e_refactor_progress、project_phase3f_cleanup_roadmap、feedback_db_metadata_import + +## 背景 + +Phase 3e(4/28-29)完成 app.py 7,386→6,590 行(-10.8%),但**僅完成「搬檔」,未完成「拆乾淨」**。12-Agent 盤點揭露 6 個面向的殘留問題,且發現新的 critical 風險(DB metadata import 漏洞、孤兒表)。 + +## 完成度真相(2026-04-29 盤點基線) + +| 維度 | 完成度 | 真相 | +|------|--------|------| +| 路由搬檔(41 條 → 28 BP)| 68% | 13 條 app.py 獨有未遷移 | +| 路由「拆乾淨」 | **0%** | app.py 41 條一條未刪,28 條與 BP 同 URL 雙寫,由 first-registered-wins 決定行為 | +| `USE_MODULAR_ROUTES` 開關 | **0%** | `register_blueprints()` 從未被 app.py 呼叫,整套設計死碼 | +| services/ 模組化 | ~95% | 唯一乾淨;3 個孤兒 service(0 引用)| +| 模板統一 | ~50% | 三目錄並存 + 1 空檔 + 3 死檔 + 2 TemplateNotFound 風險 | +| DB schema vs Model | ~60% | manager.py import 漏 3 模組;6 張表有 SQL 無 ORM;realtime_sales_monthly 孤兒 | + +## 決策 + +執行 **Phase 3f 五階段收尾**,總工期估 12-15 小時(不含驗證),每階段獨立 commit、每階段 critic 審查、每階段先 SSH 驗證 production。 + +### Phase 3f-0:DB metadata 救急(30 分鐘,最高優先) +1. `database/manager.py` 補完 import: + - `permission_models`(Permission, UserPermission) + - `vendor_models` 補 VendorList / VendorEmail / EmailSendLog + - `ai_models` 顯式 import 4 個 AI history/template class + - `autoheal_models` 顯式 import 7 個 AIOps class,移除 `ai_models.py` re-export shim +2. 處置 `realtime_sales_monthly` 孤兒表: + - 選項 A:建 `database/realtime_sales_models.py`(推薦,補 ORM 一致性) + - 選項 B:移除 `app.py:693` 的 import,metrics 不依賴此 model +3. PostgreSQL 與 SQLite 初始化都執行全域 `Base.metadata.create_all()`;PostgreSQL 路徑以 process-local guard + advisory lock 保護,避免一般流量重複碰 DDL +4. `app.py` 啟動加 metadata self-check,缺表直接 `SystemExit` +5. `docker/postgres/init/01-init.sql` 的 `realtime_sales_monthly` 欄位同步 ORM,避免 fresh volume 先由 init.sql 建出窄表後 create_all 無法補欄位 + +**驗收**:Base.metadata 含全 34 個 table;create_all 在新環境零漏。 + +### Phase 3f-1:路由雙註冊徹底解(4-6 小時,P9 切分) + +**策略**:保留 Blueprint,刪 app.py 對應 `@app.route`(28 條),13 條 app.py 獨有遷至 BP。實作順序要先處理 API shadow、次要頁面與 `/brand_assets`,首頁 `/` 最後動,因為多處 `url_for('index')` 仍依賴 app endpoint name。 + +子任務(按 BP 分組獨立 commit): + +| Sprint | 範圍 | 動作 | +|--------|------|------| +| 3f-1-a | dashboard_bp | 刪 app.py:722 `/`;確認 BP 版可用 | +| 3f-1-b | edm_bp | 刪 app.py:1029, 1280 兩條 | +| 3f-1-c | export_bp | 刪 app.py:1431-1905 共 9 條;補 `/api/export/excel/seasonality_detail` 進 BP | +| 3f-1-d | api_bp | 刪 app.py:2219-2451 共 6 條 trigger/run | +| 3f-1-e | import_bp | 刪 app.py:2691, 2980 兩條 | +| 3f-1-f | monthly_bp | 刪 app.py:3076, 3083 兩條 | +| 3f-1-g | sales_bp | 刪 app.py:3592-5809 共 7 條;**同時刪 routes/sales_routes.py 6 處 wrapper(174-348)**,把實作搬進 BP | +| 3f-1-h | system routes 補強 | 現有 `system_bp` 有 `/api/system` prefix;公開 URL(`/health` `/metrics` `/logs` `/settings` 等)需新建無 prefix 的 `system_public_bp` 或拆成 public/internal 兩個 BP,不能直接塞進現有 `system_bp`。`/abc_analysis/detail` 歸 `sales_bp`。 | +| 3f-1-i | 死碼清除 | 刪 `routes/__init__.py:32-177` 整套(register_blueprints / MODULAR_ENDPOINTS / is_endpoint_modular / cleanup_duplicate_routes)+ `config.py:244-254` USE_MODULAR_ROUTES | + +**驗收**: +- `app.url_map.iter_rules()` 任一 (rule, methods) 皆唯一 +- 啟動加 self-check:duplicate detect 直接 raise SystemExit +- 全 endpoint smoke test(critic 監督) + +### Phase 3f-2:Cache 統一(2-3 小時) +1. Dockerfile + docker-compose.yml gunicorn 加 `--preload`,只作為 COW 記憶體優化,不作為一致性保證 +2. 新建 `services/cache_manager.py` 套用 ADR-016 fingerprint 模式 +3. 移除三處重複 `_SALES_*_CACHE` 定義(app.py:82, routes/sales_routes.py:32, services/cache_service.py:16)→ 統一 import `services.cache_manager` +4. `clear_cache` endpoint 從「N-POST 廣播」改「DB fingerprint pull」(已在 daily_sales bp 完成,擴及 sales) + +### Phase 3f-3:穩定性補強(1-2 小時) +1. 先在 `services/event_router.py` 補同步安全 facade(如 `notify_failure()` / `dispatch_sync()`),因現有 `EventRouter` 只有 async `dispatch()`,scheduler 直接呼叫會掉告警 +2. scheduler.py 7 處裸 `except: pass`(line 243, 254, 582, 587, 653, 890, 1222)改為 `except Exception as e: logger.exception(...)`,P1/P2 經 EventRouter 同步 facade 強制告警 +3. docker-compose.yml 刪 7 條死路徑 mount(vendor_routes.py / vendor_stockout_*.html) +4. routes/vendor_routes.py:28-30 template_folder 改用絕對路徑,移除靠 Flask fallback 的脆弱依賴 + +### Phase 3f-4:模板統一(2-3 小時) +1. 補 2 個 TemplateNotFound:`trends.html`(trend_routes.py:33)、`login_history.html`(user_routes.py:40)→ 找回或停用對應 endpoint +2. 刪 `web/templates/sales_analysis.html`(0-byte 空檔) +3. 刪 3 個雙寫死檔:`web/templates/brand_assets.html`、`web/templates/growth_analysis.html`、根目錄 `logs.html` +4. 根目錄 11 個 *.html 全搬 `templates/`,確認 docker-compose 舊 mount 已清理後,再移除 `app.py:185-188` ChoiceLoader 的 BASE_DIR fallback +5. `web/templates/` 僅留 `vendor_stockout/` 子目錄(vendor_bp 自帶 template_folder) + +### Phase 3f-5:死碼清除(30 分鐘) +1. 確認並刪除 3 個孤兒 service: + - `services/elephant_alpha_decision_router.py` + - `services/telegram_ai_integration.py` + - `services/watcher_agent.py` +2. `.env.example` 補齊 15+ 個程式碼實際讀但 example 缺的變數(AIDER_*, ELEPHANT_ALPHA_*, HEAL_SSH_*, NVIDIA_API_KEY 等) + +## 風險與回滾 + +每階段獨立 commit,CI/CD 失敗自動回滾(依賴 ADR-014 防線)。Phase 3f-1 為高風險區,必須: +1. 先在本機 `python app.py` 啟動驗證 url_map 無重複 +2. critic 審 diff 過關 +3. 部署後 SSH 健康檢查 `/health` + 抽測 5 條核心 endpoint + +## 不做的事 + +- 不做模組化開關設計回填(USE_MODULAR_ROUTES 已死,直接刪) +- 不做 ORM 全面遷移(25 張無 SQL migration 軌跡的 table 留待 Phase 4) +- 不在 3f-0 修 `docker/postgres/init/01-init.sql` 與 ORM 的 `products` 中文欄位 schema drift;這是 Phase 4 migration 題 +- 不做 openclaw_bot_routes.py(5,543 行)拆解(留 Phase 4) +- 不撤換 NGROK Token(統帥 2026-04-29 明示先忽略) + +## 完成定義 + +- app.py < 5,000 行 +- url_map 零重複 +- create_all 新環境零漏表 +- 模板僅存 `templates/` + `web/templates/vendor_stockout/` +- gunicorn `--preload` 啟用、cache 跨 worker 一致 +- scheduler 7 處 except 全改、docker-compose mount 全清 diff --git a/docs/adr/README.md b/docs/adr/README.md index f2ba368..066d60d 100644 --- a/docs/adr/README.md +++ b/docs/adr/README.md @@ -37,6 +37,8 @@ | [013](ADR-013-aiops-autoheal.md) | AIOps 自動修復閉環架構(七步閉環 + SSH Jump Executor) | Accepted | 2026-04-19 | | [014](ADR-014-ppt-report-system-v2.md) | PPT 簡報系統 V2 — 原生圖表 + 9 種報告類型 | Accepted | 2026-04-20 | | [015](ADR-015-telegram-bot-menu-restoration.md) | Telegram Bot 完整菜單系統恢復 | Accepted | 2026-04-20 | +| [016](ADR-016-daily-sales-cache-fingerprint.md) | daily_sales cache fingerprint(gunicorn 多 worker 一致性) | Accepted | 2026-04-29 | +| [017](ADR-017-modularization-cleanup-roadmap.md) | 模組化收尾路線圖(Phase 3f) | Accepted | 2026-04-29 | ## 規範 diff --git a/docs/memory/feedback_db_metadata_import.md b/docs/memory/feedback_db_metadata_import.md new file mode 100644 index 0000000..0d429c1 --- /dev/null +++ b/docs/memory/feedback_db_metadata_import.md @@ -0,0 +1,29 @@ +# DB Metadata Import 鐵律 + +## 問題 + +`Base.metadata.create_all()` 只會建立已載入 metadata 的 model。若 model 檔沒有被 `database/manager.py` 或其他啟動必經路徑 import,新環境就會漏表,但既有環境可能因舊資料庫已存在而掩蓋問題。 + +## 鐵律 + +1. 新增 SQLAlchemy model 時,必須在 `database/manager.py` 顯式 import。 +2. 禁止依賴 re-export shim 或副作用 import 讓 table 偷偷進 metadata。 +3. 若 init SQL 已有表,仍要補 ORM 或在 ADR 中明確說明為外部管理表。 +4. `app.py` 啟動 self-check 的 expected table 清單要同步更新。 +5. 新環境驗收要跑: + +```bash +python3 - <<'PY' +from database.manager import Base +print(len(Base.metadata.tables)) +print(sorted(Base.metadata.tables)) +PY +``` + +## Phase 3f-0 決策 + +- `database/manager.py` 顯式載入 permission、AI history、autoheal、import、notification、PPT、vendor、realtime sales models。 +- `database/ai_models.py` 移除 autoheal re-export shim;需要 autoheal model 的程式必須直接 import `database.autoheal_models`。 +- `database/realtime_sales_models.py` 補 `realtime_sales_monthly` ORM,讓 metrics、匯入與啟動自檢有同一份 metadata 來源。 +- PostgreSQL `create_all()` 必須經 process-local guard 與 advisory lock,避免多 worker 或多次 `DatabaseManager()` 在一般流程重複做 DDL 檢查。 +- ORM 若對齊 init SQL 管理的表,必須同步 `docker/postgres/init/01-init.sql`,因 `create_all()` 不會 alter 已存在的窄表。 diff --git a/docs/memory/history_logs.md b/docs/memory/history_logs.md index 102d9a7..e41011d 100644 --- a/docs/memory/history_logs.md +++ b/docs/memory/history_logs.md @@ -1,6 +1,7 @@ # EwoooC 專案歷史紀錄 (History Logs) ## 📌 重大里程碑 +- **2026-04-29**: ADR-017 Phase 3f 模組化收尾立案,啟動 DB metadata、路由雙註冊、cache、scheduler、模板與死碼清理六線收斂。 - **2026-04-18**: 專案正式正名為 **EwoooC**,AI 治理架構 Phase 4 結案(V10.3)。 - **2026-02-13**: WOOO AIOps SaaS 核心模組完成。 - **2026-01-24**: 曾嘗試遷移至 K3s(後於 04-18 審計確認回歸 Docker Compose)。 @@ -10,6 +11,12 @@ ## 📅 詳細更新日誌 (考古存檔) +### 2026-04-29:ADR-017 Phase 3f 模組化收尾啟動 +- **DB metadata 救急**: `database/manager.py` 改為顯式載入 permission / AI / autoheal / import / vendor / realtime_sales ORM,PostgreSQL 初始化透過 process-local guard + advisory lock 執行 `Base.metadata.create_all()`,避免新環境漏表與一般流量重複碰 DDL。 +- **realtime_sales_monthly 補 ORM**: 新增 `database/realtime_sales_models.py`,並同步 `docker/postgres/init/01-init.sql` 欄位,避免 fresh volume 先建出窄表後造成匯入欄位靜默遺失。 +- **啟動自檢**: `app.py` 啟動時檢查 34 張 expected metadata tables,缺表直接 fail fast,防止「看似啟動成功但 create_all 漏表」。 +- **路線校正**: 12-Agent 盤點確認 3f-1 需先處理 API shadow 與 `system_bp` prefix 分裂,3f-3 需先補 EventRouter 同步告警 facade,再改 scheduler 裸 `except`。 + ### 2026-04-28~29:Phase 3e 重構大戰 + daily_sales cache 隱形 bug 根除 - **app.py 縮減 -10.8%**: 7,386 → 6,590 行,11 commits 全綠零 502。 - **抽 Blueprint**: `/api/categories` → `category_routes.py` (8fce73b);`/api/test_url` + `/brand_assets` → `misc_routes.py` (e676840)。 diff --git a/docs/memory/project_phase3f_cleanup_roadmap.md b/docs/memory/project_phase3f_cleanup_roadmap.md new file mode 100644 index 0000000..110e84c --- /dev/null +++ b/docs/memory/project_phase3f_cleanup_roadmap.md @@ -0,0 +1,28 @@ +# Phase 3f 模組化收尾任務矩陣 + +> 來源:ADR-017。此檔只保存執行矩陣與紅線,完整背景與決策以 `docs/adr/ADR-017-modularization-cleanup-roadmap.md` 為準。 + +## 紅線 + +1. 每個 sub-phase 獨立 commit。 +2. 進下一階段前必須完成 critic review、本機 self-check、188 SSH 健康檢查。 +3. ADR-011 永遠有效:部署與修復不得使用 `--remove-orphans`,不得影響 `momo-db`。 + +## 時序 + +| Phase | 目標 | 風險 | 預估 | +|---|---|---|---| +| 3f-0 | DB metadata 救急,create_all 新環境零漏表 | 最高 | 30m | +| 3f-1 | 路由雙註冊徹底解除,保 Blueprint、刪 app.py duplicate | 高 | 4-6h | +| 3f-2 | Cache 統一至 fingerprint manager,gunicorn 加 `--preload` | 中 | 2-3h | +| 3f-3 | scheduler 裸 `except`、EventRouter 同步告警、compose mount、vendor template_folder | 中 | 1-2h | +| 3f-4 | 模板統一至 `templates/` 與 `web/templates/vendor_stockout/` | 低 | 2-3h | +| 3f-5 | 孤兒 service 與 `.env.example` 收尾 | 極低 | 30m | + +## 已校正事項 + +- 3f-0 metadata 驗收為 34 張表,不是 33 張。 +- 3f-1 的公開 `/health`、`/metrics`、`/settings` 等不能直接遷進現有 `system_bp`,因它有 `/api/system` prefix。 +- 3f-2 的 `--preload` 只降低 copy-on-write 記憶體成本,一致性仍以 DB fingerprint 為準。 +- 3f-3 必須先補 `EventRouter` 同步 facade,再把 scheduler P1/P2 失敗導入告警。 +- 3f-4 移除 ChoiceLoader fallback 前,需先清 docker-compose 舊 mount 與根目錄模板。 diff --git a/services/ai_orchestrator.py b/services/ai_orchestrator.py index 9814615..ffabcb3 100644 --- a/services/ai_orchestrator.py +++ b/services/ai_orchestrator.py @@ -9,7 +9,7 @@ from sqlalchemy import text from services.hermes_analyst_service import HermesAnalystService from services.nemoton_dispatcher_service import NemotronDispatcher from database.manager import get_session -from database.ai_models import AgentContext, ActionPlan +from database.autoheal_models import AgentContext, ActionPlan logger = logging.getLogger(__name__)