fix(awooop): cache heavy operator summaries
Some checks failed
CD Pipeline / tests (push) Successful in 1m28s
Code Review / ai-code-review (push) Successful in 14s
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-06-01 09:20:18 +08:00
parent 0e30171858
commit 159f514f55
9 changed files with 381 additions and 30 deletions

View File

@@ -203,6 +203,16 @@ interface CallbackReplyAuditSummary {
latest_callback_at?: string | null;
}
interface OperatorSummaryCacheInfo {
schema_version?: string;
status?: "hit" | "miss" | string;
source?: string;
ttl_seconds?: number;
age_seconds?: number;
stored_at?: string;
expires_at?: string;
}
interface Run {
run_id: string;
project_id: string;
@@ -471,6 +481,7 @@ interface CallbackRepliesResponse {
page: number;
per_page: number;
summary?: CallbackReplyAuditSummary | null;
cache?: OperatorSummaryCacheInfo | null;
}
interface AiRoutePolicyItem {
@@ -2769,15 +2780,25 @@ function CallbackReplyEvidencePanel({
events,
total,
summary,
cache,
loading,
error,
}: {
events: CallbackReplyEvent[];
total: number;
summary?: CallbackReplyAuditSummary | null;
cache?: OperatorSummaryCacheInfo | null;
loading: boolean;
error: string | null;
}) {
const t = useTranslations("awooop.callbackReply.events");
const tCallback = useTranslations("awooop.callbackReply");
const cacheAge = Math.max(0, Math.round(cache?.age_seconds ?? 0));
const cacheLabel = cache
? cache.status === "hit"
? t("cacheHit", { age: cacheAge, ttl: cache.ttl_seconds ?? 0 })
: t("cacheMiss", { age: cacheAge, ttl: cache.ttl_seconds ?? 0 })
: null;
return (
<section id="tg-callback-evidence" className="border border-[#e0ddd4] bg-white">
@@ -2789,9 +2810,16 @@ function CallbackReplyEvidencePanel({
<p className="text-xs text-[#77736a]">{t("subtitle")}</p>
</div>
</div>
<span className="border border-[#9bb6d9] bg-[#eef5ff] px-2 py-0.5 text-xs font-semibold text-[#1f5b9b]">
{t("total", { count: total })}
</span>
<div className="flex flex-wrap items-center gap-2">
{cacheLabel ? (
<span className="border border-[#d8d3c7] bg-white px-2 py-0.5 text-xs font-semibold text-[#5f5b52]">
{cacheLabel}
</span>
) : null}
<span className="border border-[#9bb6d9] bg-[#eef5ff] px-2 py-0.5 text-xs font-semibold text-[#1f5b9b]">
{t("total", { count: total })}
</span>
</div>
</div>
<CallbackReplyAuditSummaryPanel summary={summary} />
@@ -2800,11 +2828,7 @@ function CallbackReplyEvidencePanel({
<div className="px-4 py-4 text-sm text-[#9f2f25]">
{t("error", { error })}
</div>
) : events.length === 0 ? (
<div className="px-4 py-4 text-sm text-[#5f5b52]">
{t("empty")}
</div>
) : (
) : events.length > 0 ? (
<div className="grid gap-px bg-[#eee9dd] md:grid-cols-2 xl:grid-cols-3">
{events.slice(0, 6).map((event) => {
const status = normalizeCallbackReplyEventStatus(event.status);
@@ -2882,6 +2906,14 @@ function CallbackReplyEvidencePanel({
);
})}
</div>
) : loading ? (
<div className="px-4 py-4 text-sm text-[#5f5b52]">
{t("loading")}
</div>
) : (
<div className="px-4 py-4 text-sm text-[#5f5b52]">
{t("empty")}
</div>
)}
</section>
);
@@ -3302,6 +3334,8 @@ export default function RunsPage() {
const [callbackEvents, setCallbackEvents] = useState<CallbackReplyEvent[]>([]);
const [callbackEventsTotal, setCallbackEventsTotal] = useState(0);
const [callbackAuditSummary, setCallbackAuditSummary] = useState<CallbackReplyAuditSummary | null>(null);
const [callbackEventsLoading, setCallbackEventsLoading] = useState(true);
const [callbackCacheInfo, setCallbackCacheInfo] = useState<OperatorSummaryCacheInfo | null>(null);
const [callbackEventsError, setCallbackEventsError] = useState<string | null>(null);
const [aiRouteStatus, setAiRouteStatus] = useState<AiRouteStatusResponse | null>(null);
const [aiRouteStatusError, setAiRouteStatusError] = useState<string | null>(null);
@@ -3346,7 +3380,7 @@ export default function RunsPage() {
.catch(() => {});
}, []);
const fetchRuns = useCallback(async () => {
const fetchRuns = useCallback(async (options?: { refresh?: boolean }) => {
try {
setError(null);
const params = new URLSearchParams();
@@ -3423,20 +3457,38 @@ export default function RunsPage() {
if (INCIDENT_ID_FILTER_RE.test(normalizedIncidentFilter)) {
callbackParams.set("incident_id", normalizedIncidentFilter);
}
const callbackRes = await fetch(
`${API_BASE}/api/v1/platform/runs/callback-replies?${callbackParams.toString()}`
);
if (callbackRes.ok) {
const callbackData: CallbackRepliesResponse = await callbackRes.json();
setCallbackEvents(Array.isArray(callbackData.items) ? callbackData.items : []);
setCallbackEventsTotal(callbackData.total ?? 0);
setCallbackAuditSummary(callbackData.summary ?? null);
setCallbackEventsError(null);
} else {
if (options?.refresh) {
callbackParams.set("refresh", "true");
}
setCallbackEventsLoading(true);
try {
const callbackRes = await fetch(
`${API_BASE}/api/v1/platform/runs/callback-replies?${callbackParams.toString()}`
);
if (callbackRes.ok) {
const callbackData: CallbackRepliesResponse = await callbackRes.json();
setCallbackEvents(Array.isArray(callbackData.items) ? callbackData.items : []);
setCallbackEventsTotal(callbackData.total ?? 0);
setCallbackAuditSummary(callbackData.summary ?? null);
setCallbackCacheInfo(callbackData.cache ?? null);
setCallbackEventsError(null);
} else {
setCallbackEvents([]);
setCallbackEventsTotal(0);
setCallbackAuditSummary(null);
setCallbackCacheInfo(null);
setCallbackEventsError(`HTTP ${callbackRes.status}`);
}
} catch (callbackError) {
setCallbackEvents([]);
setCallbackEventsTotal(0);
setCallbackAuditSummary(null);
setCallbackEventsError(`HTTP ${callbackRes.status}`);
setCallbackCacheInfo(null);
setCallbackEventsError(
callbackError instanceof Error ? callbackError.message : "callback evidence failed"
);
} finally {
setCallbackEventsLoading(false);
}
try {
@@ -3461,6 +3513,7 @@ export default function RunsPage() {
setLastRefresh(new Date());
} catch (err) {
setError(err instanceof Error ? err.message : "載入失敗");
setCallbackEventsLoading(false);
} finally {
setLoading(false);
}
@@ -3555,7 +3608,7 @@ export default function RunsPage() {
<div className="flex items-center gap-2">
<span className="text-xs text-muted-foreground"> 30 </span>
<button
onClick={() => { setLoading(true); fetchRuns(); }}
onClick={() => { setLoading(true); fetchRuns({ refresh: true }); }}
disabled={loading}
className="flex items-center gap-2 px-3 py-2 text-sm text-muted-foreground hover:text-foreground hover:bg-accent rounded-lg transition disabled:opacity-50"
aria-label="立即重新整理"
@@ -3688,6 +3741,8 @@ export default function RunsPage() {
events={callbackEvents}
total={callbackEventsTotal}
summary={callbackAuditSummary}
cache={callbackCacheInfo}
loading={callbackEventsLoading}
error={callbackEventsError}
/>