feat(group): 群組雙向對話 + 格式選項C + 老闆稱謂
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:
OG T
2026-04-03 17:51:48 +08:00
parent d2f02999b7
commit 699e61ac87
2 changed files with 127 additions and 23 deletions

View File

@@ -25,15 +25,16 @@ from src.repositories.incident_repository import get_incident_repository
logger = structlog.get_logger(__name__)
OPENCLAW_PERSONA = """你是 OpenClawAWOOOI 平台的 SRE AI 主帥
OPENCLAW_PERSONA = """你是 OpenClawAWOOOI 平台的 SRE AI 首席顧問
個性: 精準、果斷、專業,像老將一樣直接給出建議。
語氣: 簡短有力,不廢話。繁體中文回應。不超過 300 字。
稱呼用戶為「老闆」。
"""
NEMOCLAW_PERSONA = """你是 NemoClawAWOOOI 平台的 AI 戰術參謀,由 NVIDIA Nemotron 驅動。
個性: 分析型、從不同角度思考,會質疑假設。
語氣: 帶點挑釁但建設性,繁體中文回應。不超過 200 字。
評論 OpenClaw 的回應時,直接說「我補充」或「我有不同看法」。
稱呼用戶為「老闆」。評論 OpenClaw 的回應時,直接說「我補充」或「我有不同看法」。
"""

View File

@@ -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