# routes/openclaw_bot_routes.py 拆分地圖 > **產出時間**:2026-04-24 > **現況**:5999 行單檔(God File),僅被 `app.py:683` 一處 import > **目的**:為 H1 重構提供設計藍圖,**本文件不改任何 .py** > **交付對象**:refactor-specialist / critic / planner --- ## 0. 全景快照 | 指標 | 數字 | |------|------| | 總行數 | 5999 | | 外部 importer | 1(`app.py:683 from routes.openclaw_bot_routes import openclaw_bot_bp`) | | Blueprint routes | 4(`/bot/telegram/webhook`、`/bot/internal/cmd`、`/bot/telegram/set_webhook`、`/bot/telegram/webhook_info`) | | 頂層函數 | ~90 個 | | 模組級全域 (mutable) | 5(`_GOALS`、`_input_pending`、`_excel_pending`、`_seen_update_ids/_order`、`_rate_tracker`、`_scheduler`、`_sched_lock_fh`、`_MPL_FONT_SETUP_DONE`) | | 第三方 import | requests, flask, sqlalchemy, matplotlib(延遲), openpyxl(延遲), apscheduler(延遲), pandas(延遲) | | 跨專案服務 | `services.mcp_context_service`、`services.openclaw_learning_service`、`services.pchome_crawler`、`services.ppt_generator`、`database.manager`、`services.logger_manager` | --- ## 1. 全檔分區(按行號順序) | # | 行號 | 區塊 | 主要符號 | 對外依賴 | 內部共享狀態 | |---|-----|------|---------|---------|-------------| | A | 1-72 | imports、Blueprint、env 常數 | `GEMINI_*`、`BOT_TOKEN`、`ALLOWED_GROUP`、`NVIDIA_*`、`openclaw_bot_bp` | 3rd-party + 4 個 services | — | | B | 74-138 | **安全/限流/去重基礎設施** | `_is_authorized`、`_check_rate_limit`、`_seen_update_ids`、`_seen_update_order`、`_rate_tracker`、`TRIGGER_KEYWORDS`、`ALLOWED_USERS` | os、time | 持有 rate + seen 狀態 | | C | 140-251 | **Telegram API wrapper** | `_tg`、`_strip_markdown`、`send_message`、`answer_callback`、`send_typing`、`send_photo`、`send_document` | requests | 無(純函數) | | D | 253-262 | **Mutable 全域 dict 宣告** | `_GOALS`、`_scheduler`、`_input_pending`、`_excel_pending` | — | 跨整個檔案共用(核心耦合點) | | E | 264-295 | 中文字型搜尋 | `_CHINESE_FONT_PATHS`、`_FONT_DL_URL`、`_get_chinese_font` | os、requests | — | | F | 299-611 | **Excel 日報產生器**(非 PDF,名稱誤導) | `generate_daily_pdf` | openpyxl(延遲)、tempfile | 依賴 `query_*`、`TAIPEI_TZ` | | G | 613-1127 | **DB Query 層 #1(複雜型)** | `query_category_sales`、`query_category_monthly`、`query_comparison`、`query_daily_history`、`query_restock_forecast`、`query_category_detail`、`query_promo_comparison`、`query_anomalies`、`query_growth_data`、`query_vendor_bcg_data`、`get_goal_status` | DatabaseManager、SQLAlchemy text | 讀 `_GOALS`(`get_goal_status`) | | H | 1129-1381 | **Matplotlib 圖表** | `_MPL_FONT_SETUP_DONE`、`_setup_mpl_chinese`、`gen_trend_chart`、`gen_products_chart` | matplotlib(延遲)、`_get_chinese_font` | `_MPL_FONT_SETUP_DONE` | | I | 1384-1479 | **策略分析** | `analyze_product_strategy`、`_analyze_strategy_range` | `query_top_products`、MCPRouter/query_mcp | 讀 DB + MCP | | J | 1481-1960 | **訊息格式化 #1** | `fmt_category`、`fmt_comparison`、`fmt_goal_status`、`_short_id`、`fmt_restock_forecast`、`fmt_category_detail`、`fmt_promo_comparison`、`track_competitor_price_changes`、`fmt_monthly`、`fmt_strategy` | — | 純字串處理 | | K | 1962-2723 | **AI 簡報分析 + PPT 指令分派** | `_clean_ai_text`、`_ppt_ai_analysis`、`_generate_ppt_cmd` | requests(NIM)、`services.ppt_generator`(延遲) | 呼叫大量 `query_*`、`_ppt_ai_analysis` 被 `_generate_ppt_cmd` 內部用 10 次 | | L | 2724-2910 | **Excel 匯入** | `_EXCEL_REQUIRED_COLS`、`_EXCEL_OPTIONAL_COLS`、`_validate_excel_format`、`_fmt_excel_validation_report`、`_download_telegram_file`、`_handle_excel_import` | pandas(延遲) | 寫 `_excel_pending` | | M | 2912-3321 | **排程任務(6 個 cron job)** | `send_morning_report`、`send_evening_report`、`send_weekly_report`、`check_anomalies`、`send_competitor_report`、`send_daily_excel`、`_sched_lock_fh` | `query_*`、`generate_daily_pdf`、`gen_trend_chart`、`send_message/photo/document`、`pchome_*` | 產生告警後去重寫 DB | | N | 3323-3388 | **Scheduler 啟動/指令註冊** | `start_scheduler`、`register_commands` | apscheduler(延遲)、fcntl | 寫 `_scheduler` | | O | 3392-3649 | **Inline Keyboard / Submenu / Await Prompt** | `_BACK`、`main_menu_keyboard`、`_submenu_*`(10 個)、`_SUBMENUS`、`_AWAIT_PROMPTS`、`sales_quick_kb` | `latest_date`、`_GOALS` | — | | P | 3651-3895 | **DB Query 層 #2(基礎型)** | `_db`、`normalize_date`、`latest_date`、`query_sales`、`query_top_products`、`query_top_vendors`、`query_weekly_trend`、`query_trend_range`、`query_monthly_summary`、`query_date_range`、`query_available_months`、`query_top_products_range` | DatabaseManager、SQLAlchemy text | — | | Q | 3897-3979 | **日期/意圖解析** | `resolve_date`、`resolve_query_intent` | — | — | | R | 3981-4325 | **訊息格式化 #2** | `MEDALS`、`fmt_sales`、`_esc`、`_pchome_link`、`PCHOME_URL`、`fmt_products`、`fmt_vendors`、`fmt_trend`、`fmt_trend_summary`、`gen_aggregated_chart` | matplotlib(延遲) | — | | S | 4327-4345 | Help 關鍵字判斷 | `_HELP_KEYWORDS`、`_is_help_question` | — | — | | T | 4347-4510 | **Gemini Function Calling 工具定義** | `_FC_TOOLS`、`_execute_tool` | `query_*`、MCP 家族、`retrieve_knowledge` | — | | U | 4512-4698 | **openclaw_answer(AI 入口)** | `openclaw_answer` | requests(Gemini/NVIDIA)、`_FC_TOOLS`、`_execute_tool`、`build_rag_context`、`store_conversation` | — | | V | 4700-5587 | **handle_cmd 單體(28 個 cmd 分支)** | `handle_cmd` | **幾乎所有上游函數** | 寫 `_GOALS`、讀 `_excel_pending` | | W | 5589-5943 | **telegram_webhook(354 行)** | route `/bot/telegram/webhook` | `_is_authorized`、`_check_rate_limit`、`_SUBMENUS`、`_AWAIT_PROMPTS`、`_input_pending`、`_excel_pending`、`_GOALS`、`handle_cmd`、`openclaw_answer`、Gemini Vision API | 寫 `_seen_update_*`、`_input_pending`、`_GOALS` | | X | 5946-5970 | internal_cmd 端點 | route `/bot/internal/cmd` | `handle_cmd`(threaded) | — | | Y | 5973-5999 | 管理端點 + blueprint hook | `set_webhook`、`webhook_info`、`_on_register` | `_tg`、`register_commands`、`start_scheduler` | — | --- ## 2. 內部呼叫熱度(依賴分析) ### 2.1 熱點函數(拆檔風險高,被 5+ 處呼叫) | 函數 | 呼叫次數 | 所在區塊 | 拆檔策略 | |------|---------|---------|---------| | `send_message` | 105 | C | **保留成 shared utility,最早抽出**(`services/openclaw_tg.py`)| | `handle_cmd` | 97(含內部遞迴) | V | **最後才拆**(中央調度器) | | `query_top_products` | 17 | P | 抽到 `services/openclaw_queries.py` | | `query_weekly_trend` | 11 | P | 同上 | | `query_sales` | 11 | P | 同上 | | `_ppt_ai_analysis` | 10 | K | 同 `_generate_ppt_cmd` 一起抽(`services/openclaw_ppt.py`) | | `query_monthly_summary` | 7 | P | 同上 | | `answer_callback` | 3, `send_photo` 6, `send_typing` 5 | C | 與 send_message 一起抽 | | `query_top_vendors` | 6, `analyze_product_strategy` 6 | P/I | 同上 | | `get_goal_status` | 5 | G | **讀 `_GOALS` 全域**,若 query 抽出需注入或 goal state 隨之抽 | ### 2.2 低耦合函數(抽離成本低) - **Gemini Vision 圖片辨識** (webhook 5694-5752, 58 行) — 目前 inline 在 `telegram_webhook`,自成函數且完全獨立,**最先可抽**。 - `gen_trend_chart` / `gen_products_chart` / `gen_aggregated_chart` — 純 matplotlib,依賴 `_setup_mpl_chinese` + DB query 結果(非直接)。 - `resolve_date` / `resolve_query_intent` — 無副作用、僅字串處理。 - `_validate_excel_format` / `_fmt_excel_validation_report` / `_download_telegram_file` — Excel 匯入子系統(L 區),只被 `_handle_excel_import` 用。 ### 2.3 Dead / 1-呼叫點函數(可就地內聯) - `_strip_markdown`(C 區)— 僅 `send_message` 內部降級時使用。 - 所有 6 個排程任務(`send_*_report`、`check_anomalies`、`send_daily_excel`)各自僅被 `start_scheduler.add_job` 呼叫 1 次。 ### 2.4 全域狀態耦合圖 ``` ┌─────────────┐ │ _GOALS │──read──► get_goal_status (G), _submenu_goals (O), generate_daily_pdf (F) │ (dict) │──write─► handle_cmd (V: goal), telegram_webhook (W: await.goal_*) └─────────────┘ ┌──────────────────┐ │ _input_pending │──read/write──► telegram_webhook (W) │ (chat_id→state) │ only consumer; await flow 狀態機 └──────────────────┘ ┌──────────────────┐ │ _excel_pending │──write──► _handle_excel_import (L) │ │──read───► handle_cmd (V: import_confirm/cancel) └──────────────────┘ ┌──────────────────┐ │ _seen_update_* │──read/write──► telegram_webhook (W) │ (去重) │ └──────────────────┘ ┌──────────────────┐ │ _rate_tracker │──read/write──► _check_rate_limit (B) via telegram_webhook (W) └──────────────────┘ ┌──────────────────┐ │ _MPL_FONT_SETUP │ → _setup_mpl_chinese (H), module-local init flag └──────────────────┘ ┌─────────────────────┐ │ _scheduler/_lock_fh │ → start_scheduler (N) only └─────────────────────┘ ``` **關鍵發現**:`_input_pending` 和 `_seen_update_*` 的讀寫皆集中在 `telegram_webhook`(W 區),**只要 webhook 區塊抽出,狀態即可隨之遷移**。`_GOALS` 則跨 4 個區塊讀寫,需要以 service 或 singleton 封裝。 ### 2.5 依賴層級(ASCII,抽象層次由低到高) ``` ┌──────────────────────────────────────────────────────────────────────┐ │ Layer 0 常數 + env(A 區)— 任何層皆可 import │ ├──────────────────────────────────────────────────────────────────────┤ │ Layer 1 純 util: Telegram API (C)、中文字型 (E)、safety (B 部分) │ ├──────────────────────────────────────────────────────────────────────┤ │ Layer 2 Query (G, P) + Intent (Q) + 全域 state (D) │ ├──────────────────────────────────────────────────────────────────────┤ │ Layer 3 Chart (H) + Strategy (I) + Format (J, R) — 消費 Layer 2 │ ├──────────────────────────────────────────────────────────────────────┤ │ Layer 4 Excel 匯入 (L) + Excel 匯出 (F) — 消費 Layer 1/2 │ ├──────────────────────────────────────────────────────────────────────┤ │ Layer 5 AI: FC 工具 (T, U) + PPT 分析/分派 (K) │ ├──────────────────────────────────────────────────────────────────────┤ │ Layer 6 Keyboard/Menu (O) + Schedule Job (M) — 消費 L4 + L1 │ ├──────────────────────────────────────────────────────────────────────┤ │ Layer 7 Command dispatcher (V: handle_cmd) │ ├──────────────────────────────────────────────────────────────────────┤ │ Layer 8 Webhook + Blueprint route (W, X, Y) + Scheduler boot (N) │ └──────────────────────────────────────────────────────────────────────┘ ``` 重要違規:目前 F 區 `generate_daily_pdf` 同時依賴 Layer 2(query)和 Layer 4(openpyxl),已是跨層。抽分時建議將其放 Layer 4 並注入 query callback。 --- ## 3. 外部依賴掃描 ### 3.1 誰 import 本檔 ```bash $ grep -rn "from routes.openclaw_bot_routes import\|from routes import openclaw_bot_routes" --include="*.py" app.py:683: from routes.openclaw_bot_routes import openclaw_bot_bp ``` **只有一處**,且只暴露 `openclaw_bot_bp`。這對重構極為友善 — 拆分後只要保留 `openclaw_bot_bp` blueprint 可從新主模組 re-export,`app.py` 完全不動。 ### 3.2 本檔 import 的外部模組 | 模組 | 用途 | 風險 | |------|------|------| | `database.manager.DatabaseManager` | DB 連線 | 被 query 層大量使用 | | `services.logger_manager.SystemLogger` | log | 全檔共用 `sys_log` | | `services.mcp_context_service` | 外部情報 | 9 個函數 import | | `services.openclaw_learning_service` | RAG/學習 | try-except,optional | | `services.pchome_crawler` | 比價 | try-except,optional | | `services.ppt_generator`(延遲) | PPT | 在 `_generate_ppt_cmd` 內部 import | --- ## 4. 拆分提案(修正版) 原 critic 6 檔方案大方向正確,但**經實際依賴分析**需要兩處修正: 1. **Excel 匯入(L 區)** 被遺漏,應獨立為 `services/openclaw_excel.py`(Excel 匯入 + Excel 匯出 = 一檔)。 2. **Gemini Function Calling(T+U 區)** 依賴太多 `query_*`,且是 webhook 訊息分支第二層入口,應獨立為 `services/openclaw_nlu.py`(非 commands)。 3. `services/openclaw_menus.py` 目前 265 行偏小但高度自成一體,可採納。 4. 排程任務(M+N 區,465 行)應獨立為 `services/openclaw_scheduler.py`(原提案未涵蓋)。 ### 4.1 修正後拆分目標(10 檔) | 新檔 | 來源區塊 | 預估行數 | 職責 | Export interface | |------|---------|---------|------|-----------------| | `routes/openclaw_webhook.py` | A + W + X + Y + N(boot) | ~500 | Blueprint 4 個 route、webhook 分派、scheduler 啟動 hook | `openclaw_bot_bp`(只此一個 public) | | `services/openclaw_tg.py` | C + B + E | ~220 | Telegram API wrapper、授權、限流、去重、字型 | `send_message`、`answer_callback`、`send_typing`、`send_photo`、`send_document`、`_tg`、`is_authorized`、`check_rate_limit`、`SeenCache`、`get_chinese_font` | | `services/openclaw_queries.py` | G + P + Q + I + 部分 D(`_GOALS` facade) | ~1200 | 所有 DB query + 日期解析 + 策略分析 + goal state | 所有 `query_*`、`resolve_date`、`resolve_query_intent`、`analyze_product_strategy`、`get_goal_status`、`GoalState`(封裝 `_GOALS`) | | `services/openclaw_charts.py` | H + R 中 `gen_aggregated_chart` | ~320 | matplotlib 圖表 | `gen_trend_chart`、`gen_products_chart`、`gen_aggregated_chart`、`setup_mpl_chinese` | | `services/openclaw_excel.py` | F + L | ~500 | Excel 匯出(日報)+ Excel 匯入(驗證/handler) | `generate_daily_excel_report`、`handle_excel_import`、`ExcelPendingStore` | | `services/openclaw_format.py` | J + R 其餘 | ~630 | 純訊息格式化 | 所有 `fmt_*`、`MEDALS`、`_pchome_link`、`_esc`、`_short_id` | | `services/openclaw_ppt.py` | K | ~560 | PPT AI 分析 + 9 種 PPT 生成分派 | `generate_ppt_cmd`、`_ppt_ai_analysis`(private) | | `services/openclaw_nlu.py` | S + T + U | ~360 | Gemini Function Calling 入口 | `openclaw_answer`、`_FC_TOOLS`(private) | | `services/openclaw_scheduler.py` | M + N(core) | ~470 | 6 個排程 job + start_scheduler | `start_scheduler`、`register_commands` | | `services/openclaw_commands.py` | V + O | ~1050 | `handle_cmd` 主分派 + 所有 submenu/inline keyboard | `handle_cmd`、`main_menu_keyboard`、`SUBMENUS`、`AWAIT_PROMPTS`、`InputPendingStore` | 合計 ~5810 行(扣除重複 import、header docstring 合理),vs 原 5999 行。 --- ## 5. 風險與順序 ### 5.1 拆分順序(由低風險 → 高風險,7 階段) | Phase | 新檔 | 風險 | 前置條件 | 驗證 | |-------|------|------|---------|------| | **A1** | `services/openclaw_tg.py` | 🟢 低 | 無(純 wrapper,無狀態跨區除 `_seen_update_*` 留在 webhook) | Webhook 回覆 / 按鈕 / 圖片 / 文件 4 路徑各 1 次煙霧測 | | **A2** | `services/openclaw_charts.py` | 🟢 低 | A1(`send_photo` import) | `/chart`、早報 PNG 附圖 | | **A3** | `services/openclaw_format.py` | 🟢 低 | 無副作用 | 隨 A4 回歸 | | **A4** | `services/openclaw_queries.py` | 🟡 中 | `_GOALS` 需以 `GoalState` 封裝 | 13 個 query 都跑過;`/sales`、`/top`、`/goal`、`/promo`、`/restock`、`/compare` | | **A5** | `services/openclaw_excel.py` | 🟡 中 | A4(用 query) | `/report` 下載 + 群組拖 xlsx 匯入 | | **A6** | `services/openclaw_scheduler.py` | 🟡 中 | A1-A5 | **停用 4/5 個 job,留 1 個做 dry-run**;觀察 08:00 競品日報 3 日連續成功 | | **B1** | `services/openclaw_ppt.py` | 🟠 高 | A3 + A4 | 9 種 PPT 全跑一遍 | | **B2** | `services/openclaw_nlu.py` | 🟠 高 | A4 | `openclaw_answer` 20 句對話、FC 3 個工具各觸發 | | **B3** | `services/openclaw_commands.py` | 🔴 最高 | 全部 | 28 個 cmd 分支全測,Excel 匯入+目標設定+await 狀態機 | | **B4** | `routes/openclaw_webhook.py` | 🟠 高 | B3 | 保留 blueprint 同名,`app.py` 零變更 | **安全網**:每 Phase 一個 commit(不做 feature flag — 風險控制靠**小 diff + 可還原**),每階段部署後要跑 smoke(見 5.2)。 ### 5.2 Regression 測試重點 ``` [每 Phase 共同] 1. GET /bot/telegram/webhook_info → 200 且 url 正確 2. POST /bot/internal/cmd {cmd:sales} → 200,群組收到業績訊息 3. Telegram 群組發 「今日業績」 → openclaw_answer 回覆且 ≤10s 4. Telegram 群組按「📊業績查詢 → 今日」 → 看到業績 + quick_kb 5. Telegram 群組發 /goal 150000 → _GOALS 寫入、下次 /goal 顯示 150000 [Phase A5 額外] 6. 拖一個 .xlsx 進群組 → 驗證報告出現、按 ✅ 匯入 OK 7. /report 2026/04/20 → 收到 Excel 附件 [Phase A6 額外] 8. 手動 _scheduler.get_jobs() 在 shell 中驗證 6 個 job 都在 [Phase B1 額外] 9. cmd:ppt:daily / weekly / monthly / strategy / competitor / promo / growth / vendor / bcg 各跑一次 [Phase B4 最終] 10. 所有 schedule job 接 5 天觀察(8:00/8:30/8:45/9:00/12:00/15:00/18:00/21:00 + 週一 9:00) ``` ### 5.3 工時估算(refactor-specialist 參考) | Phase | 人時 | 備註 | |-------|------|------| | A1 TG wrapper | 3h | 機械搬運 | | A2 Charts | 2h | | | A3 Format | 2h | | | A4 Queries + GoalState | 6h | `_GOALS` 封裝需設計、跨 4 個呼叫點 | | A5 Excel | 4h | `_excel_pending` 遷移 | | A6 Scheduler | 4h | 監控期不算 | | B1 PPT | 5h | `_ppt_ai_analysis` prompt 內容多 | | B2 NLU | 4h | `_FC_TOOLS` schema 保持不動 | | B3 Commands + Menus | 10h | 28 個分支、`handle_cmd` 內部互相呼叫(遞迴) | | B4 Webhook thin | 5h | `_seen_update_*` + `_input_pending` 遷移、Gemini Vision 抽函數 | | **小計** | **45h** | 不含 smoke 測試與部署監控時段 | | **含監控** | **60h(~2 週 Sprint)** | | ### 5.4 不可忽略的坑 1. **`generate_daily_pdf` 其實是 Excel**(F 區),真 PDF 只是函數名。重構時改名 `generate_daily_excel_report` 為佳;注意 `send_daily_excel` 排程會呼叫它。 2. **`handle_cmd` 會遞迴呼叫自己**(webhook await flow → `handle_cmd('sales', ...)`、圖片 Vision → `handle_cmd('competitor', ...)`)—拆到 commands.py 時要注意不要同時在 webhook.py 也留副本。 3. **`_ppt_ai_analysis` 在 `_generate_ppt_cmd` 被呼叫 10 次**,兩者必須同檔(openclaw_ppt.py)不可分離。 4. **`_seen_update_*` 雙結構去重(ADR-類修補)**:deque + set 互為見證,拆檔時兩個必須綁定,不能只搬一個。 5. **Blueprint `record_once` hook 觸發 `start_scheduler`**(5995-5998)—scheduler 遷移後記得 hook 仍要呼叫新位置。 6. **`import calendar as _cal`、`from datetime import date as _date` 散落在 `handle_cmd` 內部**(4723-4724)—搬遷時要一起帶走,否則 UnboundLocalError。 7. **Gemini Vision 圖片分支**(5694-5752, 58 行)目前 inline 在 `telegram_webhook` 裡,抽成 function 後既減負擔又可單測 — 建議 Phase A1 的時候一起做。 8. **`_on_register` blueprint hook**(Y 區)只有 4 行但是整個排程啟動的唯一觸發點 — webhook.py 必須保留此 hook 對應新的 `start_scheduler` import。 --- ## 6. 最終交付檢核單(給接手 refactor-specialist) - [ ] `app.py:683` 可以不變(`from routes.openclaw_bot_routes import openclaw_bot_bp` 仍有效:可在 `routes/openclaw_bot_routes.py` 保留 compat shim `from routes.openclaw_webhook import openclaw_bot_bp` 1 行) - [ ] 4 個 Blueprint route 行為 1:1 相同(webhook、internal_cmd、set_webhook、webhook_info) - [ ] 6 個排程任務於 `start_scheduler` 裡仍以相同 cron 表達式註冊 - [ ] 28 個 `handle_cmd` 分支全部保留,分支字串(中/英 alias)不變 - [ ] Global state 遷移清單:`_GOALS`、`_input_pending`、`_excel_pending`、`_seen_update_*`、`_rate_tracker`、`_scheduler`、`_sched_lock_fh`、`_MPL_FONT_SETUP_DONE` 各有確定的新家(module-level 或封裝成物件) - [ ] 跨檔 import 循環檢查:`openclaw_commands` ↔ `openclaw_queries` 不得互相 import(commands 單向 depend on queries) - [ ] 監控 3 日生產驗證後才視為完成 --- *本地圖基於 routes/openclaw_bot_routes.py 5999 行完整掃描產出。若實際 LoC 後續變動,需重新跑 grep 熱度統計(本文件 2.1 節)再修訂拆分邊界。*