diff --git a/.claude/hooks/__test__/commit-quality.test.sh b/.claude/hooks/__test__/commit-quality.test.sh new file mode 100755 index 0000000..64086bc --- /dev/null +++ b/.claude/hooks/__test__/commit-quality.test.sh @@ -0,0 +1,58 @@ +#!/usr/bin/env bash +# commit-quality.js 單元測試 +# 4 case:Bash 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 commit,staged 含 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 default(35 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 ]] diff --git a/.claude/hooks/commit-quality.js b/.claude/hooks/commit-quality.js index 32f2ac7..fa45c0b 100644 --- a/.claude/hooks/commit-quality.js +++ b/.claude/hooks/commit-quality.js @@ -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', '') + [/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); }); diff --git a/.claude/settings.json b/.claude/settings.json index a419104..5621c37 100644 --- a/.claude/settings.json +++ b/.claude/settings.json @@ -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\\('原訊息 ', '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" + } ] } ] diff --git a/docs/guides/DISK_EXPANSION_GUIDE.md b/docs/guides/DISK_EXPANSION_GUIDE.md new file mode 100644 index 0000000..23f030b --- /dev/null +++ b/docs/guides/DISK_EXPANSION_GUIDE.md @@ -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 diff --git a/docs/p9_completion_report_20260424.md b/docs/p9_completion_report_20260424.md new file mode 100644 index 0000000..9f330fc --- /dev/null +++ b/docs/p9_completion_report_20260424.md @@ -0,0 +1,132 @@ +# [P9-COMPLETION] Telegram Bot 全景修復(12 Agent / 16 派遣) + +**日期**: 2026-04-24 +**模式**: P9(拆解)→ P7 × 16 +**總結人**: Planner (Opus 4.7 / 1M) + +--- + +## 1. 範圍涵蓋 + +| 嚴重度 | 已修 | 待處理 | +|--------|------|--------| +| Critical | 3(C1 PoC / C2 handler / C3 fail-closed) | C1 Bot Token 吊銷(須 BotFather 親操作) | +| High | 4(H4 LRU / H6 rate-limit / H7 POSTGRES_PASSWORD / Hook 盲點) | H1 5969 行拆分、KM 雙寫、AIOps 停擺、雙 Bot 架構 | +| Medium | 2(M 清散落 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-fast(H7)| +| `docker-compose.yml` | +8/-2 | 188 主機 env 修正同步回 git(Phase 1.5)| +| `routes/openclaw_bot_routes.py` | +146/-105 | C2 三 handler 補齊、C3 fail-closed、prefix 統一 | +| `services/telegram_bot_service.py` | +202/-19 | update_ids LRU(H4)、callback rate-limit(H6)| +| `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 統一(方案 C,2 工天) +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 → Redis(multi-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),統帥須最優先處理。 diff --git a/docs/p9_completion_report_v2_20260424.md b/docs/p9_completion_report_v2_20260424.md new file mode 100644 index 0000000..c0cc63f --- /dev/null +++ b/docs/p9_completion_report_v2_20260424.md @@ -0,0 +1,219 @@ +# [P9-COMPLETION-v2] 三 AI NLP 修復 + P9-1 合併 Sprint 總報告 + +> 日期:2026-04-24 +> 統籌者:planner(P9 模式) +> 前身:`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 | 統帥 | 三專案共用 Bot,revoke 後三個 `.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 或 Pro;2.0 於 2026-06-01 EOL,剩 5 週) | fullstack + web-researcher | P9-2-C P0 | +| S3-2 | Callback prefix 統一(方案 C:308 按鈕加 `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: + +### 批次 A:Hook + 散落檔清理(風險最低) +**檔案**:`.claude/hooks/commit-quality.js`、`.claude/settings.json`、`.env.example` +**Smoke test**: +- 本機 `git commit` 嘗試塞入假 Token,Hook 應阻擋 +- Claude Code 重新啟動,settings.json 能正常載入 +- `.env.example` diff 內容不含真實 KEY + +### 批次 B:Telegram 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 NLP(P9-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 策略" → OpenClaw(Gemini)回繁中策略 +- `ai_orchestrator.py` 的 SQL 查詢不再丟 `ProgrammingError: text() required` +- `ai_insights` 表新增紀錄,embedding 欄位有值(走 `_enqueue_embedding`) + +### 批次 D:compose + 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-F:critic 審查無 🔴/🟠 遺留 +- [ ] 部署後三路 NLP smoke test 全綠 + +### 整體 P9-1 + P9-2 合併 +- [ ] 四批次 commit 依序部署完成 +- [ ] `https://mo.wooo.work/health` 持續 200 +- [ ] Telegram 三路對話繁中化正確 +- [ ] 統帥決策點 D1-D5 全打勾 +- [ ] 下一輪 Sprint 2(AIOps 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 可備戰但不推進。 diff --git a/docs/refactor/callback_prefix_proposal.md b/docs/refactor/callback_prefix_proposal.md new file mode 100644 index 0000000..139c1bf --- /dev/null +++ b/docs/refactor/callback_prefix_proposal.md @@ -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-3482(promo/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-1144(trend/keywords/daily 返回群 — 原為底線 `menu_main/menu_trend/menu_keywords/menu_daily`) +- L1208-1212、1265-1266(settings 面板) +- L1436、1445、1515-1516、1558-1559(取消/繼續) + +### 2-c. `services/telegram_templates.py` +免改(已全 `momo:` 前綴)。 + +## 3. Handler(dispatcher)衝擊 + +### `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:" +``` diff --git a/docs/refactor/openclaw_bot_routes_split_plan.md b/docs/refactor/openclaw_bot_routes_split_plan.md new file mode 100644 index 0000000..0bb7811 --- /dev/null +++ b/docs/refactor/openclaw_bot_routes_split_plan.md @@ -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` | 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 節)再修訂拆分邊界。* diff --git a/services/mcp_context_service.py b/services/mcp_context_service.py new file mode 100644 index 0000000..98a236e --- /dev/null +++ b/services/mcp_context_service.py @@ -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()