fix: Phase 2 P0 全清零 — 14 項安全與功能修復完成
Some checks failed
CD Pipeline / deploy (push) Failing after 2m59s
Some checks failed
CD Pipeline / deploy (push) Failing after 2m59s
P0-06: google_drive_service.py — pickle.load() 改 JSON token(消除 RCE 風險) P0-07: bot_api_routes.py:30 — BOT_API_TOKEN 移除硬編碼預設值 clawdbot_momo_2026 P0-08: auto_import_index.html — showAlert innerHTML 改 createTextNode(XSS 修復) P0-09: abc_analysis_detail.html + dashboard.html + daily_sales.html — Jinja2 | e 轉義 P0-10: openclaw_bot_routes.py:2634 — vendor PPT 補 return ppt_path(廠商報告恢復) P0-11: telegram_bot_service.py:177-214 — cmd_start/cmd_help 補 try/except P0-12: app.py:689-712 — 10 個 Blueprint 補齊 register(消滅 404 路由) P0-13: auto_heal_service.py — 實作 _write_heal_log(),AIOps 稽核閉環補完 P0-14: monitoring/prometheus.yml — 取消 alert_rules comment;新增 alert_rules.yml Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -235,6 +235,7 @@ class AutoHealService:
|
||||
action_type = playbook["action_type"]
|
||||
params = playbook.get("action_params", {})
|
||||
|
||||
started_at = datetime.now()
|
||||
if action_type == "DOCKER_RESTART":
|
||||
result = self._docker_restart(params)
|
||||
elif action_type == "WAIT_RETRY":
|
||||
@@ -246,7 +247,8 @@ class AutoHealService:
|
||||
else:
|
||||
return AutoHealResult(success=False, action=action_type, message="Unhandled action_type")
|
||||
|
||||
self._alert_and_store(playbook, context)
|
||||
duration_ms = (datetime.now() - started_at).total_seconds() * 1000
|
||||
self._alert_and_store(playbook, context, result, duration_ms)
|
||||
return result
|
||||
|
||||
def _docker_restart(self, params: Dict[str, Any]) -> AutoHealResult:
|
||||
@@ -304,7 +306,49 @@ class AutoHealService:
|
||||
out = result["stdout"] or result["stderr"]
|
||||
return AutoHealResult(success=result["success"], action="SSH_CMD", message=out)
|
||||
|
||||
def _alert_and_store(self, playbook: Dict[str, Any], context: Dict[str, Any]) -> None:
|
||||
def _alert_and_store(
|
||||
self,
|
||||
playbook: Dict[str, Any],
|
||||
context: Dict[str, Any],
|
||||
result: "AutoHealResult",
|
||||
duration_ms: float = 0.0,
|
||||
) -> None:
|
||||
_store_escalation(playbook["action_type"])
|
||||
# TODO: integrate triaged_alert if needed
|
||||
self._log.info("[AutoHeal] Alert stored for: %s", playbook["action_type"])
|
||||
self._write_heal_log(playbook, context, result, duration_ms)
|
||||
|
||||
def _write_heal_log(
|
||||
self,
|
||||
playbook: Dict[str, Any],
|
||||
context: Dict[str, Any],
|
||||
result: "AutoHealResult",
|
||||
duration_ms: float,
|
||||
) -> None:
|
||||
"""Write heal execution record to heal_logs table (ADR-013 Step 6/7)."""
|
||||
from database.autoheal_models import HealLog
|
||||
incident_id = context.get("incident_id")
|
||||
if not incident_id:
|
||||
self._log.warning("[AutoHeal] _write_heal_log: incident_id missing in context, skipping DB write")
|
||||
return
|
||||
session = get_session()
|
||||
try:
|
||||
log_entry = HealLog(
|
||||
incident_id=int(incident_id),
|
||||
playbook_id=int(playbook["id"]),
|
||||
action_type=playbook["action_type"],
|
||||
action_detail=json.dumps(playbook.get("action_params", {}), ensure_ascii=False),
|
||||
result="success" if result.success else "failed",
|
||||
result_output=result.message[:2000] if result.message else None,
|
||||
duration_ms=duration_ms,
|
||||
)
|
||||
session.add(log_entry)
|
||||
session.commit()
|
||||
self._log.info(
|
||||
"[AutoHeal] heal_log written: incident_id=%s playbook=%s result=%s duration_ms=%.1f",
|
||||
incident_id, playbook["id"], log_entry.result, duration_ms,
|
||||
)
|
||||
except Exception as exc:
|
||||
self._log.error("[AutoHeal] _write_heal_log DB error: %s", exc)
|
||||
session.rollback()
|
||||
finally:
|
||||
session.close()
|
||||
|
||||
@@ -7,10 +7,10 @@ Google Drive 服務模組
|
||||
|
||||
import os
|
||||
import io
|
||||
import json
|
||||
import logging
|
||||
from typing import List, Optional, Dict, Any
|
||||
from datetime import datetime
|
||||
import pickle
|
||||
|
||||
from google.auth.transport.requests import Request
|
||||
from google.oauth2.credentials import Credentials
|
||||
@@ -27,7 +27,8 @@ SCOPES = ['https://www.googleapis.com/auth/drive']
|
||||
|
||||
# 認證檔案路徑
|
||||
CREDENTIALS_FILE = 'config/google_credentials.json'
|
||||
TOKEN_FILE = 'config/google_token.pickle'
|
||||
TOKEN_FILE = 'config/google_token.json'
|
||||
_LEGACY_PICKLE_FILE = 'config/google_token.pickle'
|
||||
|
||||
|
||||
class GoogleDriveService:
|
||||
@@ -46,10 +47,18 @@ class GoogleDriveService:
|
||||
bool: 認證是否成功
|
||||
"""
|
||||
try:
|
||||
# 舊版 pickle token 遷移提示(不自動刪除舊檔)
|
||||
if os.path.exists(_LEGACY_PICKLE_FILE) and not os.path.exists(TOKEN_FILE):
|
||||
logger.warning(
|
||||
"[GoogleDrive] 偵測到舊版 token.pickle,已改用 JSON 格式。"
|
||||
"請重新執行認證流程以產生新 token,舊 pickle 檔案不會被自動刪除。"
|
||||
)
|
||||
|
||||
# 檢查是否已有 token
|
||||
if os.path.exists(TOKEN_FILE):
|
||||
with open(TOKEN_FILE, 'rb') as token:
|
||||
self.credentials = pickle.load(token)
|
||||
with open(TOKEN_FILE, 'r') as token:
|
||||
token_data = json.load(token)
|
||||
self.credentials = Credentials.from_authorized_user_info(token_data, SCOPES)
|
||||
|
||||
# 如果沒有有效憑證,進行認證流程
|
||||
if not self.credentials or not self.credentials.valid:
|
||||
@@ -70,9 +79,9 @@ class GoogleDriveService:
|
||||
# 對於「電腦版應用程式」類型,使用預設行為讓 Google 自動選擇埠號
|
||||
self.credentials = flow.run_local_server()
|
||||
|
||||
# 儲存憑證供下次使用
|
||||
with open(TOKEN_FILE, 'wb') as token:
|
||||
pickle.dump(self.credentials, token)
|
||||
# 儲存憑證供下次使用(JSON 格式,安全無 RCE 風險)
|
||||
with open(TOKEN_FILE, 'w') as token:
|
||||
token.write(self.credentials.to_json())
|
||||
logger.info(f"憑證已儲存到: {TOKEN_FILE}")
|
||||
|
||||
# 建立 Drive API 服務
|
||||
|
||||
@@ -176,17 +176,21 @@ class TrendTelegramBot:
|
||||
|
||||
async def cmd_start(self, update: Update, context):
|
||||
"""開始指令 - 顯示主選單"""
|
||||
user = update.effective_user
|
||||
await update.message.reply_text(
|
||||
f"👋 嗨,{user.first_name}!\n\n"
|
||||
f"我是 *MOMO 趨勢助手*,您的智能商業分析夥伴。\n"
|
||||
f"請選擇下方功能,或直接輸入問題:",
|
||||
reply_markup=self._get_main_menu_keyboard()
|
||||
)
|
||||
try:
|
||||
user = update.effective_user
|
||||
await update.message.reply_text(
|
||||
f"👋 嗨,{user.first_name}!\n\n"
|
||||
f"我是 *MOMO 趨勢助手*,您的智能商業分析夥伴。\n"
|
||||
f"請選擇下方功能,或直接輸入問題:",
|
||||
reply_markup=self._get_main_menu_keyboard()
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"[Bot] cmd_start 失敗: {e}", exc_info=True)
|
||||
|
||||
async def cmd_help(self, update: Update, context):
|
||||
"""說明指令"""
|
||||
help_text = """
|
||||
try:
|
||||
help_text = """
|
||||
📖 *指令說明*
|
||||
|
||||
*趨勢查詢*
|
||||
@@ -204,8 +208,10 @@ class TrendTelegramBot:
|
||||
|
||||
*每日摘要*
|
||||
/daily - 查看今日趨勢摘要
|
||||
"""
|
||||
await update.message.reply_text(help_text, parse_mode='Markdown')
|
||||
"""
|
||||
await update.message.reply_text(help_text, parse_mode='Markdown')
|
||||
except Exception as e:
|
||||
logger.error(f"[Bot] cmd_help 失敗: {e}", exc_info=True)
|
||||
|
||||
async def cmd_trend(self, update: Update, context):
|
||||
"""趨勢查詢指令"""
|
||||
|
||||
Reference in New Issue
Block a user