[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:
58
.claude/hooks/__test__/commit-quality.test.sh
Executable file
58
.claude/hooks/__test__/commit-quality.test.sh
Executable file
@@ -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 ]]
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
178
docs/guides/DISK_EXPANSION_GUIDE.md
Normal file
178
docs/guides/DISK_EXPANSION_GUIDE.md
Normal 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
|
||||
132
docs/p9_completion_report_20260424.md
Normal file
132
docs/p9_completion_report_20260424.md
Normal 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 | 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),統帥須最優先處理。
|
||||
219
docs/p9_completion_report_v2_20260424.md
Normal file
219
docs/p9_completion_report_v2_20260424.md
Normal file
@@ -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 可備戰但不推進。
|
||||
116
docs/refactor/callback_prefix_proposal.md
Normal file
116
docs/refactor/callback_prefix_proposal.md
Normal 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-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:"
|
||||
```
|
||||
282
docs/refactor/openclaw_bot_routes_split_plan.md
Normal file
282
docs/refactor/openclaw_bot_routes_split_plan.md
Normal 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` | 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 節)再修訂拆分邊界。*
|
||||
74
services/mcp_context_service.py
Normal file
74
services/mcp_context_service.py
Normal 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()
|
||||
Reference in New Issue
Block a user