feat(ops): restore Telegram chain + P2/P3 price decisions + ADR-011
All checks were successful
CD Pipeline / deploy (push) Successful in 1m19s

P2 (Inline Keyboard 降價決策):
- routes/bot_api_routes.py: POST /bot/api/price-decision/notify
- services/telegram_bot_service.py: pa:/pr: callback handlers

P3 (OpenClaw 自動觸發):
- services/openclaw_strategist_service.py: Gemini 週報末尾輸出
  PRICE_DECISIONS_JSON,解析後自動推送 inline keyboard 給 admin

Ops 修復(跨專案隔離與容器斷訊根因):
- ADR-011 全面規範多專案共存邊界、禁用 --remove-orphans
- .gitea/workflows/cd.yaml: sync 模式一次重啟三容器
  (原本僅 momo-pro-system,scheduler/telegram-bot 靜默落伍)
- run_telegram_bot.py: 從 scripts/tools/ 複製到根目錄
  (消滅 docker-compose mount 建空目錄的陷阱)
- CLAUDE.md: 補核心容器表、診斷黃金三句、緊急指令

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
ogt
2026-04-19 12:25:04 +08:00
parent 986908222d
commit 8d0b79cd00
7 changed files with 577 additions and 17 deletions

View File

@@ -130,7 +130,8 @@ jobs:
if: steps.deploy_type.outputs.type == 'sync'
run: |
ssh -i ~/.ssh/id_deploy -o StrictHostKeyChecking=no ollama@192.168.0.188 \
"docker restart momo-pro-system && echo '✅ momo-pro-system 已重啟'"
"docker restart momo-pro-system momo-scheduler momo-telegram-bot 2>&1 && \
echo '✅ 三容器已重啟app/scheduler/telegram-bot'"
# ── 模式 B重建 Docker ImageDockerfile / requirements.txt 變動) ──
- name: 同步所有檔案並重建 Image

View File

@@ -11,19 +11,34 @@
| 主機 | IP | 角色 |
|------|----|------|
| 110 (Gateway) | `192.168.0.110` | Nginx, Gitea, n8n, Superset |
| 188 (App/AI) | `192.168.0.188` | EwoooC App, DB, Ollama |
| 188 (App/AI) | `192.168.0.188` | EwoooC App, DB, Ollama(多專案共存,見 ADR-011 |
## 核心容器(三動一 DB缺一不可
| 容器 | 角色 | 啟動入口 |
|------|------|---------|
| `momo-pro-system` | Flask/Gunicorn 主應用 | `app.py`(手動 docker run 歷史債,見 ADR-011 |
| `momo-scheduler` | 13 個排程任務(爬蟲/AI/備份/通知) | `run_scheduler.py` |
| `momo-telegram-bot` | Telegram 互動 + 每日 09:00 推播 | `run_telegram_bot.py`(根目錄副本) |
| `momo-db` | PostgreSQL + pgvector | compose service `momo-postgres` |
## 常用指令
```bash
# 本地開發
source venv/bin/activate && python app.py
# 部署(標準)→ push 即自動部署Gitea Actions
# 部署(標準)→ push 即自動部署Gitea Actions,同時重啟三容器
git push origin main
# 手動部署 → 188via 110 跳板,緊急用
scp -o ProxyJump=wooo@192.168.0.110 <file> ollama@192.168.0.188:/home/ollama/momo-pro/
ssh -J wooo@192.168.0.110 ollama@192.168.0.188 "docker restart momo-pro-system"
# 🔍 診斷黃金三句(任何 Telegram/排程異常先跑
ssh wooo@192.168.0.110 "ssh ollama@192.168.0.188 \"\
docker ps --format '{{.Names}} | {{.Status}}' | grep momo-; \
docker exec momo-scheduler env | grep -iE 'TELEGRAM|NVIDIA'; \
docker logs momo-scheduler --since 1h | grep -E 'Telegram|Error' | tail -10\""
# 🆘 緊急重建單容器(不影響 momo-db 資料)
ssh wooo@192.168.0.110 "ssh ollama@192.168.0.188 \"\
cd /home/ollama/momo-pro && docker compose up -d --no-deps --force-recreate <service>\""
# ⚠️ 禁用 --remove-orphans會清掉 momo-db見 ADR-011
```
## CI/CD
@@ -46,3 +61,10 @@ ssh -J wooo@192.168.0.110 ollama@192.168.0.188 "docker restart momo-pro-system"
| 歷史日誌 | [docs/memory/history_logs.md](docs/memory/history_logs.md) |
| 憑證對照表 | [docs/memory/credentials_passbook.md](docs/memory/credentials_passbook.md) |
| AIOps 存檔 | [docs/external/aiops_saas.md](docs/external/aiops_saas.md) |
| 跨專案隔離(**必讀**| [docs/adr/ADR-011-cross-project-resource-isolation.md](docs/adr/ADR-011-cross-project-resource-isolation.md) |
## AI 開發鐵律Token 優化)
1. **狙擊手模式**:禁止在未獲授權的情況下,使用 `ls`, `grep`, `cat` 等指令在專案內進行盲搜。
2. **精準打擊**:統帥給定任務時,若已明確指出目標檔案路徑,請直接修改該檔案,嚴禁漫無目的地掃描其他關聯模組。
3. **上下文克制**:不要主動讀取超過 300 行以上的檔案,除非統帥明確要求。需要理解架構時,優先依賴本專案的 SOT 文件或統帥的直接指示。

View File

@@ -0,0 +1,86 @@
# ADR-011: 跨專案資源隔離與 Container 管理原則
- **Status**: Accepted
- **Date**: 2026-04-19
- **Deciders**: 統帥
- **Related**: ADR-008文件對齊實況, ADR-010Gitea CI/CD
## Context
2026-04-19 發現 AI Agent 連續數日沒發任何 Telegram 通知(日報/比價/異常告警全斷)。根因調查揭露 188 主機是**多專案共享環境**momo-pro、clawbot-v5、AWOOOI、wooo-aiops 等 5+ 個 compose project 並存,過去的部署操作缺乏隔離邊界檢查,造成下列風險:
### 實況盤點
1. `momo-pro-system` 容器**不是** compose 管理的(手動 docker runlabels 全空)
2. `momo-scheduler` / `momo-db` 是 compose 建立但 **service name 對不上現行 compose 檔**(視為 orphan
3. `momo-telegram-bot` 容器**從未被建立**docker ps -a 無歷史)
4. `docker compose up` 會自動建立 orphan 警告,若使用 `--remove-orphans` 會**清掉 momo-db**(資料災難)
5. `momo-pro/.env` 使用的 bot 實為 **`OpenClawAwoooI_Bot`(共用三專案)**,非 momo-pro 獨有
6. `docker-compose.yml` 部分 volume mount 指向**不存在**的根目錄檔案(如 `./run_telegram_bot.py`docker 會自動建空目錄造成 `__main__` 錯誤
7. compose 檔宣告的 `momo-network` 與既有 `momo-pro_default` **網路分裂**,新舊容器無法互連 DNS
### 衝擊
- 排程容器看似在跑,但環境變數沒注入 → 靜默失敗
- 跨專案盲改可能破壞 `momo-db``openclaw` 容器
- 未來 CD 若無隔離檢查,會重複踩同樣的洞
## Decision
### ① 隔離邊界(不可跨越)
| 資源 | 歸屬 | 不可做的事 |
|------|------|------------|
| `momo-db`(pgvector/pg14) | momo-pro 獨家 | 任何其他專案不得連入 `momo-pro_default` 網路 |
| `NVIDIA_API_KEY` | momo-pro 獨家 | 不得移入其他專案的 .env |
| `services/{hermes,nemoton,openclaw}_*_service.py` | momo-pro 獨家 | AWOOOI 若要用必須透過 HTTP API 呼叫,禁止 import |
| `openclaw` 容器port 8088 | **clawbot-v5 專案** | momo-pro CD 不得 stop/remove 該容器 |
| Container 命名 `momo-*` 前綴 | momo-pro 保留 | 其他專案禁用此前綴 |
### ② Container 操作原則
- **禁用** `docker compose down` / `--remove-orphans`(會殃及 orphan label 的 momo-db
- **首選** `docker compose up -d --no-deps --force-recreate <service>` 精準重建單一容器
- **救急** `docker network connect momo-pro_default <container>` 處理網路分裂
- **容器操作前三句診斷**(見 `memory/reference_docker_topology.md`)必跑
### ③ docker-compose.yml 修正(本 ADR 同步提交)
1. `networks` 改為引用既有網路,杜絕新舊分裂:
```yaml
networks:
momo-pro_default:
external: true
```
所有 service 的 `networks:` 段落明確指向 `momo-pro_default`
2. `scheduler` / `telegram-bot` 的 `depends_on` 改為 `condition: service_started` 或移除,避免連帶觸發 `momo-pro-system` 重建衝突
3. `run_telegram_bot.py` mount 路徑從 `./run_telegram_bot.py` 改為 `./scripts/tools/run_telegram_bot.py`(或本地根目錄補檔)
### ④ CD Pipeline 補強cd.yaml
sync 模式在 `docker restart momo-pro-system` 之後增加:
```bash
docker restart momo-scheduler momo-telegram-bot
```
rebuild 模式在 `docker compose up -d` 之後增加健康檢查三容器全綠。
### ⑤ Telegram 共用 Bot 規範
- callback_data 必加專案 prefix`momo:pa:{id}` / `momo:pr:{id}`
- 推播訊息標頭加 `[EwoooC]` 以便群組內識別來源
- `services/openclaw_strategist_service.py:229` 的硬編碼 token fallback 移除,改為顯式 fail
## Consequences
**正面**
- 未來任何專案動作前,強制查 label 驗證歸屬,消滅「改 A 壞 B」風險
- 網路統一後,新建容器不會再因 DNS 失敗而靜默停擺
- CD 同步重啟三容器根除「scheduler 跟不上代碼」的斷訊路徑
**負面**
- 舊有的手動 docker run 歷史債仍在(如 `momo-pro-system` 無 compose label完整遷移需一次停機窗口
- 共用 bot 的長期策略需另立專題:拆分成 `momo-pro-bot` / `awoooi-bot` / `openclaw-bot` 三個獨立 bot
## References
- `memory/reference_188_multi_project.md` — 188 上專案清單與歸屬
- `memory/reference_docker_topology.md` — 四容器實況與診斷指令
- `memory/feedback_shared_telegram_bot.md` — 共用 bot 踩坑
- `memory/project_telegram_restore_20260419.md` — 本次斷訊修復記錄

View File

@@ -7,6 +7,7 @@ Bot API 路由模組
"""
import os
import requests
from datetime import datetime, timezone, timedelta
from functools import wraps
from flask import Blueprint, request, jsonify
@@ -709,3 +710,101 @@ def bot_ai_suggest():
'success': False,
'error': str(e)
}), 500
# ===== 降價決策通知 =====
@bot_api_bp.route('/bot/api/price-decision/notify', methods=['POST'])
@require_api_token
def price_decision_notify():
"""
觸發 Telegram 降價決策通知(推送給所有 is_admin=True 的用戶)
Request JSON:
- product_sku: 商品貨號(必填)
- product_name: 商品名稱(必填)
- current_price: 現價(數字,必填)
- suggested_price: 建議降至(數字,必填)
- reason: AI 理由(必填)
- insight_id: ai_insights 表的 ID必填供回調按鈕使用
- report_url: 分析報表連結(選填)
"""
from sqlalchemy import text as sa_text
data = request.get_json() or {}
required_fields = ['product_sku', 'product_name', 'current_price', 'suggested_price', 'reason', 'insight_id']
missing = [f for f in required_fields if data.get(f) is None]
if missing:
return jsonify({'success': False, 'error': f'Missing fields: {", ".join(missing)}'}), 400
product_sku = data['product_sku']
product_name = data['product_name']
insight_id = int(data['insight_id'])
report_url = data.get('report_url', '')
try:
current_price = float(data['current_price'])
suggested_price = float(data['suggested_price'])
except (ValueError, TypeError) as e:
return jsonify({'success': False, 'error': f'Invalid price value: {e}'}), 400
token = os.getenv('TELEGRAM_BOT_TOKEN')
if not token:
return jsonify({'success': False, 'error': 'TELEGRAM_BOT_TOKEN not configured'}), 500
drop_pct = (current_price - suggested_price) / current_price * 100 if current_price > 0 else 0
message = (
f"💰 *降價決策請求*\n\n"
f"🏷️ 商品:{product_name}\n"
f"📦 貨號:`{product_sku}`\n"
f"💵 現價:${current_price:,.0f}\n"
f"📉 建議降至:${suggested_price:,.0f}(↓{drop_pct:.1f}%\n\n"
f"🤖 *AI 理由:*\n{data['reason']}"
)
keyboard = {"inline_keyboard": [
[
{"text": "✅ 批准降價", "callback_data": f"pa:{insight_id}"},
{"text": "❌ 拒絕", "callback_data": f"pr:{insight_id}"}
]
]}
if report_url:
keyboard["inline_keyboard"].append([{"text": "🔗 查看報表", "url": report_url}])
db = DatabaseManager()
sent_count = 0
errors = []
try:
with db.engine.connect() as conn:
rows = conn.execute(sa_text(
"SELECT telegram_id FROM telegram_users WHERE is_active = true AND is_admin = true"
)).fetchall()
except Exception as e:
sys_log.error(f"[BotAPI] price_decision_notify DB error: {e}")
return jsonify({'success': False, 'error': f'DB error: {e}'}), 500
tg_url = f"https://api.telegram.org/bot{token}/sendMessage"
for row in rows:
try:
resp = requests.post(tg_url, json={
"chat_id": row[0],
"text": message,
"parse_mode": "Markdown",
"reply_markup": keyboard,
}, timeout=10)
if resp.ok:
sent_count += 1
else:
errors.append(f"chat_id={row[0]}: {resp.text[:120]}")
except Exception as e:
errors.append(f"chat_id={row[0]}: {e}")
sys_log.info(f"[BotAPI] price_decision_notify sent={sent_count}/{len(rows)} insight_id={insight_id}")
return jsonify({
'success': True,
'insight_id': insight_id,
'sent': sent_count,
'total_admins': len(rows),
'errors': errors,
})

148
run_telegram_bot.py Normal file
View File

@@ -0,0 +1,148 @@
#!/usr/bin/env python3
"""
Telegram Bot 獨立執行腳本
用法:
python run_telegram_bot.py
環境變數:
TELEGRAM_BOT_TOKEN: Telegram Bot Token (必填)
功能:
- 啟動 Telegram Bot 監聽
- 每日 09:00 推播趨勢摘要
- 處理用戶指令:/trend, /search, /copy, /keywords, /daily, /settings
"""
import os
import sys
import asyncio
import logging
from datetime import datetime, time
from dotenv import load_dotenv
# 載入環境變數
load_dotenv()
# 設定日誌
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
handlers=[
logging.StreamHandler(),
logging.FileHandler('logs/telegram_bot.log', encoding='utf-8')
]
)
logger = logging.getLogger('TelegramBot')
def check_dependencies():
"""檢查必要的套件"""
try:
from telegram import Update
from telegram.ext import Application
logger.info("✅ python-telegram-bot 已安裝")
return True
except ImportError:
logger.error("❌ 請安裝 python-telegram-bot: pip install python-telegram-bot")
return False
def check_token():
"""檢查 Bot Token"""
token = os.getenv('TELEGRAM_BOT_TOKEN')
if not token:
logger.error("❌ 請在 .env 設定 TELEGRAM_BOT_TOKEN")
logger.info(" 1. 在 Telegram 搜尋 @BotFather")
logger.info(" 2. 發送 /newbot 建立新 Bot")
logger.info(" 3. 複製 Token 到 .env 檔案")
return None
logger.info("✅ Bot Token 已設定")
return token
async def main():
"""主程式"""
print("=" * 60)
print(" MOMO Pro System - Telegram Bot")
print("=" * 60)
print()
# 檢查依賴
if not check_dependencies():
sys.exit(1)
# 檢查 Token
token = check_token()
if not token:
sys.exit(1)
# 導入 Bot 服務
try:
from services.telegram_bot_service import TelegramBotService
logger.info("✅ TelegramBotService 已載入")
except ImportError as e:
logger.error(f"❌ 無法載入 TelegramBotService: {e}")
sys.exit(1)
# 建立 Bot 服務
bot_service = TelegramBotService(token)
# 取得 Application
app = bot_service.get_application()
if not app:
logger.error("❌ 無法建立 Bot Application")
sys.exit(1)
# 設定每日推播排程 (每天 09:00)
from telegram.ext import JobQueue
async def daily_summary_job(context):
"""每日摘要推播任務"""
logger.info("📤 執行每日趨勢摘要推播...")
await bot_service.send_daily_summary()
# 啟動 Bot
logger.info("🚀 啟動 Telegram Bot...")
logger.info(" 指令列表:")
logger.info(" /trend [分類] - 查看熱門趨勢")
logger.info(" /search [關鍵字] - AI 網路搜尋")
logger.info(" /copy [商品名] - 生成行銷文案")
logger.info(" /keywords - 熱門關鍵字")
logger.info(" /daily - 每日趨勢摘要")
logger.info(" /settings - 通知設定")
print()
# 初始化並啟動
await app.initialize()
await app.start()
# 設定每日推播 (09:00 台北時間)
job_queue = app.job_queue
if job_queue:
# 計算下一個 09:00 的時間
target_time = time(hour=9, minute=0, second=0)
job_queue.run_daily(daily_summary_job, time=target_time, name='daily_summary')
logger.info("📅 已設定每日 09:00 推播趨勢摘要")
# 開始輪詢
await app.updater.start_polling(drop_pending_updates=True)
logger.info("✅ Bot 已啟動,按 Ctrl+C 停止")
# 保持運行
try:
while True:
await asyncio.sleep(1)
except KeyboardInterrupt:
logger.info("🛑 收到停止信號...")
finally:
await app.updater.stop()
await app.stop()
await app.shutdown()
logger.info("👋 Bot 已停止")
if __name__ == '__main__':
# 確保 logs 目錄存在
os.makedirs('logs', exist_ok=True)
# 執行
asyncio.run(main())

View File

@@ -31,15 +31,19 @@ def _build_citation_footer(start_date: str, end_date: str) -> str:
return ""
TYPE_LABEL = {
"price_alert": "競價告警",
"human_review": "人工覆核",
"recommendation": "推薦商品",
"km_price_competition": "KM競價情報",
"km_sales_anomaly": "KM銷量異常",
"price_alert": "競價告警",
"human_review": "人工覆核",
"recommendation": "推薦商品",
"km_price_competition": "KM競價情報",
"km_sales_anomaly": "KM銷量異常",
"km_promotion_opportunity": "KM促銷機會",
"km_market_trend": "KM市場趨勢",
"relearn_event": "重新學習事件",
"backup_status": "備份狀態",
"km_market_trend": "KM市場趨勢",
"relearn_event": "重新學習事件",
"backup_status": "備份狀態",
"price_recommendation": "降價建議",
"price_decision_feedback": "降價決策回饋",
"weekly_meta": "週報策略",
"meta_analysis": "Meta 分析",
}
lines = ["\n\n---", "📚 **本報告引用來源:**"]
@@ -177,6 +181,20 @@ def generate_weekly_strategy_report(force_tg_alert: bool = False) -> str:
請用繁體中文,語氣保持專業、精煉、具備行動力。
在報告的每個具體數據或告警描述後,若來自「資料二」,請在句末標注【引用自 {start_date_str} {end_date_str} 的洞察】。
---
在報告最末尾,**必須**輸出以下標記行與 JSON若本週無明確降價建議則輸出空陣列
PRICE_DECISIONS_JSON:
[
{{
"product_sku": "貨號(若無則填空字串)",
"product_name": "商品名稱",
"current_price": 現價數字,
"suggested_price": 建議降至數字,
"reason": "一句話理由(中文)"
}}
]
只輸出純 JSON 陣列,不加 markdown 代碼塊,不加任何其他說明文字。
"""
# 3. 呼叫 Gemini
@@ -196,13 +214,128 @@ def generate_weekly_strategy_report(force_tg_alert: bool = False) -> str:
metadata={"start_date": start_date_str, "end_date": end_date_str, "generated_by": "Gemini-2.0-Flash"}
)
sys_log.info(f"[OCStrategist] 週報產出成功並已雙寫存入 AI 知識庫 (ID: {insight_id})")
# 5. 通知 Telegram
# 5. 解析降價決策並推送 Telegram Inline Keyboard
price_recs = _parse_price_recommendations(report_md)
if price_recs:
_send_price_decision_requests(price_recs, period_str, source_insight_id=insight_id)
# 6. 週報摘要通知 Telegram
if force_tg_alert:
_notify_telegram_group(report_md, period_str)
return report_md
def _parse_price_recommendations(report_md: str) -> list:
"""從 Gemini 週報中解析 PRICE_DECISIONS_JSON 區塊,回傳降價建議清單。"""
marker = "PRICE_DECISIONS_JSON:"
idx = report_md.find(marker)
if idx == -1:
return []
raw = report_md[idx + len(marker):].strip()
# 找最後一個 ] 確保取完整陣列(欄位值含 ] 時 find 會截斷)
end = raw.rfind("]")
if end == -1:
return []
raw = raw[: end + 1]
try:
recs = json.loads(raw)
if not isinstance(recs, list):
return []
valid = []
for r in recs:
if all(k in r for k in ("product_name", "current_price", "suggested_price", "reason")):
r.setdefault("product_sku", "")
try:
r["current_price"] = float(r["current_price"])
r["suggested_price"] = float(r["suggested_price"])
except (ValueError, TypeError):
continue
if r["suggested_price"] < r["current_price"]:
valid.append(r)
return valid
except json.JSONDecodeError as e:
sys_log.warning(f"[OCStrategist] price_recs JSON 解析失敗: {e}")
return []
def _send_price_decision_requests(recs: list, period_str: str, source_insight_id: int = None):
"""
對每筆降價建議:
1. 寫入 ai_insightsinsight_type='price_recommendation')取得 insight_id
2. 查詢所有 is_admin=true 的 Telegram 用戶
3. 用 TELEGRAM_BOT_TOKEN 發送含 ✅/❌ inline keyboard 的訊息
"""
bot_token = os.getenv('TELEGRAM_BOT_TOKEN')
if not bot_token:
sys_log.warning("[OCStrategist] TELEGRAM_BOT_TOKEN 未設定,略過降價決策通知")
return
# 查管理員 chat_id
session = get_session()
try:
rows = session.execute(
text("SELECT telegram_id FROM telegram_users WHERE is_active = true AND is_admin = true")
).fetchall()
except Exception as e:
sys_log.error(f"[OCStrategist] 查詢管理員失敗: {e}")
return
finally:
session.close()
if not rows:
sys_log.info("[OCStrategist] 無 is_admin 管理員,略過降價決策通知")
return
admin_ids = [row[0] for row in rows]
tg_url = f"https://api.telegram.org/bot{bot_token}/sendMessage"
for rec in recs:
# 寫 KM
meta = {**rec, "period": period_str}
if source_insight_id:
meta["source_weekly_meta_id"] = source_insight_id
rec_insight_id = store_insight(
insight_type="price_recommendation",
content=f"建議 {rec['product_name']} 從 ${rec['current_price']:,.0f} 降至 ${rec['suggested_price']:,.0f}{rec['reason']}",
period=period_str,
product_sku=rec["product_sku"] or None,
metadata=meta,
)
if not rec_insight_id:
sys_log.warning(f"[OCStrategist] store_insight 失敗,略過 {rec['product_name']}")
continue
drop_pct = (rec["current_price"] - rec["suggested_price"]) / rec["current_price"] * 100
msg = (
f"💰 *降價決策請求*\n\n"
f"🏷️ 商品:{rec['product_name']}\n"
f"📦 貨號:`{rec['product_sku'] or 'N/A'}`\n"
f"💵 現價:${rec['current_price']:,.0f}\n"
f"📉 建議降至:${rec['suggested_price']:,.0f}(↓{drop_pct:.1f}%\n\n"
f"🤖 *AI 理由:*\n{rec['reason']}"
)
keyboard = {"inline_keyboard": [[
{"text": "✅ 批准降價", "callback_data": f"pa:{rec_insight_id}"},
{"text": "❌ 拒絕", "callback_data": f"pr:{rec_insight_id}"},
]]}
for chat_id in admin_ids:
try:
resp = requests.post(tg_url, json={
"chat_id": chat_id,
"text": msg,
"parse_mode": "Markdown",
"reply_markup": keyboard,
}, timeout=10)
if not resp.ok:
sys_log.warning(f"[OCStrategist] TG send 失敗 chat_id={chat_id}: {resp.text[:100]}")
except Exception as e:
sys_log.error(f"[OCStrategist] TG send 例外 chat_id={chat_id}: {e}")
sys_log.info(f"[OCStrategist] 降價決策推送 insight_id={rec_insight_id}{len(admin_ids)} 位管理員")
def _notify_telegram_group(report_md: str, period_str: str):
"""
推送至 Telegram

View File

@@ -461,6 +461,77 @@ class TrendTelegramBot:
elif data.startswith("settings_"):
await self._handle_settings_callback(query, data)
# ===== 降價決策按鈕 =====
elif data.startswith("pa:"):
await self._handle_price_approve(query, data[3:])
elif data.startswith("pr:"):
await self._handle_price_reject(query, data[3:])
async def _handle_price_approve(self, query, insight_id_str: str):
"""批准降價:寫 KM feedback + 移除按鈕"""
from services.openclaw_learning_service import store_insight
from datetime import date as date_cls
try:
insight_id = int(insight_id_str)
except ValueError:
await query.answer("無效的決策 ID", show_alert=True)
return
user = query.from_user
operator = user.full_name or f"id_{user.id}"
store_insight(
insight_type="price_decision_feedback",
content=f"管理員批准降價建議source_insight_id={insight_id}",
period=date_cls.today().isoformat(),
metadata={
"decision": "approve",
"source_insight_id": insight_id,
"operator": operator,
"operator_tg_id": user.id,
}
)
original = query.message.text or ""
await query.edit_message_text(
f"{original}\n\n✅ *已批准降價*\n操作人:{operator}",
parse_mode='Markdown'
)
async def _handle_price_reject(self, query, insight_id_str: str):
"""拒絕降價:寫 KM 訓練保守策略 + 移除按鈕"""
from services.openclaw_learning_service import store_insight
from datetime import date as date_cls
try:
insight_id = int(insight_id_str)
except ValueError:
await query.answer("無效的決策 ID", show_alert=True)
return
user = query.from_user
operator = user.full_name or f"id_{user.id}"
store_insight(
insight_type="price_decision_feedback",
content=f"管理員拒絕降價建議source_insight_id={insight_id}),訓練保守策略",
period=date_cls.today().isoformat(),
metadata={
"decision": "reject",
"source_insight_id": insight_id,
"operator": operator,
"operator_tg_id": user.id,
}
)
original = query.message.text or ""
await query.edit_message_text(
f"{original}\n\n❌ *已拒絕降價*\n操作人:{operator}\n📚 已記錄為保守策略訓練資料",
parse_mode='Markdown'
)
async def _show_trend_by_category(self, query, category: str):
"""顯示指定分類的趨勢"""
try: