fix(api): surface Work Item handoff in Telegram cards
This commit is contained in:
@@ -40,10 +40,12 @@ from src.core.config import settings
|
||||
from src.core.redis_client import get_redis
|
||||
from src.services.awooop_ansible_audit_service import summarize_ansible_execution
|
||||
from src.services.awooop_deeplinks import (
|
||||
AWOOOP_WEB_BASE_URL,
|
||||
incident_alerts_url,
|
||||
incident_runs_url,
|
||||
incident_truth_chain_button_row,
|
||||
incident_truth_chain_reply_markup,
|
||||
work_item_url,
|
||||
)
|
||||
from src.services.approval_action_classifier import (
|
||||
is_executable_repair_approval_action,
|
||||
@@ -983,6 +985,58 @@ def _format_manual_asset_ledger_lines(*, compact: bool = False) -> list[str]:
|
||||
]
|
||||
|
||||
|
||||
def _normalize_awooop_url(url: str) -> str:
|
||||
raw = str(url or "").strip()
|
||||
if not raw:
|
||||
return ""
|
||||
if raw.startswith(("https://", "http://")):
|
||||
return raw
|
||||
if raw.startswith("/awooop/"):
|
||||
return f"{AWOOOP_WEB_BASE_URL}/zh-TW{raw}"
|
||||
if raw.startswith("/"):
|
||||
return f"{AWOOOP_WEB_BASE_URL}{raw}"
|
||||
return raw
|
||||
|
||||
|
||||
def _repair_candidate_work_item_url(
|
||||
*,
|
||||
incident_id: str = "",
|
||||
repair_candidate_work_item_href: str = "",
|
||||
repair_candidate_work_item_id: str = "",
|
||||
) -> str:
|
||||
normalized_href = _normalize_awooop_url(repair_candidate_work_item_href)
|
||||
if normalized_href:
|
||||
return normalized_href
|
||||
if repair_candidate_work_item_id:
|
||||
return work_item_url(
|
||||
repair_candidate_work_item_id,
|
||||
incident_id=incident_id,
|
||||
project_id="awoooi",
|
||||
)
|
||||
return ""
|
||||
|
||||
|
||||
def _repair_candidate_work_item_refs_from_metadata(metadata: object) -> tuple[str, str]:
|
||||
if not isinstance(metadata, dict):
|
||||
return "", ""
|
||||
package = metadata.get("repair_candidate_draft_package")
|
||||
if not isinstance(package, dict):
|
||||
package = {}
|
||||
work_item = package.get("awooop_work_item")
|
||||
if not isinstance(work_item, dict):
|
||||
work_item = {}
|
||||
href = (
|
||||
str(work_item.get("work_item_url") or "")
|
||||
or str(work_item.get("work_item_href") or "")
|
||||
or str(metadata.get("repair_candidate_work_item_href") or "")
|
||||
)
|
||||
work_item_id = (
|
||||
str(work_item.get("work_item_id") or "")
|
||||
or str(metadata.get("repair_candidate_work_item_id") or "")
|
||||
)
|
||||
return href, work_item_id
|
||||
|
||||
|
||||
def _format_manual_handoff_package_lines(
|
||||
*,
|
||||
incident_id: str,
|
||||
@@ -1064,7 +1118,12 @@ def _format_manual_handoff_package_lines(
|
||||
f"├ PlayBook 草案欄位:<code>{html.escape(field_text)}</code>",
|
||||
)
|
||||
insert_at += 1
|
||||
if repair_candidate_work_item_href:
|
||||
work_item_deep_link = _repair_candidate_work_item_url(
|
||||
incident_id=incident_ref,
|
||||
repair_candidate_work_item_href=repair_candidate_work_item_href,
|
||||
repair_candidate_work_item_id=repair_candidate_work_item_id,
|
||||
)
|
||||
if work_item_deep_link:
|
||||
work_item_label = "AwoooP 修復候選草案"
|
||||
if repair_candidate_work_item_id:
|
||||
work_item_label = (
|
||||
@@ -1075,7 +1134,7 @@ def _format_manual_handoff_package_lines(
|
||||
insert_at,
|
||||
(
|
||||
"├ 工作項目:"
|
||||
f"<a href=\"{html.escape(str(repair_candidate_work_item_href), quote=True)}\">"
|
||||
f"<a href=\"{html.escape(str(work_item_deep_link), quote=True)}\">"
|
||||
f"{work_item_label}</a>"
|
||||
),
|
||||
)
|
||||
@@ -1088,7 +1147,13 @@ def _format_manual_handoff_package_lines(
|
||||
),
|
||||
)
|
||||
if not compact:
|
||||
lines.append("按鈕:<b>處置包</b> 看完整證據,<b>重診</b> 重新收集,<b>Runs</b> 追蹤狀態")
|
||||
if work_item_deep_link:
|
||||
lines.append(
|
||||
"按鈕:<b>Work Item</b> 開啟 owner review,"
|
||||
"<b>處置包</b> 看完整證據,<b>Runs</b> 追蹤狀態"
|
||||
)
|
||||
else:
|
||||
lines.append("按鈕:<b>處置包</b> 看完整證據,<b>重診</b> 重新收集,<b>Runs</b> 追蹤狀態")
|
||||
return lines
|
||||
|
||||
|
||||
@@ -4854,6 +4919,8 @@ class TelegramGateway:
|
||||
notification_type: str = "",
|
||||
# 2026-04-27 Claude Sonnet 4.6: B3 — LLM 動態按鈕(ActionPlan,可選)
|
||||
action_plan: object = None,
|
||||
repair_candidate_work_item_href: str = "",
|
||||
repair_candidate_work_item_id: str = "",
|
||||
) -> dict:
|
||||
"""
|
||||
建立 Inline Keyboard
|
||||
@@ -4901,11 +4968,21 @@ class TelegramGateway:
|
||||
info_row: list[dict] = []
|
||||
secondary_row: list[dict] = []
|
||||
if incident_id:
|
||||
work_item_deep_link = _repair_candidate_work_item_url(
|
||||
incident_id=incident_id,
|
||||
repair_candidate_work_item_href=repair_candidate_work_item_href,
|
||||
repair_candidate_work_item_id=repair_candidate_work_item_id,
|
||||
)
|
||||
if work_item_deep_link:
|
||||
info_row.append({
|
||||
"text": "🧾 Work Item",
|
||||
"url": work_item_deep_link,
|
||||
})
|
||||
info_row.extend([
|
||||
{"text": "🧰 處置包", "callback_data": f"detail:{incident_id}"},
|
||||
{"text": "🔄 重診", "callback_data": f"reanalyze:{incident_id}"},
|
||||
])
|
||||
secondary_row.extend([
|
||||
{"text": "🔄 重診", "callback_data": f"reanalyze:{incident_id}"},
|
||||
{"text": "📊 歷史", "callback_data": f"history:{incident_id}"},
|
||||
{"text": "🔕 靜默", "callback_data": silence_nonce},
|
||||
])
|
||||
@@ -5369,6 +5446,8 @@ class TelegramGateway:
|
||||
suggested_action=suggested_action,
|
||||
alert_category=alert_category,
|
||||
notification_type=notification_type,
|
||||
repair_candidate_work_item_href=repair_candidate_work_item_href,
|
||||
repair_candidate_work_item_id=repair_candidate_work_item_id,
|
||||
)
|
||||
|
||||
# 發送訊息:2026-04-30 統帥指示,告警卡片完整切到 SRE 戰情室群組。
|
||||
@@ -9964,6 +10043,8 @@ class TelegramGateway:
|
||||
username: str,
|
||||
execution_triggered: bool,
|
||||
approval_action: str | None = None,
|
||||
repair_candidate_work_item_href: str = "",
|
||||
repair_candidate_work_item_id: str = "",
|
||||
) -> None:
|
||||
"""
|
||||
2026-04-09 Claude Sonnet 4.6: 批准/拒絕後立即更新 Telegram 訊息狀態。
|
||||
@@ -10005,7 +10086,16 @@ class TelegramGateway:
|
||||
)
|
||||
if no_action_approval:
|
||||
status_emoji = "🟠"
|
||||
suffix = "已轉人工處置包;請按處置包或重診補修復候選,這不是執行中"
|
||||
work_item_deep_link = _repair_candidate_work_item_url(
|
||||
incident_id=incident_id,
|
||||
repair_candidate_work_item_href=repair_candidate_work_item_href,
|
||||
repair_candidate_work_item_id=repair_candidate_work_item_id,
|
||||
)
|
||||
suffix = (
|
||||
"已轉 owner review;請開 Work Item / 處置包補齊修復候選,這不是執行中"
|
||||
if work_item_deep_link
|
||||
else "已轉人工處置包;請按處置包或重診補修復候選,這不是執行中"
|
||||
)
|
||||
else:
|
||||
suffix = "⚡ 執行中..." if execution_triggered else "已簽核,等待更多簽核"
|
||||
else:
|
||||
@@ -10020,12 +10110,27 @@ class TelegramGateway:
|
||||
try:
|
||||
# 1. 移除批准/拒絕按鈕(只保留資訊按鈕列)
|
||||
if no_action_approval:
|
||||
info_buttons = [[
|
||||
{"text": "🧰 處置包", "callback_data": f"detail:{incident_id}"},
|
||||
{"text": "🔄 重診", "callback_data": f"reanalyze:{incident_id}"},
|
||||
], [
|
||||
{"text": "📊 歷史", "callback_data": f"history:{incident_id}"},
|
||||
]]
|
||||
no_action_first_row: list[dict[str, str]] = []
|
||||
work_item_deep_link = _repair_candidate_work_item_url(
|
||||
incident_id=incident_id,
|
||||
repair_candidate_work_item_href=repair_candidate_work_item_href,
|
||||
repair_candidate_work_item_id=repair_candidate_work_item_id,
|
||||
)
|
||||
if work_item_deep_link:
|
||||
no_action_first_row.append({
|
||||
"text": "🧾 Work Item",
|
||||
"url": work_item_deep_link,
|
||||
})
|
||||
no_action_first_row.append(
|
||||
{"text": "🧰 處置包", "callback_data": f"detail:{incident_id}"}
|
||||
)
|
||||
info_buttons = [
|
||||
no_action_first_row,
|
||||
[
|
||||
{"text": "🔄 重診", "callback_data": f"reanalyze:{incident_id}"},
|
||||
{"text": "📊 歷史", "callback_data": f"history:{incident_id}"},
|
||||
],
|
||||
]
|
||||
else:
|
||||
info_buttons = [[
|
||||
{"text": "📋 詳情", "callback_data": f"detail:{incident_id}"},
|
||||
@@ -10132,6 +10237,9 @@ class TelegramGateway:
|
||||
# 2026-04-22 Claude Sonnet 4.6: 只有真正轉為 APPROVED 才發「執行中...」
|
||||
# 非 PENDING 狀態下 sign_approval early-return → approval 是舊 record
|
||||
# 此時不應發「執行中...」,應告知用戶告警已處理過
|
||||
_work_item_href, _work_item_id = _repair_candidate_work_item_refs_from_metadata(
|
||||
getattr(approval, "metadata", None)
|
||||
)
|
||||
if approval.status == ApprovalStatus.APPROVED and execution_triggered:
|
||||
_execution_allowed = not is_no_action_approval_action(
|
||||
getattr(approval, "action", None)
|
||||
@@ -10144,6 +10252,8 @@ class TelegramGateway:
|
||||
username=username,
|
||||
execution_triggered=execution_triggered and _execution_allowed,
|
||||
approval_action=getattr(approval, "action", None),
|
||||
repair_candidate_work_item_href=_work_item_href,
|
||||
repair_candidate_work_item_id=_work_item_id,
|
||||
)
|
||||
elif (
|
||||
approval.status == ApprovalStatus.APPROVED
|
||||
@@ -10156,6 +10266,8 @@ class TelegramGateway:
|
||||
username=username,
|
||||
execution_triggered=False,
|
||||
approval_action=getattr(approval, "action", None),
|
||||
repair_candidate_work_item_href=_work_item_href,
|
||||
repair_candidate_work_item_id=_work_item_id,
|
||||
)
|
||||
else:
|
||||
# 告警已是 execution_failed / execution_success / rejected 等終態
|
||||
|
||||
Reference in New Issue
Block a user