feat(telegram): expose alert learning registry readback
Some checks failed
CD Pipeline / workflow-shape (push) Successful in 1s
CD Pipeline / cancel-stale-cd (push) Has been skipped
CD Pipeline / tests (push) Successful in 1m11s
CD Pipeline / post-deploy-checks (push) Has been cancelled
CD Pipeline / build-and-deploy (push) Has been cancelled

This commit is contained in:
Your Name
2026-07-02 20:20:48 +08:00
parent 0a7edbf3ce
commit 32d0109dcd
9 changed files with 524 additions and 13 deletions

View File

@@ -554,11 +554,35 @@ interface AiAlertCardDeliverySummary {
delivery_receipt_required_total: number;
runtime_write_gate_open_count: number;
runtime_write_allowed: boolean;
learning_writeback_ready_total?: number;
learning_writeback_missing_total?: number;
km_writeback_ref_ready_total?: number;
playbook_writeback_ref_ready_total?: number;
rag_chunk_ref_ready_total?: number;
mcp_evidence_ref_ready_total?: number;
post_verifier_ref_ready_total?: number;
learning_registry_target_count?: number;
learning_registry_ready_target_count?: number;
learning_registry_missing_target_count?: number;
learning_registry_next_action?: string;
latest_sent_at?: string | null;
latest_queued_at?: string | null;
production_write_count: number;
}
interface AiAlertCardLearningRegistryBinding {
target: string;
status: string;
consumer_surface: string;
source_field: string;
ref?: string | null;
latest_ref?: string | null;
ready_receipt_count?: number;
target_write_performed?: boolean;
metadata_only?: boolean;
next_action?: string;
}
interface AiAlertCardDeliveryItem {
message_id: string;
run_id: string;
@@ -578,6 +602,10 @@ interface AiAlertCardDeliveryItem {
runtime_write_allowed: boolean;
candidate_only: boolean;
delivery_receipt_readback_required: boolean;
learning_writeback_ready?: boolean;
learning_writeback_status?: string;
learning_writeback_refs?: Record<string, unknown>;
learning_registry_bindings?: AiAlertCardLearningRegistryBinding[];
source_refs: Record<string, unknown>;
run_state?: string | null;
agent_id?: string | null;
@@ -585,12 +613,29 @@ interface AiAlertCardDeliveryItem {
run_detail_href?: string | null;
}
interface AiAlertCardLearningRegistry {
schema_version: string;
status: string;
work_item_id: string;
project_id: string;
source_endpoint: string;
consumer_context_route: string;
target_count: number;
ready_target_count: number;
missing_target_count: number;
ready_receipt_ref_count: number;
next_action: string;
targets: AiAlertCardLearningRegistryBinding[];
operation_boundaries?: Record<string, boolean>;
}
interface AiAlertCardsResponse {
items?: AiAlertCardDeliveryItem[];
total?: number;
page?: number;
per_page?: number;
summary?: AiAlertCardDeliverySummary | null;
learning_registry?: AiAlertCardLearningRegistry | null;
}
interface AiRoutePolicyItem {
@@ -3620,11 +3665,13 @@ function CallbackReplyEvidencePanel({
function AiAlertCardDeliveryPanel({
items,
summary,
learningRegistry,
loading,
error,
}: {
items: AiAlertCardDeliveryItem[];
summary?: AiAlertCardDeliverySummary | null;
learningRegistry?: AiAlertCardLearningRegistry | null;
loading: boolean;
error: string | null;
}) {
@@ -3640,6 +3687,17 @@ function AiAlertCardDeliveryPanel({
const failed = summary?.failed_total ?? 0;
const pending = summary?.pending_total ?? 0;
const runtimeGateOpen = summary?.runtime_write_gate_open_count ?? 0;
const registryReady = learningRegistry?.ready_target_count
?? summary?.learning_registry_ready_target_count
?? 0;
const registryTotal = learningRegistry?.target_count
?? summary?.learning_registry_target_count
?? 0;
const registryStatus = registryTotal > 0 && registryReady === registryTotal
? "Registry ready"
: registryTotal > 0
? "Registry waiting"
: "Registry empty";
const statusLabel = total === 0
? "尚無送達讀回"
: failed > 0
@@ -3664,6 +3722,14 @@ function AiAlertCardDeliveryPanel({
const targetLabel = (target: string) => ({
wazuh_dashboard_api: "Wazuh 儀表板/API 讀回",
}[target] ?? "未分類目標");
const registryTargetLabel = (target: string) => ({
km: "KM",
playbook: "PlayBook",
rag: "RAG",
mcp: "MCP",
verifier: "Verifier",
ai_agent: "AI Agent",
}[target] ?? target);
return (
<section
@@ -3688,6 +3754,9 @@ function AiAlertCardDeliveryPanel({
<span className="border border-[#d8d3c7] bg-white px-2 py-0.5 text-xs font-semibold text-[#5f5b52]">
{runtimeGateOpen}
</span>
<span className="border border-[#9bb6d9] bg-[#eef5ff] px-2 py-0.5 text-xs font-semibold text-[#1f5b9b]">
{registryReady}/{registryTotal} registry
</span>
</div>
</div>
@@ -3696,7 +3765,11 @@ function AiAlertCardDeliveryPanel({
{ label: "總數", value: total, detail: `最新 ${latest}` },
{ label: "已送達", value: sent, detail: "Telegram 鏡像已送達" },
{ label: "失敗", value: failed, detail: "需查送達錯誤" },
{ label: "等待", value: pending, detail: `影子 ${summary?.shadow_total ?? 0}` },
{
label: "Registry",
value: registryReady,
detail: `${registryStatus} · refs ${learningRegistry?.ready_receipt_ref_count ?? 0}`,
},
].map((metric) => (
<div key={metric.label} className="bg-white px-4 py-3">
<p className="text-xs font-semibold text-[#77736a]">{metric.label}</p>
@@ -3706,6 +3779,46 @@ function AiAlertCardDeliveryPanel({
))}
</div>
{learningRegistry && (
<div className="border-b border-[#e0ddd4] bg-[#fbfcfb] px-4 py-4">
<div className="flex flex-wrap items-center justify-between gap-2">
<div>
<p className="text-sm font-semibold text-[#141413]">AI Loop learning registry</p>
<p className="mt-1 text-xs text-[#77736a]">
{learningRegistry.consumer_context_route} · {learningRegistry.next_action}
</p>
</div>
<span className="border border-[#cbdccf] bg-white px-2 py-0.5 text-xs font-semibold text-[#17602a]">
{learningRegistry.ready_target_count}/{learningRegistry.target_count}
</span>
</div>
<div className="mt-3 grid gap-px bg-[#e0ddd4] sm:grid-cols-2 xl:grid-cols-6">
{learningRegistry.targets.map((target) => {
const ready = target.status === "ready_for_consumer_context";
return (
<div key={target.target} className="bg-white px-3 py-2">
<p className="text-xs font-semibold text-[#141413]">
{registryTargetLabel(target.target)}
</p>
<p className={cn(
"mt-1 text-xs font-semibold",
ready ? "text-[#17602a]" : "text-[#8a5a08]"
)}>
{ready ? "ready" : "waiting"} · {target.ready_receipt_count ?? 0}
</p>
<p
className="mt-1 truncate font-mono text-[11px] leading-4 text-[#77736a]"
title={target.latest_ref ?? target.ref ?? target.consumer_surface}
>
{target.latest_ref ?? target.ref ?? target.consumer_surface}
</p>
</div>
);
})}
</div>
</div>
)}
{error ? (
<div className="px-4 py-4 text-sm text-[#9f2f25]">{error}</div>
) : items.length > 0 ? (
@@ -3750,6 +3863,14 @@ function AiAlertCardDeliveryPanel({
<p><span className="font-semibold">{targetLabel(item.target)}</span></p>
<p><span className="font-mono">{sourceRefCount}</span></p>
<p><span className="font-mono">{item.runtime_write_gate_count}</span></p>
<p>
Registry
<span className="font-mono">
{item.learning_registry_bindings?.filter((binding) => (
binding.status === "ready_for_consumer_context"
)).length ?? 0}
</span>
</p>
</div>
<Link
href={runHref as never}
@@ -4240,6 +4361,8 @@ export default function RunsPage() {
const [callbackEventsError, setCallbackEventsError] = useState<string | null>(null);
const [aiAlertCardItems, setAiAlertCardItems] = useState<AiAlertCardDeliveryItem[]>([]);
const [aiAlertCardSummary, setAiAlertCardSummary] = useState<AiAlertCardDeliverySummary | null>(null);
const [aiAlertLearningRegistry, setAiAlertLearningRegistry] =
useState<AiAlertCardLearningRegistry | null>(null);
const [aiAlertCardLoading, setAiAlertCardLoading] = useState(true);
const [aiAlertCardError, setAiAlertCardError] = useState<string | null>(null);
const [aiRouteStatus, setAiRouteStatus] = useState<AiRouteStatusResponse | null>(null);
@@ -4437,15 +4560,18 @@ export default function RunsPage() {
const aiAlertData: AiAlertCardsResponse = await aiAlertRes.json();
setAiAlertCardItems(Array.isArray(aiAlertData.items) ? aiAlertData.items : []);
setAiAlertCardSummary(aiAlertData.summary ?? null);
setAiAlertLearningRegistry(aiAlertData.learning_registry ?? null);
setAiAlertCardError(null);
} else {
setAiAlertCardItems([]);
setAiAlertCardSummary(null);
setAiAlertLearningRegistry(null);
setAiAlertCardError(`端點尚未部署或暫時不可用HTTP ${aiAlertRes.status}`);
}
} catch (aiAlertError) {
setAiAlertCardItems([]);
setAiAlertCardSummary(null);
setAiAlertLearningRegistry(null);
setAiAlertCardError("無法讀回 AI 事件卡送達狀態");
} finally {
setAiAlertCardLoading(false);
@@ -4794,6 +4920,7 @@ export default function RunsPage() {
<AiAlertCardDeliveryPanel
items={aiAlertCardItems}
summary={aiAlertCardSummary}
learningRegistry={aiAlertLearningRegistry}
loading={aiAlertCardLoading}
error={aiAlertCardError}
/>