diff --git a/.env.example b/.env.example index 96cb697..4e2b09d 100644 --- a/.env.example +++ b/.env.example @@ -369,6 +369,7 @@ MOMO_EVENT_ROUTER_REPLAY_LIMIT=3 # [選填] AI 自動化 Smoke 歷史保存 MOMO_AI_AUTOMATION_SMOKE_HISTORY=/app/data/ai_automation_smoke_history.jsonl MOMO_AI_AUTOMATION_SMOKE_HISTORY_LIMIT=200 +CODEBASE_INVENTORY_RECEIPT_PATH=/app/data/ai_automation/codebase_inventory/latest.json # [選填] OpenClaw Telegram bot OPENCLAW_BOT_TOKEN=your_openclaw_bot_token_here diff --git a/app.py b/app.py index af48aae..926e911 100644 --- a/app.py +++ b/app.py @@ -32,7 +32,6 @@ try: from flask import Flask, render_template, jsonify, request, send_file, redirect, url_for, flash, session from werkzeug.utils import secure_filename from pyngrok import ngrok, conf - import schedule from sqlalchemy import desc, and_, func, text, literal, case from sqlalchemy import inspect # V-New: 用於檢查資料表是否存在 from sqlalchemy.orm import joinedload @@ -42,7 +41,6 @@ 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, run_pchome_match_backfill_task from database.manager import DatabaseManager from database.models import Base, Product, PriceRecord, MonthlySummaryAnalysis from database.edm_models import PromoProduct @@ -146,6 +144,24 @@ app = Flask(__name__, template_folder=TEMPLATE_DIR, static_folder=STATIC_DIR) app.config['SEND_FILE_MAX_AGE_DEFAULT'] = timedelta(days=7) +app.config.update( + COMPRESS_ALGORITHM=['br', 'gzip'], + COMPRESS_LEVEL=6, + COMPRESS_MIN_SIZE=1024, + COMPRESS_MIMETYPES=[ + 'text/html', + 'text/css', + 'text/plain', + 'text/javascript', + 'application/javascript', + 'application/json', + 'image/svg+xml', + ], +) + +from flask_compress import Compress # noqa: E402 + +compress = Compress(app) @app.url_defaults @@ -666,6 +682,12 @@ def add_security_response_headers(response): "style-src 'self' 'unsafe-inline' https:; script-src 'self' 'unsafe-inline' https:; " "connect-src 'self' https: wss:", ) + if request.endpoint == 'static': + if request.args.get('v') == SYSTEM_VERSION: + response.headers['Cache-Control'] = 'public, max-age=31536000, immutable' + else: + response.headers.setdefault('Cache-Control', 'public, max-age=604800') + return response if session.get('logged_in'): response.headers.setdefault('Cache-Control', 'private, no-store') return response @@ -1179,35 +1201,16 @@ def prepare_calendar_data(df, selected_month): # ================= ⚙️ 5. 服務啟動邏輯 ================= def run_schedule(): - """在背景執行緒中運行排程""" - sys_log.info("🚀 排程服務已啟動,等待任務...") - while True: - schedule.run_pending() - time.sleep(1) + """Backward-compatible local runner; production uses momo-scheduler.""" + from services.web_scheduler_compat import run_schedule as _run_schedule + + return _run_schedule(sys_log) def init_scheduler(): - """初始化排程任務(Gunicorn 模式下也會執行)""" - schedule.every(1).hours.do(run_momo_task) - schedule.every(1).hours.do(run_edm_task) - schedule.every(1).hours.do(run_festival_task) - sys_log.info(f"📅 已設定每小時執行主站、EDM與購物節爬蟲任務") + """Backward-compatible local scheduler; never imported by Gunicorn startup.""" + from services.web_scheduler_compat import init_scheduler as _init_scheduler - schedule.every(30).minutes.do(run_auto_import_task) - sys_log.info(f"📅 已設定每 30 分鐘執行 Google Drive 自動匯入任務") - - schedule.every(30).minutes.do(run_whitepage_check) - sys_log.info(f"📅 已設定每 30 分鐘執行網頁白頁監控任務") - - schedule.every(4).hours.do(run_competitor_price_feeder_task) - sys_log.info(f"📅 已設定每 4 小時執行 PChome 競品價格抓取任務") - - schedule.every().day.at("10:30").do(run_pchome_match_backfill_task) - sys_log.info(f"📅 已設定每日 10:30 執行 PChome 待比對補抓與挑品重算任務") - - # 啟動排程執行緒 - scheduler_thread = threading.Thread(target=run_schedule, daemon=True) - scheduler_thread.start() - sys_log.info("✅ 排程器已在背景執行緒中啟動") + return _init_scheduler(sys_log) # V-New: 在模組載入時自動初始化排程(Gunicorn 模式下也會執行) # 🚩 V-Fix 2026-01-14: 停用自動排程器以避免多個 gunicorn workers 重複執行任務 @@ -1224,34 +1227,10 @@ def start_flask(): app.run(host='0.0.0.0', port=80, use_reloader=False) def scheduled_job_wrapper(): - """執行 MOMO 爬蟲任務並發送通知""" - timestamp = datetime.now(TAIPEI_TZ).strftime('%H:%M:%S') - sys_log.info(f"⏰ [{timestamp}] 啟動背景抓取執行緒...") - - def job(): - # 1. 執行爬蟲 - run_momo_task() - - # 2. 發送通知 (僅發送今日異動) - try: - # 重新載入通知模組 - import importlib - import scheduler - import services.notification_manager - importlib.reload(scheduler) - importlib.reload(services.notification_manager) - from services.notification_manager import NotificationManager - - stats = get_dashboard_stats() - - # 只要有任何異動數據就發送通知 - if any(stats.values()): - screenshot_path = scheduler.capture_page_screenshot("http://127.0.0.1/", "momo_dashboard") - NotificationManager().send_momo_report(stats, screenshot_path) - except Exception as e: - sys_log.error(f"[Scheduler] ❌ 發送通知失敗: {e}") + """Backward-compatible command wrapper with lazy scheduler imports.""" + from services.web_scheduler_compat import scheduled_job_wrapper as _scheduled_job_wrapper - threading.Thread(target=job, daemon=True).start() + return _scheduled_job_wrapper(sys_log, get_dashboard_stats) if __name__ == "__main__": banner = f" MOMO 專業數據管理系統 {SYSTEM_VERSION} " diff --git a/config.py b/config.py index 083b399..0b16188 100644 --- a/config.py +++ b/config.py @@ -402,7 +402,7 @@ YOUTUBE_API_KEY = os.getenv('YOUTUBE_API_KEY', '') # ========================================== # 系統版本與路徑 # ========================================== -SYSTEM_VERSION = "V10.776" +SYSTEM_VERSION = "V10.777" LOG_FILE_PATH = os.path.join(BASE_DIR, 'logs/system.log') public_url = PUBLIC_URL # 用於模板顯示 diff --git a/docs/AI_INTELLIGENCE_MODULE_SOT.md b/docs/AI_INTELLIGENCE_MODULE_SOT.md index c13c86d..9a5795d 100644 --- a/docs/AI_INTELLIGENCE_MODULE_SOT.md +++ b/docs/AI_INTELLIGENCE_MODULE_SOT.md @@ -1,8 +1,8 @@ # PChome 業績成長自動化作戰系統 — AI 競價情報模組 Single Source of Truth -> **最後更新**: 2026-07-10 (台北時間) +> **最後更新**: 2026-07-11 (台北時間) > **狀態**: 🟠 Partial。四 Agent、PixelRAG、MCP/RAG registry、PChome controlled-preview families 與多項 smoke/readback 已建立;但 access control、database identity/RBAC、full asset reconciliation、software supply chain、same-run controlled apply、restore drill、internal RAG canary 與 MCP/RAG runtime closure 尚未全部完成。任何局部 receipt 不得代表整體閉環完成。 -> **適用版本**: V10.773 +> **適用版本**: V10.777 --- @@ -15,6 +15,7 @@ - Gitea CD 必須執行 `report_security_governance_review.py --strict`,且不得解析 GitHub action/source/image。 - production 安全與治理完成度必須分開顯示 program、asset coverage、runtime closure;目前結論是 partial,不是 complete。 - 目前 ordered P0 以 `docs/guides/ai_automation_mainline_work_items.md` 為準:先關閉 access exposure,再做 identity/RBAC、webhook trust、supply chain、asset reconciliation、controlled-apply envelope、internal RAG canary 與 MCP/RAG runtime closure。 +- V10.777 起 web process 只能註冊 OpenClaw Blueprint,不得在 import 或 `record_once` 啟動 APScheduler、timed jobs 或 embedding thread;10 個 OpenClaw timed jobs 與唯一 embedding worker runtime owner 必須由 `momo-scheduler` 明確啟動。每日 04:15 的 codebase inventory 只掃描程式與前端資產並寫 artifact receipt,不讀 secret、不寫業務 DB。 --- diff --git a/docs/guides/modularization_governance.md b/docs/guides/modularization_governance.md index 98fca2c..44c29b2 100644 --- a/docs/guides/modularization_governance.md +++ b/docs/guides/modularization_governance.md @@ -47,3 +47,10 @@ - 是否讓既有 >800 行檔案淨增加?若是,先抽模組或更新 inventory 與拆分計畫。 - 是否新增重複 cache / DB manager / Telegram / AI client 初始化?若是,改用既有 service。 - 是否需要更新 ADR、memory、SOT 或 guide?若是,本次 commit 一起更新。 + +## 自動盤點與狀態回寫 + +- 本機或 CI 使用 `python3 scripts/ops/report_codebase_modularization_performance.py` 取得 JSON;加 `--write` 才原子寫入 receipt。 +- 正式 `momo-scheduler` 每日 04:15 自動執行,receipt 固定為 `data/ai_automation/codebase_inventory/latest.json`。 +- work item 狀態只允許 `completed`、`in_progress`、`not_started`;拆出 helper 不代表整個巨型模組 completed。 +- source/test 綠燈只代表候選變更可部署;必須另有 scheduler receipt、web process 無背景 worker、壓縮/cache header 與 production health readback 才能關閉 runtime lane。 diff --git a/docs/memory/code_modularization_inventory_20260430.md b/docs/memory/code_modularization_inventory_20260430.md index 7404d99..a2df64d 100644 --- a/docs/memory/code_modularization_inventory_20260430.md +++ b/docs/memory/code_modularization_inventory_20260430.md @@ -121,7 +121,7 @@ |---:|---|---|---| | 44888 | `services/pchome_mapping_backlog_service.py` | P0 gate 巨型 service | 立即停止新增 closeout package;拆 policy、executor、verifier、receipt、reporter | | 14289 | `services/ai_automation_smoke_service.py` | P0 smoke 巨型 service | 拆 family registry、collectors、health projection、metrics adapter | -| 9442 | `routes/openclaw_bot_routes.py` | P0 巨型 Blueprint | route、command、report、scheduler hook 分離 | +| 9383 | `routes/openclaw_bot_routes.py` | P0 巨型 Blueprint、拆分進行中 | scheduler hook 已移到 `services/openclaw_bot/scheduled_jobs.py`;下一步拆 report、command | | 7641 | `routes/ai_routes.py` | P0 巨型 Blueprint | PChome growth、AI automation、recommendation route extension 分離 | | 5865 | `services/marketplace_product_matcher.py` | P0 matcher 巨型 service | identity、variant、unit、scoring、explanation 分離 | | 5513 | `services/ppt_generator.py` | P0 報表巨型 service | orchestration、slides、charts、registry 分離 | @@ -142,9 +142,9 @@ | 1417 | `services/telegram_templates.py` | P1 templates | decision、incident、report renderer 分離 | | 1409 | `services/import_service.py` | P1 import | acquisition、validation、writer、verifier 分離 | | 1394 | `services/telegram_bot_service.py` | P1 bot | command、formatter、client 分離 | -| 1370 | `run_scheduler.py` | P1 scheduler entry | registration、catchup、observability wrappers 分離 | +| 1440 | `run_scheduler.py` | P1 scheduler entry、拆分進行中 | OpenClaw timed-job registry 已外移;下一步拆 task registration 與 runtime startup | | 1306 | `routes/system_public_routes.py` | P1 public routes | health、metrics、public projection 分離 | -| 1290 | `app.py` | P1 bootstrap | app factory 與 startup guards 分離 | +| 1269 | `app.py` | P1 bootstrap、拆分進行中 | web scheduler 相容層已移到 `services/web_scheduler_compat.py`;下一步拆 extension setup | | 1272 | `services/code_review_pipeline_service.py` | P2 code review | scan、normalize、persist、report 分離 | | 1202 | `services/pchome_revenue_growth_service.py` | P1 revenue growth | sales query、offer comparison、scoring、projection 分離 | | 1188 | `services/ppt_auto_generation_service.py` | P2 PPT automation | resolver、queue、generator、catchup 分離 | @@ -152,7 +152,7 @@ | 1023 | `routes/cicd_routes.py` | P2 CI/CD Blueprint | query、deploy action、route glue 分離 | | 1014 | `services/pchome_crawler.py` | P1 crawler | transport、parser、fallback、rate policy 分離 | | 1014 | `routes/daily_sales_routes.py` | P2 Daily Sales Blueprint | query、format、export、route glue 分離 | -| 1013 | `routes/export_routes.py` | P2 Export Blueprint | command、path policy、download 分離 | +| 1017 | `routes/export_routes.py` | P2 Export Blueprint | command、path policy、download 分離;string dtype Excel sanitizer 已修正 | | 989 | `routes/market_intel_routes.py` | P2 Market Intel Blueprint | page、API、MCP registration 分離 | | 966 | `services/trend_crawler.py` | P2 trend crawler | adapters、parser、persistence 分離 | | 942 | `services/learning_pipeline.py` | P2 RAG learning | distill、promotion、persistence、telemetry 分離 | @@ -178,7 +178,7 @@ ## 工作項目 -1. P0:持續拆 `routes/openclaw_bot_routes.py`;Telegram API helper 已搬到 `services/openclaw_bot/telegram_api.py`,Inline Keyboard builders 已搬到 `services/openclaw_bot/menu_keyboards.py`,下一步拆 report formatting 或 command dispatcher。 +1. P0:持續拆 `routes/openclaw_bot_routes.py`;Telegram API helper、Inline Keyboard builders 與 10 個 timed-job scheduler hook 已搬離巨型 Blueprint,下一步拆 report formatting 或 command dispatcher。 2. P0:拆 `routes/sales_routes.py`,先把 chart/query/calendar 計算搬到 `services/sales/`。 3. P0:拆 `scheduler.py`,建立 `jobs/` 或 `services/scheduler/` task registry。 4. P0:拆 `routes/admin_observability_routes.py`,先搬純查詢函式到 `services/observability_query_service.py`,再搬 AutoHeal / Code Review / AiderHeal / throttle mutation 到 `services/observability_action_service.py`。 @@ -191,3 +191,10 @@ - `tests/test_modularization_governance.py` 會掃描所有 Python 檔案;任何新的 >800 行檔案沒有列在本 inventory 就會失敗。 - 若檔案拆小後低於 800 行,可從本清單移除並同步更新測試。 + +### 2026-07-11 自動盤點 + +- `python3 scripts/ops/report_codebase_modularization_performance.py` 會為 Python 與前端大型資產建立穩定 work item ID、P0/P1、`in_progress/not_started` 狀態與拆分方向。 +- `momo-scheduler` 每日 04:15 執行同一盤點,原子寫入 `data/ai_automation/codebase_inventory/latest.json`;失敗只記 degraded receipt,不得阻斷其他排程。 +- 本次基線:447 個 production Python modules、260,059 行、48 個大於等於 800 行模組、33 個大型前端資產、23 個 P0、6/6 runtime controls 通過;program completion 69.1%,runtime closure 仍為 partial。 +- `services/openclaw_learning_service.py` 已降至 789 行;embedding worker 的唯一 runtime owner 已移到 `momo-scheduler`,因此不再列入大檔債務,但仍需由 production worker/readback 驗證。 diff --git a/requirements.txt b/requirements.txt index 4370b99..98288f1 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,5 @@ Flask>=2.3 +Flask-Compress>=1.24,<2 Flask-WTF>=1.2.2 gunicorn>=20.1 pandas>=1.5 diff --git a/routes/export_routes.py b/routes/export_routes.py index 530477c..868ca48 100644 --- a/routes/export_routes.py +++ b/routes/export_routes.py @@ -65,7 +65,11 @@ def _sanitize_excel_dataframe(df: pd.DataFrame) -> pd.DataFrame: return df cleaned = df.copy() for column in cleaned.columns: - if cleaned[column].dtype == object: + column_dtype = cleaned[column].dtype + if ( + pd.api.types.is_object_dtype(column_dtype) + or pd.api.types.is_string_dtype(column_dtype) + ): cleaned[column] = cleaned[column].map(_sanitize_excel_cell) return cleaned diff --git a/routes/openclaw_bot_routes.py b/routes/openclaw_bot_routes.py index 372bc33..e6d8a7a 100644 --- a/routes/openclaw_bot_routes.py +++ b/routes/openclaw_bot_routes.py @@ -346,7 +346,6 @@ TRIGGER_KEYWORDS = [] # 空 = 全部回應(小龍蝦是專用業務群組) # ── 目標管理(記憶體,跨 session 用 DB 儲存)───────────────────── _GOALS: dict = {} # {'daily','monthly','quarterly','half','yearly': float} -_scheduler = None # ── 輸入等待狀態機(chat_id → pending action)──────────────────── _input_pending: dict = {} # {chat_id: {'action': str, 'label': str}} @@ -4814,70 +4813,13 @@ def send_daily_excel(): def start_scheduler(): - """啟動排程(Flask app 啟動後呼叫)""" - global _scheduler - try: - if _scheduler is not None and _scheduler.running: - sys_log.info("[OpenClawBot] Scheduler 已在執行中,跳過重複啟動") - return - - from apscheduler.schedulers.background import BackgroundScheduler - from apscheduler.triggers.cron import CronTrigger - - _scheduler = BackgroundScheduler(timezone='Asia/Taipei') - _scheduler.add_job( - send_morning_report, - CronTrigger(hour=8, minute=30), - id="openclaw_send_morning_report", - replace_existing=True, - ) - _scheduler.add_job( - send_competitor_report, - CronTrigger(hour=8, minute=0), - id="openclaw_send_competitor_report", - replace_existing=True, - ) - _scheduler.add_job( - send_daily_excel, - CronTrigger(hour=8, minute=45), - id="openclaw_send_daily_excel", - replace_existing=True, - ) - _scheduler.add_job( - send_evening_report, - CronTrigger(hour=21, minute=0), - id="openclaw_send_evening_report", - replace_existing=True, - ) - _scheduler.add_job( - send_weekly_report, - CronTrigger(day_of_week='mon', hour=9, minute=0), - id="openclaw_send_weekly_report", - replace_existing=True, - ) - _scheduler.add_job( - check_anomalies, - CronTrigger(hour='9,12,15,18', minute=0), - id="openclaw_check_anomalies", - replace_existing=True, - ) - # ADR-019 Phase 6: 每日 09:00 主動巡檢資料新鮮度,缺資料時透過 EventRouter 發警告 - try: - from services.data_freshness_probe import run_data_freshness_probe - _scheduler.add_job( - run_data_freshness_probe, - CronTrigger(hour=9, minute=5), - id="openclaw_data_freshness_probe", - replace_existing=True, - ) - except ImportError as _e: - sys_log.warning(f"[OpenClawBot] data_freshness_probe 未安裝,跳過:{_e}") - _scheduler.start() - sys_log.info("[OpenClawBot] Scheduler started ✓ (competitor/morning/excel/evening/weekly/anomaly/freshness)") - except ImportError: - sys_log.warning("[OpenClawBot] APScheduler 未安裝 — 排程功能停用") - except Exception as e: - sys_log.error(f"[OpenClawBot] start_scheduler: {e}") + """Compatibility readback for callers of the retired web-worker scheduler.""" + sys_log.info("[OpenClawBot] Timed jobs are owned by momo-scheduler") + return { + "status": "delegated", + "runtime_owner": "momo-scheduler", + "scheduler": "schedule", + } def register_commands(): @@ -9437,6 +9379,5 @@ def webhook_info(): @openclaw_bot_bp.record_once def _on_register(_state): - """Blueprint 被 app.register_blueprint 時自動啟動排程""" - start_scheduler() - sys_log.info("[OpenClawBot] Blueprint registered — scheduler boot triggered") + """Register HTTP routes without starting background workers in Gunicorn.""" + sys_log.info("[OpenClawBot] Blueprint registered; timed jobs delegated to momo-scheduler") diff --git a/run_scheduler.py b/run_scheduler.py index 85677e3..8d103b9 100644 --- a/run_scheduler.py +++ b/run_scheduler.py @@ -358,6 +358,9 @@ def _register_schedules(): schedule.every().day.at("04:00").do(run_backup_monitor_task) logger.info("📅 每日 04:00:backup_monitor") + schedule.every().day.at("04:15").do(run_codebase_modularization_performance_task) + logger.info("📅 每日 04:15:codebase_modularization_performance inventory") + schedule.every().monday.at("06:00").do(run_weekly_strategy_task) logger.info("📅 每週一 06:00:weekly_strategy") @@ -389,6 +392,10 @@ def _register_schedules(): schedule.every().day.at("07:00").do(_monthly_report_gate) logger.info("📅 每月1日 07:00:monthly_report") + from services.openclaw_bot.scheduled_jobs import register_openclaw_schedules + + register_openclaw_schedules(schedule, logger) + def run_daily_token_report_task(): """每日 23:55 — Operation Ollama-First v5.0 Phase 1 收尾:LLM Token 日報。 @@ -1298,6 +1305,46 @@ def run_cleanup_agent_context(): session.close() +def run_codebase_modularization_performance_task(): + """Scan source/assets, generate work items, and persist a bounded receipt.""" + try: + from services.codebase_modularization_performance_service import ( + build_codebase_inventory, + write_codebase_inventory_receipt, + ) + + result = build_codebase_inventory() + receipt_path = write_codebase_inventory_receipt(result) + payload = { + "status": "completed", + "summary": result["summary"], + "receipt_path": str(receipt_path), + "top_p0_work_items": [ + item + for item in result["python_work_items"] + result["frontend_work_items"] + if item["priority"] == "P0" + ][:20], + } + _save_stats("codebase_modularization_performance", payload) + logger.info( + "[CodebaseInventory] completion=%s p0=%s receipt=%s", + payload["summary"]["program_completion_percent"], + payload["summary"]["p0_work_item_count"], + receipt_path, + ) + return payload + except Exception as error: + logger.error("[CodebaseInventory] task failed: %s", error, exc_info=True) + _notify_scheduler_failure( + "run_codebase_modularization_performance_task", + error, + source="Scheduler.CodebaseInventory", + event_type="codebase_inventory_failure", + title="Codebase 模組化與效能盤點失敗", + ) + return {"status": "failed", "error_kind": type(error).__name__} + + def _run_elephant_alpha_engine(): """Daemon thread: ElephantAlpha 自主監控引擎(獨立 asyncio loop)""" loop = None @@ -1320,6 +1367,29 @@ if __name__ == "__main__": _register_schedules() logger.info("✅ 全部排程任務已註冊") + try: + from services.openclaw_bot.embedding_worker_runtime import start_embedding_worker + + _embedding_thread = start_embedding_worker() + logger.info( + "[OpenClawRuntime] embedding worker=%s alive=%s", + _embedding_thread.name, + _embedding_thread.is_alive(), + ) + except Exception as _embedding_error: + logger.error( + "[OpenClawRuntime] embedding worker startup failed: %s", + _embedding_error, + exc_info=True, + ) + _notify_scheduler_failure( + "start_openclaw_embedding_worker", + _embedding_error, + source="Scheduler.OpenClawRuntime", + event_type="openclaw_embedding_worker_startup_failure", + title="OpenClaw embedding worker 啟動失敗", + ) + _ea_thread = threading.Thread( target=_run_elephant_alpha_engine, daemon=True, diff --git a/scripts/ops/report_codebase_modularization_performance.py b/scripts/ops/report_codebase_modularization_performance.py new file mode 100644 index 0000000..cd273de --- /dev/null +++ b/scripts/ops/report_codebase_modularization_performance.py @@ -0,0 +1,34 @@ +#!/usr/bin/env python3 +"""Report or persist the current codebase modularization/performance inventory.""" + +from __future__ import annotations + +import argparse +import json +import sys +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[2] +if str(ROOT) not in sys.path: + sys.path.insert(0, str(ROOT)) + +from services.codebase_modularization_performance_service import ( # noqa: E402 + build_codebase_inventory, + write_codebase_inventory_receipt, +) + + +def main() -> int: + parser = argparse.ArgumentParser() + parser.add_argument("--write", action="store_true", help="persist the latest runtime receipt") + args = parser.parse_args() + result = build_codebase_inventory() + if args.write: + result["receipt_path"] = str(write_codebase_inventory_receipt(result)) + print(json.dumps(result, ensure_ascii=False, indent=2)) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/services/codebase_modularization_performance_service.py b/services/codebase_modularization_performance_service.py new file mode 100644 index 0000000..fe2cd7c --- /dev/null +++ b/services/codebase_modularization_performance_service.py @@ -0,0 +1,217 @@ +"""Automated codebase modularization and frontend payload inventory.""" + +from __future__ import annotations + +import hashlib +import json +import os +from datetime import datetime, timedelta, timezone +from pathlib import Path +from typing import Iterable + + +ROOT = Path(__file__).resolve().parents[1] +TAIPEI_TZ = timezone(timedelta(hours=8)) +PYTHON_DEBT_LINES = 800 +FRONTEND_P0_BYTES = 50_000 +FRONTEND_P1_BYTES = 20_000 +IN_PROGRESS_PYTHON_PATHS = { + "app.py", + "routes/openclaw_bot_routes.py", + "run_scheduler.py", +} +IGNORED_PARTS = { + ".git", + ".pytest_cache", + ".venv", + "__pycache__", + "backups", + "tests", + "venv", +} + + +def _iter_python_files(root: Path) -> Iterable[Path]: + for path in root.rglob("*.py"): + relative = path.relative_to(root) + if any(part in IGNORED_PARTS for part in relative.parts): + continue + if relative.parts[:2] in {("docs", "design"), ("docs", "design_audit_frontend")}: + continue + yield path + + +def _iter_frontend_files(root: Path) -> Iterable[Path]: + for folder in ("templates", "static", "web/templates", "web/static"): + base = root / folder + if not base.exists(): + continue + for path in base.rglob("*"): + if path.is_file() and path.suffix.lower() in {".html", ".css", ".js"}: + yield path + + +def _line_count(path: Path) -> int: + with path.open(encoding="utf-8", errors="ignore") as handle: + return sum(1 for _ in handle) + + +def _priority_for_python(lines: int) -> str: + if lines >= 2_000: + return "P0" + if lines >= PYTHON_DEBT_LINES: + return "P1" + return "P2" + + +def _priority_for_frontend(size_bytes: int) -> str: + return "P0" if size_bytes >= FRONTEND_P0_BYTES else "P1" + + +def _split_direction(path: str) -> str: + if path.startswith("routes/"): + return "separate route glue, queries, commands, and projections" + if path == "scheduler.py" or path == "run_scheduler.py": + return "separate task registry, domain jobs, runtime workers, and receipts" + if path.startswith("services/"): + return "separate policy, adapters, execution, verification, and reporting" + if path == "app.py": + return "extract app factory, extensions, blueprint registry, and startup guards" + return "separate cohesive domain responsibilities behind stable interfaces" + + +def _work_item_id(kind: str, path: str) -> str: + digest = hashlib.sha256(path.encode("utf-8")).hexdigest()[:10] + return f"{kind}-{digest}" + + +def _runtime_controls(root: Path) -> list[dict]: + app_source = (root / "app.py").read_text(encoding="utf-8") + bot_source = (root / "routes/openclaw_bot_routes.py").read_text(encoding="utf-8") + learning_source = (root / "services/openclaw_learning_service.py").read_text(encoding="utf-8") + login_source = (root / "templates/login.html").read_text(encoding="utf-8") + registration = bot_source[bot_source.index("@openclaw_bot_bp.record_once") :] + return [ + { + "id": "web_no_eager_scheduler_import", + "passed": "from scheduler import" not in app_source, + }, + { + "id": "openclaw_web_registration_no_scheduler_boot", + "passed": "start_scheduler()" not in registration, + }, + { + "id": "embedding_worker_explicit_runtime_owner", + "passed": "Thread(target=_embedding_worker_loop" not in learning_source, + }, + { + "id": "text_compression_enabled", + "passed": "Compress(app)" in app_source and "COMPRESS_ALGORITHM" in app_source, + }, + { + "id": "versioned_static_immutable", + "passed": "max-age=31536000, immutable" in app_source, + }, + { + "id": "login_has_no_third_party_assets", + "passed": "https://" not in login_source and " + - +
- + + + EwoooC

營運數據工作台

登入後先看 PChome 業績、價差、缺貨與 AI 建議。

- -
@@ -295,26 +34,23 @@