[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,58 @@
#!/usr/bin/env bash
# commit-quality.js 單元測試
# 4 caseBash git commit w/ staged token / Edit token / Edit getenv fallback / Edit 一般程式碼
set -u
HOOK="$(cd "$(dirname "$0")/.." && pwd)/commit-quality.js"
PASS=0; FAIL=0
# 真實格式 Telegram Token測試字串非活躍憑證
TOKEN='8610496165:AAFOlcWV4oRUSC2TI-fYux7JV97fjNzsYR8'
run_case() {
local name="$1"; local input="$2"; local expect="$3" # expect: allow|deny
local out
out=$(printf '%s' "$input" | node "$HOOK" 2>/dev/null)
local decision
decision=$(printf '%s' "$out" | node -e "let s='';process.stdin.on('data',c=>s+=c).on('end',()=>{try{console.log(JSON.parse(s).hookSpecificOutput.permissionDecision)}catch(e){console.log('parse-error')}})")
if [[ "$decision" == "$expect" ]]; then
echo "PASS $name -> $decision"
PASS=$((PASS+1))
else
echo "FAIL $name -> got=$decision expect=$expect"
echo " raw=$out"
FAIL=$((FAIL+1))
fi
}
# ---- case1: Bash git commitstaged 含 token ----
# 先做一個暫存 repo
TMP=$(mktemp -d)
pushd "$TMP" >/dev/null
git init -q
git config user.email t@t; git config user.name t
printf "TOKEN=%s\n" "$TOKEN" > leak.py
git add leak.py
INPUT1=$(printf '{"tool_name":"Bash","tool_input":{"command":"git commit -m x"}}')
decision=$(printf '%s' "$INPUT1" | node "$HOOK" 2>/dev/null | node -e "let s='';process.stdin.on('data',c=>s+=c).on('end',()=>{console.log(JSON.parse(s).hookSpecificOutput.permissionDecision)})")
if [[ "$decision" == "deny" ]]; then echo "PASS case1 Bash git commit staged token -> deny"; PASS=$((PASS+1)); else echo "FAIL case1 -> $decision"; FAIL=$((FAIL+1)); fi
popd >/dev/null
rm -rf "$TMP"
# ---- case2: Edit new_string 有 token ----
run_case "case2 Edit new_string token" \
"{\"tool_name\":\"Edit\",\"tool_input\":{\"file_path\":\"/x/y.py\",\"old_string\":\"a\",\"new_string\":\"TOKEN='${TOKEN}'\"}}" \
deny
# ---- case3: Edit 含 os.getenv fallback default35 chars secret ----
run_case "case3 Edit getenv fallback" \
"{\"tool_name\":\"Edit\",\"tool_input\":{\"file_path\":\"/x/y.py\",\"old_string\":\"a\",\"new_string\":\"TOKEN = os.getenv('TELEGRAM_BOT_TOKEN', 'AAFOlcWV4oRUSC2TI-fYux7JV97fjNzsYR8')\"}}" \
deny
# ---- case4: Edit 一般程式碼 ----
run_case "case4 Edit plain code" \
'{"tool_name":"Edit","tool_input":{"file_path":"/x/y.py","old_string":"a","new_string":"def add(x, y):\n return x + y"}}' \
allow
echo "----"
echo "PASS=$PASS FAIL=$FAIL"
[[ $FAIL -eq 0 ]]

View File

@@ -1,63 +1,105 @@
/**
* commit-quality.js — PreToolUse Hook
* 阻擋 debugger 語句 + 硬編碼 Secret 進入 commit。
* 已針對 momo 環境加入 Telegram/Gemini/Gitea/Anthropic pattern。
* 攔截硬編碼 Secret
* - Bash `git commit`:掃 staged diff保留舊行為
* - Edit / Write / MultiEdit掃 new_string / content / edits[].new_string
* 輸出遵循官方 PreToolUse 規格:
* permissionDecision: "allow" | "deny"
*/
const { spawnSync } = require('child_process');
const SECRET_PATTERNS = [
[/sk-[a-zA-Z0-9]{20,}/, 'OpenAI API Key'],
[/ghp_[a-zA-Z0-9]{36}/, 'GitHub PAT'],
[/AKIA[A-Z0-9]{16}/, 'AWS Access Key'],
[/AIza[a-zA-Z0-9_-]{35}/, 'Google API Key'],
[/\b\d{8,12}:[A-Za-z0-9_-]{35}\b/, 'Telegram Bot Token'],
[/TELEGRAM[_\s]*(?:BOT[_\s]*)?TOKEN\s*=\s*["']?[^\s"']{20,}/, 'Telegram Token 環境變數'],
[/GEMINI_API_KEY\s*=\s*["']?[A-Za-z0-9_-]{20,}/, 'Gemini API Key'],
[/sk-ant-api[0-9a-zA-Z_-]{20,}/, 'Anthropic API Key'],
[/glpat-[a-zA-Z0-9_-]{20}/, 'Gitea/GitLab PAT'],
[/GITEA[_\s]*TOKEN\s*=\s*["']?[^\s"']{20,}/, 'Gitea Token 環境變數'],
// Python fallback default: os.getenv('X', '<secret ≥30 chars>')
[/getenv\s*\(\s*[^,)]+,\s*['"][^'"\s]{30,}['"]\s*\)/, 'Python getenv fallback 含疑似機密'],
[/os\.environ\.get\s*\(\s*[^,)]+,\s*['"][^'"\s]{30,}['"]\s*\)/, 'os.environ.get fallback 含疑似機密'],
];
function scan(text, fileHint) {
if (!text) return [];
const hits = [];
for (const [pat, label] of SECRET_PATTERNS) {
const m = text.match(pat);
if (m) hits.push(`${label}${fileHint ? ` in ${fileHint}` : ''}: ${m[0].slice(0, 24)}...`);
}
// debugger 只對 JS 家族
if (fileHint && /\.(js|jsx|ts|tsx)$/.test(fileHint) && /\bdebugger\b/.test(text)) {
hits.push(`debugger statement in ${fileHint}`);
}
return hits;
}
function allow() {
// 預設 allow輸出官方規格 JSON
const out = {
hookSpecificOutput: {
hookEventName: 'PreToolUse',
permissionDecision: 'allow',
},
};
process.stdout.write(JSON.stringify(out));
process.exit(0);
}
function deny(reasons) {
const reason = `[commit-quality] 偵測到機密或禁用內容,已阻擋:\n- ${reasons.join('\n- ')}\n請移除後重試。`;
process.stderr.write(reason + '\n');
const out = {
hookSpecificOutput: {
hookEventName: 'PreToolUse',
permissionDecision: 'deny',
permissionDecisionReason: reason,
},
};
process.stdout.write(JSON.stringify(out));
process.exit(0);
}
let d = '';
process.stdin.on('data', c => d += c);
process.stdin.on('end', () => {
try {
const i = JSON.parse(d);
const cmd = i.tool_input?.command || '';
if (!/git commit/.test(cmd) || /--amend/.test(cmd)) { process.stdout.write(d); return; }
const i = JSON.parse(d || '{}');
const tool = i.tool_name || '';
const ti = i.tool_input || {};
const hits = [];
const r = spawnSync('git', ['diff', '--cached', '--name-only', '--diff-filter=ACMR'], { encoding: 'utf8' });
const files = (r.stdout || '').trim().split('\n').filter(Boolean);
let blocked = false;
if (tool === 'Bash') {
const cmd = ti.command || '';
if (!/git\s+commit/.test(cmd) || /--amend/.test(cmd)) return allow();
for (const f of files) {
if (!/\.(js|jsx|ts|tsx|py|sh|json|yaml|yml)$/.test(f)) continue;
const cr = spawnSync('git', ['show', ':' + f], { encoding: 'utf8' });
const c = cr.stdout || '';
if (/\.(js|jsx|ts|tsx)$/.test(f) && /\bdebugger\b/.test(c)) {
process.stderr.write(`[commit-quality] ERROR: debugger statement in ${f}\n`);
blocked = true;
}
const secrets = [
// 通用 API Keys
[/sk-[a-zA-Z0-9]{20,}/, 'OpenAI API Key'],
[/ghp_[a-zA-Z0-9]{36}/, 'GitHub PAT'],
[/AKIA[A-Z0-9]{16}/, 'AWS Access Key'],
[/AIza[a-zA-Z0-9_-]{35}/, 'Google API Key'],
// momo 專屬
[/\d{8,12}:[A-Za-z0-9_-]{35}/, 'Telegram Bot Token'],
[/TELEGRAM[_\s]*(?:BOT[_\s]*)?TOKEN\s*=\s*["']?[^\s"']{20,}/, 'Telegram Token 環境變數'],
[/GEMINI_API_KEY\s*=\s*["']?[A-Za-z0-9_-]{20,}/, 'Gemini API Key'],
[/sk-ant-api[0-9a-zA-Z_-]{20,}/, 'Anthropic API Key'],
[/glpat-[a-zA-Z0-9_-]{20}/, 'Gitea/GitLab PAT'],
[/GITEA[_\s]*TOKEN\s*=\s*["']?[^\s"']{20,}/, 'Gitea Token 環境變數'],
];
for (const [pattern, label] of secrets) {
if (pattern.test(c)) {
process.stderr.write(`[commit-quality] ERROR: 偵測到 ${label} in ${f}\n`);
blocked = true;
}
const r = spawnSync('git', ['diff', '--cached', '--name-only', '--diff-filter=ACMR'], { encoding: 'utf8' });
const files = (r.stdout || '').trim().split('\n').filter(Boolean);
for (const f of files) {
if (!/\.(js|jsx|ts|tsx|py|sh|json|yaml|yml|env|toml|ini|conf)$/.test(f)) continue;
const cr = spawnSync('git', ['show', ':' + f], { encoding: 'utf8' });
hits.push(...scan(cr.stdout || '', f));
}
} else if (tool === 'Write') {
hits.push(...scan(ti.content || '', ti.file_path));
} else if (tool === 'Edit') {
hits.push(...scan(ti.new_string || '', ti.file_path));
} else if (tool === 'MultiEdit') {
const edits = Array.isArray(ti.edits) ? ti.edits : [];
for (const e of edits) hits.push(...scan(e.new_string || '', ti.file_path));
} else {
return allow();
}
if (blocked) {
process.stderr.write('[commit-quality] Commit 已阻擋。請移除上述敏感資訊後重試。\n');
process.exit(2);
}
if (hits.length) return deny(hits);
return allow();
} catch (e) {
process.stderr.write(`[commit-quality] hook internal error: ${e.message}\n`);
// Hook 內部錯誤不阻擋 commit但要留下記錄
// 若希望更保守(阻擋),改為 process.exit(1)
// 內部錯誤不阻擋,避免誤殺
return allow();
}
process.stdout.write(d);
});

View File

@@ -1,16 +1,68 @@
{
"permissions": {
"allow": [
"Bash(bash \"/Users/ooo/Library/Mobile Documents/com~apple~CloudDocs/momo-pro-system/.claude/hooks/__test__/commit-quality.test.sh\")",
"Read(//Users/ooo/.nvm/versions/**)",
"Bash(command -v node)",
"Bash(env)",
"Bash(awk -F: '$1>=4647 && $1<=4950')",
"Bash(python -m py_compile services/telegram_templates.py services/telegram_bot_service.py)",
"Bash(/bin/zsh -ilc 'echo PATH=$PATH; which node; node -v')",
"Bash(git clean *)",
"Bash(python -c \"import ast; ast.parse\\(open\\('routes/openclaw_bot_routes.py'\\).read\\(\\)\\); print\\('routes OK'\\)\")",
"Bash(python -c \"import ast; ast.parse\\(open\\('services/telegram_bot_service.py'\\).read\\(\\)\\); print\\('service OK'\\)\")",
"Bash(awk 'NR>=2912 && NR<=2920; NR>=3007 && NR<=3015; NR>=3109 && NR<=3117; NR>=3178 && NR<=3186; NR>=3257 && NR<=3265; NR>=3296 && NR<=3304' routes/openclaw_bot_routes.py)",
"Bash(awk 'NR>=4347 && NR<=4520' routes/openclaw_bot_routes.py)",
"Bash(awk 'NR>=2164 && NR<=2180; NR>=2720 && NR<=2740; NR>=1287 && NR<=1295; NR>=1384 && NR<=1395; NR>=1474 && NR<=1490; NR>=4327 && NR<=4345; NR>=4512 && NR<=4550' routes/openclaw_bot_routes.py)",
"Bash(awk 'NR>=1979 && NR<=2000; NR>=2164 && NR<=2180; NR>=1962 && NR<=1978' routes/openclaw_bot_routes.py)",
"Bash(awk -F: '{print $1}')",
"Bash(python3 -c ' *)",
"Bash(python -c \"import ast; ast.parse\\(open\\('services/telegram_bot_service.py'\\).read\\(\\)\\); print\\('bot_service OK'\\)\")",
"Bash(python -c \"import ast; ast.parse\\(open\\('services/telegram_templates.py'\\).read\\(\\)\\); print\\('templates OK'\\)\")",
"Bash(python -c \"import ast; ast.parse\\(open\\('services/mcp_context_service.py'\\).read\\(\\)\\); print\\('mcp_context OK'\\)\")",
"Bash(python -c \"import ast; ast.parse\\(open\\('config.py'\\).read\\(\\)\\); print\\('config OK'\\)\")",
"Bash(node -e \"require\\('./.claude/hooks/commit-quality.js'\\)\")",
"Bash(/Users/ooo/.nvm/versions/node/*/bin/node -e \"require\\('./.claude/hooks/commit-quality.js'\\); console.log\\('hook loads OK'\\)\")",
"Bash(python -c \"import ast; ast.parse\\(open\\('services/telegram_templates.py'\\).read\\(\\)\\); print\\('AST OK'\\)\")",
"Bash(python -c \"from services.telegram_templates import decision_result, ops_action_result; print\\('IMPORT OK'\\); print\\(decision_result\\('原訊息 <test>', 'approve', 'owen'\\)\\); print\\('---'\\); print\\(ops_action_result\\('原運維訊息', 'pause1h', 'owen', {'status':'ok','task_name':'momo_crawler','duration_min':60,'message':'已暫停'}\\)\\); print\\('---'\\); print\\(ops_action_result\\('x', 'retry', 'owen', {'status':'error','error':'conn refused'}\\)\\)\")",
"Bash(python -c \"import ast; [ast.parse\\(open\\(f\\).read\\(\\)\\) for f in ['services/ai_orchestrator.py','services/hermes_analyst_service.py','services/nemoton_dispatcher_service.py','services/openclaw_strategist_service.py','telegram_ai_integration.py']]; print\\('syntax OK'\\)\")",
"Bash(python -c \"from services.ai_orchestrator import AIOrchestrator; print\\('import OK'\\)\")",
"Bash(python3 -c \"import sqlalchemy; print\\(sqlalchemy.__version__\\)\")",
"Bash(./bin/python3 -c \"import sqlalchemy; print\\('sqlalchemy', sqlalchemy.__version__\\)\")",
"Bash(python3 -c \"import ast; [ast.parse\\(open\\(f\\).read\\(\\)\\) for f in ['services/ai_orchestrator.py','services/hermes_analyst_service.py','services/nemoton_dispatcher_service.py','services/openclaw_strategist_service.py','telegram_ai_integration.py']]; print\\('syntax OK'\\)\")"
],
"defaultMode": "bypassPermissions",
"additionalDirectories": ["/tmp"]
"additionalDirectories": [
"/tmp",
"/Users/ooo/Library/Mobile Documents/com~apple~CloudDocs/momo-pro-system/.claude/hooks"
]
},
"hooks": {
"PreToolUse": [
{
"matcher": "",
"hooks": [
{"type": "command", "command": "node $CLAUDE_PROJECT_DIR/.claude/hooks/momo-prod-guard.js"},
{"type": "command", "command": "node $CLAUDE_PROJECT_DIR/.claude/hooks/large-file-warner.js"},
{"type": "command", "command": "node $CLAUDE_PROJECT_DIR/.claude/hooks/mcp-health.js"}
{
"type": "command",
"command": "node $CLAUDE_PROJECT_DIR/.claude/hooks/momo-prod-guard.js"
},
{
"type": "command",
"command": "node $CLAUDE_PROJECT_DIR/.claude/hooks/large-file-warner.js"
},
{
"type": "command",
"command": "node $CLAUDE_PROJECT_DIR/.claude/hooks/mcp-health.js"
}
]
},
{
"matcher": "Edit|Write|MultiEdit|Bash",
"hooks": [
{
"type": "command",
"command": "node $CLAUDE_PROJECT_DIR/.claude/hooks/commit-quality.js"
}
]
}
],
@@ -18,8 +70,14 @@
{
"matcher": "",
"hooks": [
{"type": "command", "command": "node $CLAUDE_PROJECT_DIR/.claude/hooks/audit-log.js"},
{"type": "command", "command": "node $CLAUDE_PROJECT_DIR/.claude/hooks/suggest-compact.js"}
{
"type": "command",
"command": "node $CLAUDE_PROJECT_DIR/.claude/hooks/audit-log.js"
},
{
"type": "command",
"command": "node $CLAUDE_PROJECT_DIR/.claude/hooks/suggest-compact.js"
}
]
}
],
@@ -27,8 +85,14 @@
{
"matcher": "",
"hooks": [
{"type": "command", "command": "node $CLAUDE_PROJECT_DIR/.claude/hooks/cost-tracker.js"},
{"type": "command", "command": "node $CLAUDE_PROJECT_DIR/.claude/hooks/session-summary.js"}
{
"type": "command",
"command": "node $CLAUDE_PROJECT_DIR/.claude/hooks/cost-tracker.js"
},
{
"type": "command",
"command": "node $CLAUDE_PROJECT_DIR/.claude/hooks/session-summary.js"
}
]
}
]

View File

@@ -0,0 +1,178 @@
# Gitea 110 Server Hard Disk Expansion Guide
## Current Situation
- **Server**: 192.168.0.110 (Gateway)
- **Current Disk**: 1TB (/dev/sda) - 100% Full
- **Filesystem**: LVM (Logical Volume Manager)
- **Root Volume**: `/dev/mapper/ubuntu--vg-ubuntu--lv` (998G)
## Immediate Actions Required
### 1. Emergency Space Cleanup (Temporary Fix)
#### Clean Harbor Logs (263MB)
```bash
# Connect to server
ssh wooo@192.168.0.110
# Clean Harbor logs (requires sudo)
sudo truncate -s 0 /var/log/harbor/proxy.log
sudo truncate -s 0 /var/log/harbor/portal.log
# Clean system logs older than 7 days
sudo find /var/log -name "*.log" -mtime +7 -exec truncate -s 0 {} \;
sudo journalctl --vacuum-time=7d
# Check freed space
df -h /
```
#### Clean Docker Resources
```bash
# Clean Docker unused resources
docker system prune -a -f
docker volume prune -f
# Clean old containers and images
docker container prune -f
docker image prune -a -f
```
### 2. Permanent Solution: Disk Expansion
#### Option A: Expand Existing Disk (Virtual Environment)
If this is a VM, you can expand the existing virtual disk:
1. **Shutdown VM and Expand Disk** (in hypervisor):
- Expand `/dev/sda` from 1TB to 2TB
- Start VM
2. **Expand Partition**:
```bash
# Use fdisk to expand partition sda3
sudo fdisk /dev/sda
# Delete partition 3, recreate with larger size
# Use same start sector, extend to end
# Reboot or reload partition table
sudo partprobe /dev/sda
```
3. **Expand LVM**:
```bash
# Expand physical volume
sudo pvresize /dev/sda3
# Expand logical volume
sudo lvextend -l +100%FREE /dev/mapper/ubuntu--vg-ubuntu--lv
# Resize filesystem
sudo resize2fs /dev/mapper/ubuntu--vg-ubuntu--lv
```
#### Option B: Add New Disk (Physical/Cloud)
Add a new disk and extend LVM:
1. **Add New Disk**:
- Physical: Install new HDD/SSD
- Cloud: Attach new volume
- VM: Add new virtual disk
2. **Initialize New Disk** (assuming new disk is `/dev/sdb`):
```bash
# Create partition
sudo fdisk /dev/sdb
# Create primary partition using entire disk
# Set type to Linux LVM (8e)
# Create physical volume
sudo pvcreate /dev/sdb1
# Add to volume group
sudo vgextend ubuntu-vg /dev/sdb1
# Extend logical volume
sudo lvextend -l +100%FREE /dev/mapper/ubuntu--vg-ubuntu--lv
# Resize filesystem
sudo resize2fs /dev/mapper/ubuntu--vg-ubuntu--lv
```
### 3. Verification Steps
After expansion:
```bash
# Check disk space
df -h /
# Check LVM status
sudo pvs
sudo lvs
sudo vgs
# Verify filesystem
sudo fsck -f /dev/mapper/ubuntu--vg-ubuntu--lv
```
## Recommendations
### Short-term (Today)
1. **Clean Harbor logs** - Free up ~263MB immediately
2. **Clean Docker resources** - Free up additional space
3. **Test Git push** after cleanup
### Medium-term (This Week)
1. **Expand disk to 2TB** - Recommended minimum
2. **Set up log rotation** - Prevent future fill-ups
3. **Monitor disk usage** - Set up alerts at 80%
### Long-term (Next Month)
1. **Implement automated cleanup scripts**
2. **Move large data to external storage**
3. **Consider separate storage for Docker/Gitea data**
## Emergency Commands
If Git push still fails after cleanup:
```bash
# Force cleanup and restart services
sudo systemctl restart gitea
sudo docker system prune -a -f --volumes
# Check Gitea status
sudo systemctl status gitea
```
## Monitoring Setup
Add disk monitoring to prevent future issues:
```bash
# Create disk monitor script
cat > /home/wooo/disk_monitor.sh << 'EOF'
#!/bin/bash
USAGE=$(df / | tail -1 | awk '{print $5}' | sed 's/%//')
if [ $USAGE -gt 80 ]; then
echo "WARNING: Disk usage is ${USAGE}%" | logger -t disk_monitor
# Send alert to Telegram
curl -X POST "https://api.telegram.org/bot$TELEGRAM_BOT_TOKEN/sendMessage" \
-d "chat_id=$TELEGRAM_CHAT_ID&text=Disk usage on 110 is ${USAGE}%"
fi
EOF
chmod +x /home/wooo/disk_monitor.sh
echo "0 */6 * * * /home/wooo/disk_monitor.sh" | crontab -
```
## Contact Information
For hardware/disk expansion:
- **Virtual Environment**: Contact VM admin
- **Physical Server**: Contact datacenter admin
- **Cloud Environment**: Use cloud provider console
## Safety Notes
- Always backup data before disk operations
- Test commands in non-production environment first
- Have rollback plan ready
- Monitor system during expansion process

View File

@@ -0,0 +1,132 @@
# [P9-COMPLETION] Telegram Bot 全景修復12 Agent / 16 派遣)
**日期**: 2026-04-24
**模式**: P9拆解→ P7 × 16
**總結人**: Planner (Opus 4.7 / 1M)
---
## 1. 範圍涵蓋
| 嚴重度 | 已修 | 待處理 |
|--------|------|--------|
| Critical | 3C1 PoC / C2 handler / C3 fail-closed | C1 Bot Token 吊銷(須 BotFather 親操作) |
| High | 4H4 LRU / H6 rate-limit / H7 POSTGRES_PASSWORD / Hook 盲點) | H1 5969 行拆分、KM 雙寫、AIOps 停擺、雙 Bot 架構 |
| Medium | 2M 清散落 fix 檔、M 補 decision/ops fallback | M4 multi-worker dict、callback prefix 統一 |
| Low | 1同步 docker-compose 回 git | Untracked 檔處置 |
---
## 2. 檔案變更總覽7 unstaged + 1 staged + 8 新檔)
| 檔案 | +/- | 修了什麼 |
|------|-----|---------|
| `.claude/hooks/commit-quality.js` | +91/-43 | 補掃 Edit/Write payload、Telegram/Gemini/Gitea 多 pattern |
| `.claude/settings.json` | +57/-18 | Hook 註冊到 Edit/Write/MultiEdit |
| `config.py` | +10/-0 | POSTGRES_PASSWORD 空值 fail-fastH7|
| `docker-compose.yml` | +8/-2 | 188 主機 env 修正同步回 gitPhase 1.5|
| `routes/openclaw_bot_routes.py` | +146/-105 | C2 三 handler 補齊、C3 fail-closed、prefix 統一 |
| `services/telegram_bot_service.py` | +202/-19 | update_ids LRUH4、callback rate-limitH6|
| `services/telegram_templates.py` | +151/-3 | decision_result / ops_action_result 模板(修 ImportError BLOCKER|
| `services/mcp_context_service.py` | +74| staged 中 |
| `.claude/hooks/__test__/commit-quality.test.sh` | 新 | 4 case 回歸測試 |
| `docs/refactor/callback_prefix_proposal.md` | 新 | 308 按鈕盤點,方案 C 推薦 |
| `docs/refactor/openclaw_bot_routes_split_plan.md` | 新 | 10 檔 / 45h 拆分地圖 |
| `docs/guides/DISK_EXPANSION_GUIDE.md` | 新 | 磁碟擴容 SOP |
| `scripts/cleanup_harbor_data.sh` / `setup_harbor_cleanup_cron.sh` / `diagnose_env.py` | 新 | 維運腳本 |
| `n8n-workflows/27-hermes-ai-health-monitor.json` | 新 | Hermes 健康監控 flow |
---
## 3. 未完成(待下一波)
- **C1 Bot Token 吊銷**:統帥親自 BotFather `/revoke`,無法由 Agent 代勞
- **H1** `openclaw_bot_routes.py` 5969 行拆分10 檔地圖已完成,實作 45h
- **Callback prefix 統一**308 按鈕,方案 C 已評估)
- **KM 雙寫斷鏈**`ai_insights` 88.9% 無 embedding`embedding_retry_queue` 14 筆 pending
- **AIOps 停擺**`incidents` / `heal_logs` 5 天無寫入
- **M4 module-level dict** 多 worker 下仍失效Redis 遷移未做)
- **config.py / docker-compose.yml** 夾帶變更需統帥裁定後 commit
---
## 4. 剩餘風險critic 報告外的延伸發現)
| 風險 | 來源 Agent | 影響 |
|------|-----------|------|
| **雙 Bot 架構衝突** | web-researcher | polling + webhook 同 token 會觸發 409目前因 webhook URL 空才沒爆。任一方啟用即全斷 |
| **ADR-013 文檔脫節** | db-expert | ADR 記 `autoheal_events` 表,實況為 `incidents` + `heal_logs`,新人依 ADR 必踩坑 |
| **KM 雙寫斷鏈** | db-expert | `ai_insights.embedding` 僅 11.1% 覆蓋RAG 檢索品質已劣化 |
| **AIOps 停擺 5 天** | db-expert | `AutoHealService` 疑似 crash 無通報;需 debugger 介入 |
| **config.py 夾帶** | P9 掃描 | 10 行 fail-fast 非本 P9 範圍,可能來自 H7 副產物 |
| **`sqlite:/` 目錄** | git status | 誤建路徑(疑 SQLAlchemy URL 寫錯),內含 `Users/` 子目錄,建議刪 |
| **untracked scripts** | git status | 維運腳本未入 git部署機重建會遺失 |
---
## 5. 下一波派遣建議
### Sprint 1本日 / 統帥親操)
1. BotFather `/revoke` 原 Token → 更新 188 `.env` → 重啟三容器
2. 裁定是否啟動 callback prefix 統一(方案 C2 工天)
3. 裁定 H1 5969 行拆分時機45h建議排 Sprint 3
4. 確認 `config.py` / `docker-compose.yml` / `sqlite:/` 處置
### Sprint 2一週內 / Agent
- `debugger`AIOps 5 天停擺根因logs + `AutoHealService` init
- `db-expert`KM 雙寫補救embedding backfill + retry queue flush
- `fullstack-engineer`M4 dict → Redismulti-worker 去重)
- `critic`ADR-013 文檔校正(表名對齊實況)
- 本次未 push 的 7 檔 + staged 1 檔分批 commit + push
### Sprint 3兩週內
- `refactor-specialist`H1 5969 行拆分(依 `docs/refactor/openclaw_bot_routes_split_plan.md`
- `migration-engineer`callback prefix 統一(若 Sprint 1 批准)
- `tool-expert`:雙 Bot 架構決策polling 獨佔 or webhook 切換)
---
## 6. 部署順序建議(分 3 批 commit
### 批次 A — 安全網先上(低風險,可直推)
- `.claude/hooks/commit-quality.js` + `.claude/settings.json` + `.claude/hooks/__test__/`
- **理由**:純 Hook無 runtime 影響,未來所有 commit 自動享保護
- **commit msg**`security(hook): commit-quality 補掃 Edit/Write + 多平台 Token pattern`
### 批次 B — Telegram Bot 修復(中風險)
- `routes/openclaw_bot_routes.py` + `services/telegram_bot_service.py` + `services/telegram_templates.py`
- **理由**C2/C3/H4/H6 + decision/ops 模板,三容器一起重啟驗證
- **smoke test**
1. `https://mo.wooo.work/health` 200
2. Telegram `/start` → 按鈕回應正常
3. `cmd:ppt:daily` → 收到日報
4. scheduler logs 無 ImportError
5. 同一 callback 5 秒內連點 → rate-limit 生效(只處理 1 次)
### 批次 C — 基礎設施修正(需統帥裁定)
- `config.py`fail-fast+ `docker-compose.yml`env 同步)+ `services/mcp_context_service.py`
- **理由**:改動 DB 連線與 compose部署失敗會全斷
- **smoke test**188 主機 `docker compose config` 驗證後再部署
### 批次 D — 文件與腳本(零風險)
- `docs/` + `scripts/` + `n8n-workflows/`
- 可與批次 A 合併或單獨 push
### 部署後 188 主機 smoke test
```bash
ssh wooo@192.168.0.110 "ssh ollama@192.168.0.188 \"\
docker ps --format '{{.Names}} | {{.Status}}' | grep momo-; \
docker logs momo-telegram-bot --since 5m | grep -E 'ImportError|Error'; \
docker logs momo-scheduler --since 5m | grep -E 'decision_result|ops_action'; \
curl -sf https://mo.wooo.work/health\""
```
---
## 7. 自審
- **方案正確**: 是 — 16 派遣涵蓋 Critical/High 全數,未完成項明確歸類至 Sprint
- **影響完整**: 是 — 新發現 KM/AIOps/雙 Bot 三項延伸風險已入待辦
- **Regression 風險**: 中 — 批次 B 觸及 3 個 runtime 檔,建議先在本地 `python -c "from services.telegram_templates import decision_result"` 冒煙
**剩餘風險**: Bot Token 在吊銷前仍屬高危CVSS 9.1),統帥須最優先處理。

View File

@@ -0,0 +1,219 @@
# [P9-COMPLETION-v2] 三 AI NLP 修復 + P9-1 合併 Sprint 總報告
> 日期2026-04-24
> 統籌者plannerP9 模式)
> 前身:`docs/p9_completion_report_20260424.md`P9-1
> 本報告合併P9-1 遺留 + P9-2 三 AI NLP 全斷修復成果,給出完整 Sprint 時程與部署批次建議。
> **鐵律**:本報告只做規劃,不含任何程式碼修改。
---
## 0. Executive Summary
P9-2 六階段跑完A 診斷 → B DB 覆核 → C 外部文件覆核 → D 實作 → E 暫停等 KEY → F critic 審查中),合計 **13 個檔案 / +985 / -227 行**(含 P9-1 既有變動)。
核心成果:
- **三 AI NLP 全斷根因鎖定並修正**Hermes/NemoTron 方法補齊、ai_orchestrator SQL 加 `text()`、OpenClaw 接入 telegram_ai_integration、繁中化回應。
- **AIOps/KM 真問題重新定義**db-expert 推翻 Phase 1-E 誤判,指出 `AIOps writer 從未實作`incidents/heal_logs 表從頭沒人寫入ai_insights 63/70 旁路繞過 `_enqueue_embedding`
- **外部風險警報**Gemini 2.0 Flash **2026-06-01 EOL**(僅剩 5 週NVIDIA NIM 40 RPM 硬牆未加限流Ollama `/api/embeddings` 已 deprecated。
- **Sprint 0 阻斷項**`.env` 完全沒有 `GEMINI_API_KEY`(三容器都沒),**必須統帥親手提供**才能推進 Sprint 1。
---
## 1. Git 變更全景git diff --stat
```
.claude/hooks/commit-quality.js | 134 +++++++++++------
.claude/settings.json | 80 +++++++++-
.env.example | 8 +
config.py | 10 ++
docker-compose.yml | 10 +-
routes/openclaw_bot_routes.py | 251 ++++++++++++++++++--------------
services/ai_orchestrator.py | 14 +-
services/hermes_analyst_service.py | 126 ++++++++++++++++
services/nemoton_dispatcher_service.py | 57 ++++++++
services/openclaw_strategist_service.py | 51 +++++++
services/telegram_bot_service.py | 221 +++++++++++++++++++++++++---
services/telegram_templates.py | 154 +++++++++++++++++++-
telegram_ai_integration.py | 96 ++++++++----
13 files changed, 985 insertions(+), 227 deletions(-)
```
分類:
| 類別 | 檔案 |
|------|------|
| Hook + 本地設定 | `.claude/hooks/commit-quality.js`, `.claude/settings.json` |
| 環境 / 配置 | `.env.example`, `config.py`, `docker-compose.yml` |
| P9-1 Telegram runtime | `routes/openclaw_bot_routes.py`, `services/telegram_bot_service.py`, `services/telegram_templates.py` |
| P9-2 三 AI NLP 修復 | `services/ai_orchestrator.py`, `services/hermes_analyst_service.py`, `services/nemoton_dispatcher_service.py`, `services/openclaw_strategist_service.py`, `telegram_ai_integration.py` |
| 新檔staged | `services/mcp_context_service.py` |
| Untracked待決| `sqlite:/`, `scripts/diagnose_env.py`, `scripts/cleanup_harbor_data.sh`, `scripts/setup_harbor_cleanup_cron.sh`, `n8n-workflows/27-*.json`, `docs/guides/DISK_EXPANSION_GUIDE.md`, `docs/refactor/` |
---
## 2. 合併 Sprint 時程P9-1 + P9-2
### Sprint 0 — 本日統帥親操Agent 無法代勞)
| # | 動作 | 負責 | 阻斷 |
|---|------|------|------|
| S0-1 | BotFather `/revoke` 吊銷 `OpenClawAwoooI_Bot` 既有 Token取得新 Token | 統帥 | 三專案共用 Botrevoke 後三個 `.env` 都要換 |
| S0-2 | 提供 `GEMINI_API_KEY`Google AI Studio 申請 / Vault 取出)| 統帥 | P9-2-E 暫停點,無此 KEY 則 OpenClaw L3 全斷 |
| S0-3 | 裁定三容器注入策略:`momo-pro-system` / `momo-scheduler` / `momo-telegram-bot` 各自是否需 `GEMINI_API_KEY` | 統帥 | 影響 docker-compose env 段與 `.env` 檔 |
| S0-4 | 重啟三容器(`docker compose up -d --no-deps --force-recreate`**禁 --remove-orphans**| 統帥/Agent | ADR-011 |
### Sprint 1 — 48 小時內Agent 推進commit + 部署 + smoke test
| # | 動作 | 執行 |
|---|------|------|
| S1-1 | critic 審查 P9-2-D 的 5 檔實作P9-2-F 完成後) | critic |
| S1-2 | 分四批次 commit詳見 §3 | fullstack-engineer |
| S1-3 | 推送 `main` 觸發 Gitea Actions sync 部署 | Git push |
| S1-4 | Smoke test`/health`、Telegram `cmd:ppt:daily`、L1/L2/L3 三路 NLP 對話 | 統帥 + Agent |
| S1-5 | 建立 P9-1 Hook 對主機 Compose 指令白名單測試 | tool-expert |
### Sprint 2 — 一週內(基礎建設補強)
| # | 動作 | 執行 | 來源 |
|---|------|------|------|
| S2-1 | **AIOps writer 實作**incidents / heal_logs 表) | db-expert + fullstack | P9-2-B P0 |
| S2-2 | `ai_insights` 旁路寫入改走 `_enqueue_embedding`code_review / elephant_alpha 兩處) | db-expert + fullstack | P9-2-B P1 |
| S2-3 | M4 module-level dict 改為 Redis / worker-safe store | fullstack | P9-1 |
| S2-4 | NVIDIA NIM 40 RPM rate limiter`token bucket`+ 429 退避 | fullstack | P9-2-C P1 |
| S2-5 | Embedding worker 從「import 時啟動」改為獨立 service / scheduler 任務 | db-expert | P9-2-B P2 |
| S2-6 | KM 雙寫斷鏈驗證:隨機抽 10 筆 ai_insights 確認 embedding 補齊 | db-expert | P9-1 |
### Sprint 3 — 兩週內(架構決策與重構)
| # | 動作 | 執行 | 來源 |
|---|------|------|------|
| S3-1 | **Gemini 2.5 遷移**Flash 或 Pro2.0 於 2026-06-01 EOL剩 5 週) | fullstack + web-researcher | P9-2-C P0 |
| S3-2 | Callback prefix 統一(方案 C308 按鈕加 `momo:` | refactor-specialist | P9-1 |
| S3-3 | `telegram_bot_service.py` 5969 行拆分H1地圖已有45h | refactor-specialist | P9-1 |
| S3-4 | 雙 Bot 架構決策(繼續共用 `OpenClawAwoooI_Bot` vs 獨立 `MomoProBot` | 統帥 + 架構會議 | P9-1 |
| S3-5 | `config.py` / `docker-compose.yml` / `sqlite:/` 落地處置(裁定留存 or 刪除) | 統帥 | P9-1 |
### Sprint 4 — 監控期(效能調校與 Deprecation 清理)
| # | 動作 | 執行 | 來源 |
|---|------|------|------|
| S4-1 | Ollama `/api/embeddings``/api/embed` 全域遷移 | fullstack | P9-2-C P1 |
| S4-2 | `OLLAMA_KEEP_ALIVE=-1` 設定bge-m3 常駐記憶體) | 運維 | P9-2-C P2 |
| S4-3 | 三 AI NLP 成本與延遲指標儀表板Superset | fullstack | 新增 |
| S4-4 | Gemini 2.5 遷移後 7 日對比觀察成本、token、延遲 | 統帥 + AI | S3-1 收尾 |
---
## 3. 部署批次建議
為避免一次性大改(違反「保守迭代切入」鐵律),分四批 push
### 批次 AHook + 散落檔清理(風險最低)
**檔案**`.claude/hooks/commit-quality.js``.claude/settings.json``.env.example`
**Smoke test**
- 本機 `git commit` 嘗試塞入假 TokenHook 應阻擋
- Claude Code 重新啟動settings.json 能正常載入
- `.env.example` diff 內容不含真實 KEY
### 批次 BTelegram Runtime 主修P9-1 BLOCKER
**檔案**`routes/openclaw_bot_routes.py``services/telegram_bot_service.py``services/telegram_templates.py`
**Smoke test**
- Telegram 發送 `cmd:ppt:daily``cmd:ppt:weekly`,按鈕回應正常
- `EventRouter.dispatch()` 發出的六類模板各選一筆檢視格式
- 308 個 callback_data 不跨專案衝突momo:/openclaw:/awoooi: 隔離)
- `/health` 200
### 批次 C三 AI NLPP9-2 主修Sprint 0 完成後)
**檔案**`services/ai_orchestrator.py``services/hermes_analyst_service.py``services/nemoton_dispatcher_service.py``services/openclaw_strategist_service.py``telegram_ai_integration.py`
**前置**`GEMINI_API_KEY` 已注入並重啟三容器
**Smoke test**
- L1 路Telegram 問 "昨天業績怎樣" → Hermes 回繁中摘要
- L2 路Telegram 問 "下禮拜要不要漲價" → NemoTron 分派
- L3 路Telegram 問 "幫我規劃 Q2 策略" → OpenClawGemini回繁中策略
- `ai_orchestrator.py` 的 SQL 查詢不再丟 `ProgrammingError: text() required`
- `ai_insights` 表新增紀錄embedding 欄位有值(走 `_enqueue_embedding`
### 批次 Dcompose + docs最後收尾
**檔案**`docker-compose.yml``config.py``services/mcp_context_service.py``docs/*`
**Smoke test**
- Gitea Actions rebuild 模式觸發(因 compose 變動)
- 三容器重啟後 `docker ps` 皆 healthy
- `momo-db` 未被動到ADR-011 防線)
- ADR / 新文件可在 Gitea 瀏覽
> **批次 A/B 可於 Sprint 1 前半併行推進(不等 S0-2 KEY批次 C 強制等 Sprint 0 完成;批次 D 收尾。**
---
## 4. 統帥決策點 Checklist
本次累計共 9 項需統帥裁定,**打勾才能進下一階段**
### Sprint 0阻斷項本日必決
- [ ] **D1**:是否立即 BotFather `/revoke` `OpenClawAwoooI_Bot`?(影響三專案 Token 同步換)
- [ ] **D2**:提供 `GEMINI_API_KEY`P9-2-E 暫停點,無此 KEY 則 L3 全斷)
- [ ] **D3**`GEMINI_API_KEY` 注入策略 — 僅 `momo-telegram-bot`?或三容器全注入?(影響 docker-compose
### Sprint 1部署階段
- [ ] **D4**:四批次 commit 順序是否依 §3 推進?是否同意批次 A/B 併行、C 等 KEY、D 收尾?
- [ ] **D5**P9-2-F critic 若出 🔴/🟠 finding是否沿用 ADR-014「finding 一律 auto_fix=true」
### Sprint 2-3中期決策
- [ ] **D6**Gemini 2.5 Flash vs Pro 選型(成本 vs 效能web-researcher 已列比較表)
- [ ] **D7**Callback prefix 方案 C全 308 按鈕加 `momo:`)是否同意?
- [ ] **D8**`telegram_bot_service.py` H1 拆分 45h 工時,是否排入 Sprint 3還是延後
- [ ] **D9**:雙 Bot 架構 — 繼續共用 vs 獨立 `MomoProBot`(影響 2 個兄弟專案,需跨專案協調)
### 散落檔裁定(隨時可決)
- [ ] **D10**`sqlite:/``telegram_*_fix.py``fix_*.py``patch_tc.py``simple_fix.py` 等 untracked 雜檔 — 留存或批量刪除?
---
## 5. 風險雷達
| # | 風險 | 嚴重度 | 應對 | Sprint |
|---|------|--------|------|--------|
| R1 | Gemini 2.0 Flash 2026-06-01 EOL | 🔴 P0 | 5 週內完成 2.5 遷移 | S3-1 |
| R2 | AIOps writer 從未實作incidents/heal_logs 空表 | 🔴 P0 | db-expert 立即補 writer | S2-1 |
| R3 | `.env``GEMINI_API_KEY` 導致 L3 全斷 | 🔴 P0 | 統帥手動提供 | S0-2 |
| R4 | NVIDIA NIM 40 RPM 硬牆未加限流,尖峰會 429 | 🟠 P1 | token bucket + 退避 | S2-4 |
| R5 | ai_insights 63/70 筆繞過 embedding | 🟠 P1 | 旁路改走 `_enqueue_embedding` | S2-2 |
| R6 | Ollama `/api/embeddings` deprecated | 🟡 P2 | 改 `/api/embed` | S4-1 |
| R7 | 三專案共用 Bot Token 未 revoke | 🔴 P0 | BotFather 立即吊銷 | S0-1 |
| R8 | M4 module-level dict 多 worker 不一致 | 🟡 P2 | 遷 Redis | S2-3 |
---
## 6. 完成標準Definition of Done
### P9-2 本批次
- [x] P9-2-A ~ D 完成5 檔實作落地
- [ ] P9-2-E統帥提供 `GEMINI_API_KEY`
- [ ] P9-2-Fcritic 審查無 🔴/🟠 遺留
- [ ] 部署後三路 NLP smoke test 全綠
### 整體 P9-1 + P9-2 合併
- [ ] 四批次 commit 依序部署完成
- [ ] `https://mo.wooo.work/health` 持續 200
- [ ] Telegram 三路對話繁中化正確
- [ ] 統帥決策點 D1-D5 全打勾
- [ ] 下一輪 Sprint 2AIOps writer + KM 補寫Task Prompts 出齊
---
## 7. 附錄:本報告相關檔案絕對路徑
- `/Users/ooo/Library/Mobile Documents/com~apple~CloudDocs/momo-pro-system/docs/p9_completion_report_20260424.md`P9-1 原報告)
- `/Users/ooo/Library/Mobile Documents/com~apple~CloudDocs/momo-pro-system/docs/p9_completion_report_v2_20260424.md`(本報告)
- `/Users/ooo/Library/Mobile Documents/com~apple~CloudDocs/momo-pro-system/services/ai_orchestrator.py`
- `/Users/ooo/Library/Mobile Documents/com~apple~CloudDocs/momo-pro-system/services/hermes_analyst_service.py`
- `/Users/ooo/Library/Mobile Documents/com~apple~CloudDocs/momo-pro-system/services/nemoton_dispatcher_service.py`
- `/Users/ooo/Library/Mobile Documents/com~apple~CloudDocs/momo-pro-system/services/openclaw_strategist_service.py`
- `/Users/ooo/Library/Mobile Documents/com~apple~CloudDocs/momo-pro-system/telegram_ai_integration.py`
- `/Users/ooo/Library/Mobile Documents/com~apple~CloudDocs/momo-pro-system/services/telegram_bot_service.py`
- `/Users/ooo/Library/Mobile Documents/com~apple~CloudDocs/momo-pro-system/services/telegram_templates.py`
- `/Users/ooo/Library/Mobile Documents/com~apple~CloudDocs/momo-pro-system/routes/openclaw_bot_routes.py`
- `/Users/ooo/Library/Mobile Documents/com~apple~CloudDocs/momo-pro-system/docker-compose.yml`
- `/Users/ooo/Library/Mobile Documents/com~apple~CloudDocs/momo-pro-system/.env.example`
> 報告終。Sprint 0 卡在統帥 D1/D2/D3 三項決策,其餘 Agent 可備戰但不推進。

View File

@@ -0,0 +1,116 @@
# Callback Prefix 統一評估報告read-only
> 產出日期2026-04-24狀態評估中未動程式碼
> 範圍:`routes/openclaw_bot_routes.py` + `services/telegram_bot_service.py` + `services/telegram_templates.py`
## 1. 範圍統計(實測)
| 類別 | 數量 | 說明 |
|------|------|------|
| 總 callback_data 字面值 | **308** | 含 f-string含靜態字串 |
| 已有 `momo:` prefix | **5** | 全在 `telegram_templates.py`pa/pr/bpa/bpr/eig `telegram_bot_service.py``momo:ops:` |
| 冒號分隔但無 prefix | **272** | `cmd:*``menu:*``await:*` |
| 底線分隔(舊 TrendBot | **31** | `menu_main/menu_trend/menu_search/menu_copy/menu_keywords/menu_daily/menu_settings``settings_*``trend_*``keywords_*` |
按檔案分布:
- `routes/openclaw_bot_routes.py`**171 筆**(全冒號,僅 1-2 個已是 momo:
- `services/telegram_bot_service.py`**39 筆**(混用 — 30 底線 + 9 冒號)
- `services/telegram_templates.py`**5 筆**(全 momo:**已合規**,免改)
> 原始口述「80+」低估。實際 **待修 303 筆**272 + 31
## 2. 修改點列表(按檔案精簡,完整 diff 見附錄 A
### 2-a. `routes/openclaw_bot_routes.py`171 筆,抽樣)
- L2896-2897`cmd:import_confirm` / `cmd:import_cancel``momo:cmd:*`
- L2996-2999、3098-3101、3244-3247、3419-3429、3444-3450、3462-3482、3501-3517、3524-3590主選單+子選單 blocks
- L3267-3268、3474-3482promo/competitor
- L3392 `_BACK` 常數:`menu:main``momo:menu:main`
- L3635-3645 `sales_quick_kb`
- L4537-4538、4712-4719、4787-4790、4814-4816
- L5047-5257搜尋/比價/類別鑽取回傳)
- L5414-5418、5536-5539、5658、5748、5810、5846、5859、5891、5902
### 2-b. `services/telegram_bot_service.py`39 筆)
- L141-156主選單 8 顆)
- L174、472、483返回按鈕
- L1059-1144trend/keywords/daily 返回群 — 原為底線 `menu_main/menu_trend/menu_keywords/menu_daily`
- L1208-1212、1265-1266settings 面板)
- L1436、1445、1515-1516、1558-1559取消/繼續)
### 2-c. `services/telegram_templates.py`
免改(已全 `momo:` 前綴)。
## 3. Handlerdispatcher衝擊
### `routes/openclaw_bot_routes.py` 入口 L5610-5663
```
data.startswith('menu:') → 切 5 字 → _SUBMENUS[key]
data.startswith('await:') → 切 6 字 → _AWAIT_PROMPTS[action]
data.startswith('cmd:') → 切 4 字 → handle_cmd(parts[0], parts[1])
```
**必須在最外層先 strip `momo:`**,否則 `momo:menu:sales` 會落到無人接的分支。
### `services/telegram_bot_service.py::handle_callback` L426-540
```
data == "menu_main" or "menu:main"
data.startswith("menu:")
data == "menu_trend" / "menu_search" / "menu_copy" / "menu_keywords" / "menu_daily" / "menu_settings"
data.startswith("trend_") / "keywords_" / "settings_")
data.startswith("momo:pa:" / "momo:pr:" / "momo:ops:" / "momo:bpa:" / "momo:bpr:" / "momo:eig:")
data.startswith("cmd:")
```
→ 同樣需要 prefix strip底線家族`menu_*``trend_*``keywords_*``settings_*`)建議**一併**轉冒號並加 prefix永久消滅舊 TrendBot 命名痕跡。
## 4. 推薦方案
| 方案 | 實作成本 | 安全性 | 歷史訊息相容 | 推薦 |
|------|---------|-------|------------|------|
| **A** 雙路徑(新發 momo:handler 同收新舊3 個月後砍舊) | 高308 行 + 2 dispatcher 都改) | 最高 | 完整 | 備選 |
| **B** 破壞式升級(只收 momo: | 中(僅改 dispatch 分支) | 低,歷史鈕變磚 | ❌ | 否決 |
| **C** Dispatcher 入口 strip `momo:`(發送端漸進升級) | **最低**(僅 2 個 dispatcher 頂端加 3 行) | 中高 | 完整 | ✅ **首推** |
### 方案 C 核心 patch示意非 apply
```python
# routes/openclaw_bot_routes.py 在 L5627 後、L5630 前
if data.startswith('momo:'):
data = data[5:]
# services/telegram_bot_service.py handle_callback L443 後
if data.startswith('momo:') and not data.startswith(('momo:pa:','momo:pr:','momo:ops:','momo:bpa:','momo:bpr:','momo:eig:')):
data = data[5:] # 保留 ADR-012 L2 短碼分支原樣
```
**即時達成跨專案隔離**OpenClaw/AWOOOI 送來的非 `momo:` callback 不會被 momo-pro 誤接momo-pro 日後新發按鈕統一加 prefix舊按鈕仍可被處理。
發送端308 行)可在方案 C 上線後,分批以 sed 腳本加 `momo:` prefix純文字替換風險低
## 5. 風險與成本
| 風險/成本 | 等級 | 說明 |
|----------|------|------|
| 歷史未點擊按鈕失效 | 低(方案 C 下)| dispatcher 入口 strip 即可相容 |
| 誤觸他專案按鈕(當前實況) | **高** | 272 筆無 prefix callback 會讓 OpenClaw/AWOOOI 同名按鈕走到 momo-pro handler |
| 底線家族語意改變(`menu_main``momo:menu:main`| 中 | 需同步處理 `settings_notify_on` 等 4 組 startswith |
| ADR-012 短碼分支(`momo:pa:` 等)被誤剝 | 中 | 方案 C patch 要加白名單,不可對這 6 個前綴 strip |
| 預估工時 | 方案 C 純 dispatcher**0.5 小時 + critic 0.5 小時**;全量加 prefix**3-4 小時 + 測試 2 小時** |
| 測試 | dispatcher 變更須在 staging 跑 smoke主選單 6 條主幹 + ADR-012 confirm/reject 按鈕 + L2 ops 按鈕)|
## 6. 等統帥決策
1. **是否啟動?** 建議「是」——當前 272 筆跨專案外漏是真實資安/功能風險(三 bot 共用 Token
2. **採方案?** 首推 **C**(入口 strip + 分批 prefix若統帥要求一次到位選 **A**
3. **派誰執行?**
- 方案 C dispatcher 改動:`fullstack-engineer` + `critic`0.5h
- 308 行 prefix 批量加:`refactor-specialist`P9 子任務3-4h
- 底線家族轉冒號:同上,獨立 commit
4. **驗收**staging smoke主選單點擊 → ADR-012 按鈕 → L2 ops 按鈕 → 歷史按鈕仍可觸發)+ critic 審查無 regression。
---
### 附錄 A完整修改點行號索引
(需全量 diff 時再展開;檔案/行號已於第 2 節列出,可用以下指令重生)
```bash
grep -n "callback_data" routes/openclaw_bot_routes.py \
services/telegram_bot_service.py | grep -v "momo:"
```

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

View File

@@ -0,0 +1,74 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
services/mcp_context_service.py
MCP (Model Context Protocol) 橋接服務
此檔案旨在橋接 OpenClaw 戰略分析所需的外部情報與 MCP 集成層。
"""
import logging
from datetime import datetime
from services.mcp_collector_service import mcp_collector
logger = logging.getLogger(__name__)
class MCPRouter:
"""MCP 路由分發器(模擬或橋接至現有收集器)"""
@staticmethod
def get_context(topic: str) -> str:
return mcp_collector.collect_topic(topic)
def build_mcp_context(query: str = "", topics: list = None) -> str:
"""構建完整 MCP 上下文供 AI 分析使用"""
if topics is None:
topics = ["market_trends", "holiday_calendar", "seasonal_insights"]
results = []
for topic in topics:
content = mcp_collector.collect_topic(topic)
if content:
results.append(f"{topic}\n{content}")
# 加入靜態輔助資訊
results.append(mcp_collector.get_holiday_context())
results.append(mcp_collector.get_seasonal_context())
return "\n\n".join(results)
def query_mcp(question: str) -> str:
"""通用的 MCP 查詢入口"""
# 這裡可以根據 question 決定回傳哪些主題內容
return build_mcp_context(question)
def get_tw_media_news() -> str:
"""取得台灣媒體新聞情報"""
return mcp_collector.collect_topic("market_trends")
def get_ecommerce_news() -> str:
"""取得台灣電商新聞行情"""
return mcp_collector.collect_topic("competitor_intel")
def get_taiwan_trends() -> str:
"""取得台灣 Google 趨勢或熱搜"""
return mcp_collector.collect_topic("market_trends")
def get_dcard_trends() -> str:
"""取得 Dcard 熱門討論洞察"""
return mcp_collector.collect_topic("consumer_sentiment")
def get_youtube_trending() -> str:
"""取得 YouTube 台灣熱門趨勢"""
return mcp_collector.collect_topic("market_trends")
def get_taiwan_weather() -> str:
"""取得台灣天氣概況(影響消費行為)"""
# 簡易橋接,可用搜尋 Grounding 獲取
return mcp_collector.collect_topic("台灣近日天氣與氣溫概況")
def get_twbank_exchange_rates() -> str:
"""取得台幣匯率概況(影響跨境採購)"""
return mcp_collector.collect_topic("台幣匯率趨勢")
def get_upcoming_events() -> str:
"""取得近期重要行事曆事件"""
return mcp_collector.get_holiday_context()