feat(group): 群組雙向對話 + 格式選項C + 老闆稱謂
All checks were successful
CD Pipeline / build-and-deploy (push) Successful in 7m11s
All checks were successful
CD Pipeline / build-and-deploy (push) Successful in 7m11s
1. _handle_group_message: SRE 群組訊息路由 - @OpenClawAwoooI_Bot → 只有 OpenClaw 回應 - @NemoTronAwoooI_Bot → 只有 NemoClaw 回應 - 一般訊息 → 並行回應 + 互相評論第二輪 - Bot 訊息自動忽略(防無限循環) 2. 告警格式改選項 C (老闆指示) - 【🔴 HIGH】resource_name - 區塊式,去掉 ═══ 長分隔線 3. AI persona 改稱呼用戶為「老闆」 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -25,15 +25,16 @@ from src.repositories.incident_repository import get_incident_repository
|
||||
|
||||
logger = structlog.get_logger(__name__)
|
||||
|
||||
OPENCLAW_PERSONA = """你是 OpenClaw,AWOOOI 平台的 SRE AI 主帥。
|
||||
OPENCLAW_PERSONA = """你是 OpenClaw,AWOOOI 平台的 SRE AI 首席顧問。
|
||||
個性: 精準、果斷、專業,像老將一樣直接給出建議。
|
||||
語氣: 簡短有力,不廢話。繁體中文回應。不超過 300 字。
|
||||
稱呼用戶為「老闆」。
|
||||
"""
|
||||
|
||||
NEMOCLAW_PERSONA = """你是 NemoClaw,AWOOOI 平台的 AI 戰術參謀,由 NVIDIA Nemotron 驅動。
|
||||
個性: 分析型、從不同角度思考,會質疑假設。
|
||||
語氣: 帶點挑釁但建設性,繁體中文回應。不超過 200 字。
|
||||
評論 OpenClaw 的回應時,直接說「我補充」或「我有不同看法」。
|
||||
稱呼用戶為「老闆」。評論 OpenClaw 的回應時,直接說「我補充」或「我有不同看法」。
|
||||
"""
|
||||
|
||||
|
||||
|
||||
@@ -1404,18 +1404,14 @@ class TelegramGateway:
|
||||
由 asyncio.create_task 非同步呼叫,失敗不影響主告警流程。
|
||||
"""
|
||||
try:
|
||||
# 2026-04-03 ogt: 群組告警格式對齊個人 chat 的 TelegramMessage 模板
|
||||
# 2026-04-03 ogt: 選項 C 區塊式格式(老闆指示)
|
||||
risk_emoji = RISK_EMOJI_MAP.get(risk_level.lower(), "⚪")
|
||||
summary = (
|
||||
f"═══════════════════════════\n"
|
||||
f"{risk_emoji} <b>{risk_level.upper()}</b> | <code>{html.escape(resource_name[:30])}</code>\n"
|
||||
f"═══════════════════════════\n"
|
||||
f"【{risk_emoji} {risk_level.upper()}】{html.escape(resource_name[:40])}\n\n"
|
||||
f"📋 <code>{html.escape(approval_id)}</code>\n"
|
||||
f"━━━━━━━━━━━━━━━━━━━\n"
|
||||
f"🔍 根因: {html.escape(root_cause[:120])}\n"
|
||||
f"🔧 建議: {html.escape(suggested_action[:80])}\n"
|
||||
f"━━━━━━━━━━━━━━━━━━━\n"
|
||||
f"⬇️ OpenClaw 與 NemoClaw 正在分析中..."
|
||||
f"🔍 {html.escape(root_cause[:150])}\n"
|
||||
f"🔧 {html.escape(suggested_action[:80])}\n\n"
|
||||
f"─ OpenClaw 與 NemoClaw 分析中 ─"
|
||||
)
|
||||
group_result = await self.send_to_group(text=summary)
|
||||
group_msg_id = (
|
||||
@@ -3076,21 +3072,28 @@ class TelegramGateway:
|
||||
)
|
||||
|
||||
async def _handle_chat_message(self, update_id: int, message: dict) -> None:
|
||||
"""處理统帥的文字訊息"""
|
||||
"""處理统帥的文字訊息(個人 chat 或 SRE 群組)"""
|
||||
text = message.get("text")
|
||||
user = message.get("from", {})
|
||||
user_id = user.get("id")
|
||||
chat_id = message.get("chat", {}).get("id")
|
||||
chat_type = message.get("chat", {}).get("type", "private")
|
||||
message_id = message.get("message_id")
|
||||
username = user.get("username") or user.get("first_name") or str(user_id)
|
||||
|
||||
if not text or not user_id:
|
||||
return
|
||||
|
||||
# Bot 訊息忽略(避免 Bot 互相觸發無限循環)
|
||||
if user.get("is_bot"):
|
||||
return
|
||||
|
||||
logger.info(
|
||||
"telegram_chat_received",
|
||||
update_id=update_id,
|
||||
user_id=user_id,
|
||||
username=username,
|
||||
chat_type=chat_type,
|
||||
text=text[:50],
|
||||
)
|
||||
|
||||
@@ -3103,17 +3106,11 @@ class TelegramGateway:
|
||||
return
|
||||
|
||||
# 2. /ai 指令攔截 (Phase 24 C — 2026-04-03 ogt)
|
||||
# 白名單: OPENCLAW_TG_USER_WHITELIST (與審核白名單共用)
|
||||
if text.strip().lower().startswith("/ai"):
|
||||
# C1 修復 (2026-04-03 首席架構師審查): Fail-Closed — 白名單空時拒絕所有人
|
||||
whitelist = settings.get_tg_user_whitelist()
|
||||
if not whitelist or user_id not in whitelist:
|
||||
logger.warning("telegram_ai_command_unauthorized", user_id=user_id, whitelist_empty=not whitelist)
|
||||
await self.send_notification(
|
||||
"⛔ 未授權:/ai 指令僅限白名單用戶",
|
||||
parse_mode="HTML",
|
||||
chat_id=chat_id,
|
||||
)
|
||||
await self.send_notification("⛔ 未授權:/ai 指令僅限白名單用戶", parse_mode="HTML", chat_id=chat_id)
|
||||
return
|
||||
from src.services.ai_control import handle_ai_command
|
||||
response = await handle_ai_command(text.strip())
|
||||
@@ -3121,20 +3118,126 @@ class TelegramGateway:
|
||||
logger.info("telegram_ai_command_handled", user_id=user_id, text=text[:50])
|
||||
return
|
||||
|
||||
# 3. 顯示 "正在輸入中..."
|
||||
# 3. 群組訊息路由 (2026-04-03 ogt: SRE 戰情室群組支援)
|
||||
# 群組裡 @ 指定 Bot 或直接發訊息 → 雙 AI 並行回應到群組
|
||||
is_group = chat_type in ("group", "supergroup")
|
||||
is_sre_group = str(chat_id) == str(settings.SRE_GROUP_CHAT_ID)
|
||||
|
||||
if is_group and is_sre_group:
|
||||
await self._handle_group_message(text, user_id, username, chat_id, message_id)
|
||||
return
|
||||
|
||||
# 4. 個人 chat — 顯示輸入狀態
|
||||
await self._send_chat_action(chat_id, "typing")
|
||||
|
||||
# 4. 呼叫 ChatManager 處理
|
||||
# 5. ChatManager 處理(個人 chat)
|
||||
chat_manager = get_chat_manager()
|
||||
response = await chat_manager.generate_response(
|
||||
user_id=user_id,
|
||||
username=username,
|
||||
message_text=text,
|
||||
)
|
||||
|
||||
# 5. 回覆統帥 (定向回傳)
|
||||
await self.send_notification(response, parse_mode="HTML", chat_id=chat_id)
|
||||
|
||||
async def _handle_group_message(
|
||||
self,
|
||||
text: str,
|
||||
user_id: int,
|
||||
username: str,
|
||||
chat_id: int, # noqa: ARG002
|
||||
message_id: int | None,
|
||||
) -> None:
|
||||
"""
|
||||
處理 SRE 群組訊息 (2026-04-03 ogt: Phase 22.6 Triumvirate)
|
||||
|
||||
路由規則:
|
||||
@OpenClawAwoooI_Bot <msg> → 只有 OpenClaw 回應
|
||||
@NemoTronAwoooI_Bot <msg> → 只有 NemoClaw 回應
|
||||
其他訊息 → 兩個 AI 並行回應,互相看到後可評論
|
||||
"""
|
||||
from src.services.chat_manager import get_chat_manager as _get_cm
|
||||
chat_mgr = _get_cm()
|
||||
|
||||
text_lower = text.lower()
|
||||
mention_openclaw = "@openclawawoooi_bot" in text_lower
|
||||
mention_nemo = "@nemotronawoooi_bot" in text_lower
|
||||
|
||||
# 去掉 @ mention 取出純訊息
|
||||
clean_text = text
|
||||
for mention in ["@openclawawoooi_bot", "@NemoTronAwoooI_Bot", "@OpenClawAwoooI_Bot", "@NemoTronAwoooI_Bot"]:
|
||||
clean_text = clean_text.replace(mention, "").strip()
|
||||
if not clean_text:
|
||||
clean_text = text
|
||||
|
||||
context = await chat_mgr.get_system_context()
|
||||
|
||||
if mention_openclaw and not mention_nemo:
|
||||
# 只 OpenClaw 回應
|
||||
result = await chat_mgr._call_openclaw(
|
||||
f"{context}\n用戶 {username} 在 SRE 戰情室問你:",
|
||||
clean_text,
|
||||
)
|
||||
await self.send_as_openclaw(
|
||||
text=f"🦞 <b>OpenClaw</b>\n\n{result or '🔴 無響應'}",
|
||||
reply_to_message_id=message_id,
|
||||
)
|
||||
|
||||
elif mention_nemo and not mention_openclaw:
|
||||
# 只 NemoClaw 回應
|
||||
result = await chat_mgr._call_nemotron(
|
||||
f"{context}\n用戶 {username} 在 SRE 戰情室問你:",
|
||||
clean_text,
|
||||
)
|
||||
await self.send_as_nemotron(
|
||||
text=f"🤖 <b>NemoClaw</b>\n\n{result or '🔴 無響應 (NIM 超時)'}",
|
||||
reply_to_message_id=message_id,
|
||||
)
|
||||
|
||||
else:
|
||||
# 兩個 AI 並行回應,完成後互相評論
|
||||
oc_task = asyncio.create_task(
|
||||
chat_mgr._call_openclaw(f"{context}\n用戶 {username} 在 SRE 戰情室:", clean_text)
|
||||
)
|
||||
nemo_task = asyncio.create_task(
|
||||
chat_mgr._call_nemotron(f"{context}\n用戶 {username} 在 SRE 戰情室:", clean_text)
|
||||
)
|
||||
oc_result, nemo_result = await asyncio.gather(oc_task, nemo_task, return_exceptions=True)
|
||||
|
||||
if oc_result and not isinstance(oc_result, Exception):
|
||||
await self.send_as_openclaw(
|
||||
text=f"🦞 <b>OpenClaw</b>\n\n{oc_result}",
|
||||
reply_to_message_id=message_id,
|
||||
)
|
||||
|
||||
if nemo_result and not isinstance(nemo_result, Exception):
|
||||
await self.send_as_nemotron(
|
||||
text=f"🤖 <b>NemoClaw</b>\n\n{nemo_result}",
|
||||
reply_to_message_id=message_id,
|
||||
)
|
||||
|
||||
# 互相評論:OpenClaw 看 Nemo 的回應,Nemo 看 OpenClaw 的回應
|
||||
if oc_result and nemo_result and not isinstance(oc_result, Exception) and not isinstance(nemo_result, Exception):
|
||||
oc_comment_task = asyncio.create_task(
|
||||
chat_mgr._call_openclaw(
|
||||
f"{context}\nNemoClaw 說:{nemo_result[:300]}\n你有什麼補充或不同看法?簡短回應。",
|
||||
clean_text,
|
||||
)
|
||||
)
|
||||
nemo_comment_task = asyncio.create_task(
|
||||
chat_mgr._call_nemotron(
|
||||
f"{context}\nOpenClaw 說:{oc_result[:300]}\n你有什麼補充或不同看法?簡短回應。",
|
||||
clean_text,
|
||||
)
|
||||
)
|
||||
oc_comment, nemo_comment = await asyncio.gather(oc_comment_task, nemo_comment_task, return_exceptions=True)
|
||||
|
||||
if oc_comment and not isinstance(oc_comment, Exception):
|
||||
await self.send_as_openclaw(text=f"🦞 <b>OpenClaw 補充</b>\n\n{oc_comment}")
|
||||
if nemo_comment and not isinstance(nemo_comment, Exception):
|
||||
await self.send_as_nemotron(text=f"🤖 <b>NemoClaw 補充</b>\n\n{nemo_comment}")
|
||||
|
||||
logger.info("group_message_handled", user_id=user_id, text=text[:50])
|
||||
|
||||
async def _send_chat_action(self, chat_id: int, action: str) -> None:
|
||||
"""發送聊天狀態 (e.g., typing)"""
|
||||
if not self._http_client: return
|
||||
|
||||
Reference in New Issue
Block a user