新增 Edit/Write/MultiEdit 事件攔截(原僅攔截 git commit Bash 指令), 補齊 getenv fallback 模式偵測,防止 hardcoded Token 透過工具直寫入檔案。 - .claude/hooks/commit-quality.js: 改寫為 PreToolUse JSON 格式,覆蓋 Edit/Write/MultiEdit - .claude/settings.json: 新增 Edit|Write|MultiEdit|Bash matcher 註冊 - .claude/hooks/__test__/commit-quality.test.sh: 4 case 自動化測試 - docs/guides/DISK_EXPANSION_GUIDE.md: 磁碟擴充 SOP 歸檔 - docs/p9_completion_report_*.md: P9-1 + P9-2 Sprint 完成報告 - docs/refactor/callback_prefix_proposal.md: 308 按鈕回呼前綴分析(Method C) - docs/refactor/openclaw_bot_routes_split_plan.md: 5999 行神檔拆分計畫 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
23 KiB
23 KiB
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 本檔
$ 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 檔方案大方向正確,但經實際依賴分析需要兩處修正:
- Excel 匯入(L 區) 被遺漏,應獨立為
services/openclaw_excel.py(Excel 匯入 + Excel 匯出 = 一檔)。 - Gemini Function Calling(T+U 區) 依賴太多
query_*,且是 webhook 訊息分支第二層入口,應獨立為services/openclaw_nlu.py(非 commands)。 services/openclaw_menus.py目前 265 行偏小但高度自成一體,可採納。- 排程任務(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 不可忽略的坑
generate_daily_pdf其實是 Excel(F 區),真 PDF 只是函數名。重構時改名generate_daily_excel_report為佳;注意send_daily_excel排程會呼叫它。handle_cmd會遞迴呼叫自己(webhook await flow →handle_cmd('sales', ...)、圖片 Vision →handle_cmd('competitor', ...))—拆到 commands.py 時要注意不要同時在 webhook.py 也留副本。_ppt_ai_analysis在_generate_ppt_cmd被呼叫 10 次,兩者必須同檔(openclaw_ppt.py)不可分離。_seen_update_*雙結構去重(ADR-類修補):deque + set 互為見證,拆檔時兩個必須綁定,不能只搬一個。- Blueprint
record_oncehook 觸發start_scheduler(5995-5998)—scheduler 遷移後記得 hook 仍要呼叫新位置。 import calendar as _cal、from datetime import date as _date散落在handle_cmd內部(4723-4724)—搬遷時要一起帶走,否則 UnboundLocalError。- Gemini Vision 圖片分支(5694-5752, 58 行)目前 inline 在
telegram_webhook裡,抽成 function 後既減負擔又可單測 — 建議 Phase A1 的時候一起做。 _on_registerblueprint hook(Y 區)只有 4 行但是整個排程啟動的唯一觸發點 — webhook.py 必須保留此 hook 對應新的start_schedulerimport。
6. 最終交付檢核單(給接手 refactor-specialist)
app.py:683可以不變(from routes.openclaw_bot_routes import openclaw_bot_bp仍有效:可在routes/openclaw_bot_routes.py保留 compat shimfrom routes.openclaw_webhook import openclaw_bot_bp1 行)- 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 節)再修訂拆分邊界。