[V10.4-A] 加強 commit-quality Hook + P9 文件歸檔

新增 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>
This commit is contained in:
ogt
2026-04-25 01:42:40 +08:00
parent e9b2dabffd
commit fcac03379d
9 changed files with 1218 additions and 53 deletions

View File

@@ -0,0 +1,282 @@
# 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` | requestsNIM`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_answerAI 入口)** | `openclaw_answer` | requestsGemini/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_webhook354 行)** | 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 常數 + envA 區)— 任何層皆可 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 2query和 Layer 4openpyxl已是跨層。抽分時建議將其放 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-exceptoptional |
| `services.pchome_crawler` | 比價 | try-exceptoptional |
| `services.ppt_generator`(延遲) | PPT | 在 `_generate_ppt_cmd` 內部 import |
---
## 4. 拆分提案(修正版)
原 critic 6 檔方案大方向正確,但**經實際依賴分析**需要兩處修正:
1. **Excel 匯入L 區)** 被遺漏,應獨立為 `services/openclaw_excel.py`Excel 匯入 + Excel 匯出 = 一檔)。
2. **Gemini Function CallingT+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` 不得互相 importcommands 單向 depend on queries
- [ ] 監控 3 日生產驗證後才視為完成
---
*本地圖基於 routes/openclaw_bot_routes.py 5999 行完整掃描產出。若實際 LoC 後續變動,需重新跑 grep 熱度統計(本文件 2.1 節)再修訂拆分邊界。*