[V10.321] 修正 Telegram HTML br 發送格式
All checks were successful
CD Pipeline / deploy (push) Successful in 1m6s
All checks were successful
CD Pipeline / deploy (push) Successful in 1m6s
This commit is contained in:
@@ -4,7 +4,8 @@
|
||||
================================================================================
|
||||
|
||||
【已完成】
|
||||
- V10.320 補市場情報 candidate queue review AI summary Telegram dispatch report input:新增 read-only report input builder、獨立 report route extension、UI 按鈕與 deployment readiness smoke target,在 archive summary 後整理 report input sections、report contract、message evidence 與 dispatch audit traceability;同步把 Telegram HTML 送出前的 `<br>` 統一轉為換行,避免 Bot API 回 400。API/UI 不讀 approval/Telegram token、不呼叫 LLM、不派送 Telegram、不開 DB、不寫檔、不產報表、不更新 review_state、不掛 scheduler。
|
||||
- V10.321 修正 Telegram HTML 發送格式:所有 `sendMessage` / `sendPhoto` caption 在 HTML parse mode 送出前會把 `<br>` / `<br/>` / `<BR />` 統一轉成換行,避免 Telegram Bot API 回 `Unsupported start tag "br"` 造成告警或報告送出失敗。
|
||||
- V10.320 補市場情報 candidate queue review AI summary Telegram dispatch report input:新增 read-only report input builder、獨立 report route extension、UI 按鈕與 deployment readiness smoke target,在 archive summary 後整理 report input sections、report contract、message evidence 與 dispatch audit traceability;API/UI 不讀 approval/Telegram token、不呼叫 LLM、不派送 Telegram、不開 DB、不寫檔、不產報表、不更新 review_state、不掛 scheduler。
|
||||
- V10.319 補市場情報 candidate queue review AI summary Telegram dispatch archive summary:新增 read-only archive summary builder、POST endpoint、UI 按鈕與 deployment readiness smoke target,在 Telegram dispatch archive 後整理 message identity、dispatch audit、artifact manifest 與後續 report input sections;API/UI 不讀 approval/Telegram token、不呼叫 LLM、不派送 Telegram、不開 DB、不寫檔、不更新 review_state、不掛 scheduler。
|
||||
- V10.318 收緊 Elephant Alpha HITL 告警治理:`ea_escalation` 只有真正含 SKU/價格比較的 actions 才排成 TOP 待審 SKU 卡片;非 SKU 診斷改為「待確認事項」,並用測試鎖住價格類低信心但無 DB/Hermes 實證時只 suppress、不寫 human_review、不發 Telegram,避免空泛告警打擾人工審核。
|
||||
- V10.317 修正 PChome 比價覆蓋率分子:`fetch_competitor_coverage()` 的 valid_matches 改成 `ACTIVE + 有 MOMO 最新價` 商品與有效 PChome `identity_v2` 價格的交集,不再把非活躍或無 MOMO 現價的舊 competitor_prices 列入覆蓋率,避免 daily/growth/PPT/AI 報表高估比價資料品質。
|
||||
|
||||
@@ -320,7 +320,7 @@ YOUTUBE_API_KEY = os.getenv('YOUTUBE_API_KEY', '')
|
||||
# ==========================================
|
||||
# 系統版本與路徑
|
||||
# ==========================================
|
||||
SYSTEM_VERSION = "V10.320"
|
||||
SYSTEM_VERSION = "V10.321"
|
||||
LOG_FILE_PATH = os.path.join(BASE_DIR, 'logs/system.log')
|
||||
public_url = PUBLIC_URL # 用於模板顯示
|
||||
|
||||
|
||||
@@ -610,3 +610,4 @@ POSTGRES_HOST=momo-db
|
||||
| 2026-05-20 | 指定日期競品簡報可能混用目前 `competitor_prices` 快取價 | V10.315 起 `fetch_competitor_comparison_results()` 有 start/end date 時改用 `competitor_price_history` 期間快照,MOMO 價格取報表結束日前最新價;即時報表才使用目前有效 `competitor_prices` |
|
||||
| 2026-05-20 | PChome 覆蓋率分子可能被非活躍或無 MOMO 現價 SKU 膨脹 | V10.317 起 `fetch_competitor_coverage()` 的 `valid_matches` 改為 active MOMO latest price 與有效 PChome `identity_v2` 價格交集,確保 daily/growth/PPT/AI 看到的比價資料品質不被舊快取列高估 |
|
||||
| 2026-05-20 | EA HITL 告警可能把非 SKU 診斷誤排成待審 SKU,或在缺少 DB/Hermes 實證時打擾人工 | V10.318 起 `ea_escalation` 僅對含 SKU/價格比較的 actions 使用競價卡片;非 SKU 診斷改為「待確認事項」。價格類低信心事件若無 DB/Hermes 實證,測試鎖定只 suppress、不寫 human_review、不發 Telegram |
|
||||
| 2026-05-20 | Telegram HTML parse mode 不支援 `<br>`,可能導致告警或報告送出 400 | V10.321 起 Telegram template 發送前會把 `<br>` / `<br/>` / `<BR />` 轉為換行;保留其他 HTML 標籤,非 HTML parse mode 不改寫 |
|
||||
|
||||
@@ -30,6 +30,7 @@ sys_log = logging.getLogger("TelegramTpl")
|
||||
|
||||
TELEGRAM_BOT_TOKEN_ENV = "TELEGRAM_BOT_TOKEN"
|
||||
TELEGRAM_CHAT_IDS_ENV = "TELEGRAM_CHAT_IDS"
|
||||
_TELEGRAM_HTML_BR_RE = re.compile(r"<\s*br\s*/?\s*>", re.IGNORECASE)
|
||||
|
||||
|
||||
# ══════════════════════════════════════════════════════════════════════════════
|
||||
@@ -55,6 +56,14 @@ def _get_chat_ids() -> list:
|
||||
return []
|
||||
|
||||
|
||||
def _sanitize_telegram_html(text: str, parse_mode: Optional[str] = "HTML") -> str:
|
||||
"""Telegram HTML 不支援 <br>,統一轉為換行避免 sendMessage 400。"""
|
||||
value = str(text or "")
|
||||
if parse_mode and str(parse_mode).upper() == "HTML":
|
||||
return _TELEGRAM_HTML_BR_RE.sub("\n", value)
|
||||
return value
|
||||
|
||||
|
||||
def _send_telegram_raw(text: str, chat_ids: Optional[list] = None,
|
||||
reply_markup: Optional[Dict[str, Any]] = None,
|
||||
parse_mode: Optional[str] = "HTML") -> bool:
|
||||
@@ -69,7 +78,7 @@ def _send_telegram_raw(text: str, chat_ids: Optional[list] = None,
|
||||
chat_ids = [-1003940688311] # fallback
|
||||
|
||||
url = f"https://api.telegram.org/bot{token}/sendMessage"
|
||||
payload = {"chat_id": chat_ids[0], "text": text}
|
||||
payload = {"chat_id": chat_ids[0], "text": _sanitize_telegram_html(text, parse_mode)}
|
||||
if parse_mode:
|
||||
payload["parse_mode"] = parse_mode
|
||||
if reply_markup:
|
||||
@@ -106,7 +115,7 @@ def send_telegram_with_result(text: str, chat_ids: Optional[list] = None,
|
||||
errors: List[str] = []
|
||||
|
||||
for chat_id in chat_ids:
|
||||
payload = {"chat_id": chat_id, "text": text}
|
||||
payload = {"chat_id": chat_id, "text": _sanitize_telegram_html(text, parse_mode)}
|
||||
if parse_mode:
|
||||
payload["parse_mode"] = parse_mode
|
||||
if reply_markup:
|
||||
@@ -151,7 +160,7 @@ def send_photo(photo_bytes: bytes, caption: str = "",
|
||||
try:
|
||||
r = requests.post(
|
||||
url,
|
||||
data={"chat_id": chat_ids[0], "caption": caption[:1024],
|
||||
data={"chat_id": chat_ids[0], "caption": _sanitize_telegram_html(caption, parse_mode)[:1024],
|
||||
"parse_mode": parse_mode},
|
||||
files={"photo": ("chart.png", photo_bytes, "image/png")},
|
||||
timeout=30,
|
||||
|
||||
@@ -1,4 +1,41 @@
|
||||
from services.telegram_templates import triaged_alert
|
||||
from services import telegram_templates
|
||||
from services.telegram_templates import _sanitize_telegram_html, triaged_alert
|
||||
|
||||
|
||||
def test_telegram_html_sanitizer_converts_br_tags_to_newlines():
|
||||
msg = _sanitize_telegram_html("第一行<br>第二行<br/>第三行<BR />第四行")
|
||||
|
||||
assert "<br" not in msg.lower()
|
||||
assert msg == "第一行\n第二行\n第三行\n第四行"
|
||||
assert _sanitize_telegram_html("第一行<br>第二行", parse_mode=None) == "第一行<br>第二行"
|
||||
|
||||
|
||||
def test_send_telegram_with_result_sanitizes_html_payload(monkeypatch):
|
||||
sent_payloads = []
|
||||
|
||||
class Response:
|
||||
ok = True
|
||||
status_code = 200
|
||||
text = "ok"
|
||||
|
||||
def fake_post(url, json=None, timeout=None):
|
||||
sent_payloads.append({"url": url, "json": json, "timeout": timeout})
|
||||
return Response()
|
||||
|
||||
monkeypatch.setattr(telegram_templates, "_get_bot_token", lambda: "telegram-token")
|
||||
monkeypatch.setattr("requests.post", fake_post)
|
||||
|
||||
result = telegram_templates.send_telegram_with_result(
|
||||
"第一行<br>第二行<br/>第三行",
|
||||
chat_ids=[101, 202],
|
||||
parse_mode="HTML",
|
||||
)
|
||||
|
||||
assert result["ok"] is True
|
||||
assert result["sent"] == 2
|
||||
assert [item["json"]["chat_id"] for item in sent_payloads] == [101, 202]
|
||||
assert all(item["json"]["text"] == "第一行\n第二行\n第三行" for item in sent_payloads)
|
||||
assert all(item["json"]["parse_mode"] == "HTML" for item in sent_payloads)
|
||||
|
||||
|
||||
def test_ea_escalation_uses_structured_incident_brief():
|
||||
|
||||
Reference in New Issue
Block a user