# 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 節）再修訂拆分邊界。*
