fix(awooop): cache heavy operator summaries
This commit is contained in:
@@ -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}
|
||||
/>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user