fix(小宝预警): 等待缓存加载后再触发AI建议

This commit is contained in:
Script Generator
2026-06-30 09:56:45 +08:00
parent 4cecf2d77a
commit eb9996a987
4 changed files with 57 additions and 6 deletions

View File

@@ -18,7 +18,7 @@ import { useWorkActivityStore } from '@/stores/useWorkActivityStore';
import { useXiaobaoRiskStore } from '@/stores/useXiaobaoRiskStore'; import { useXiaobaoRiskStore } from '@/stores/useXiaobaoRiskStore';
import { flattenVersions } from '@/lib/derive'; import { flattenVersions } from '@/lib/derive';
import { calcXiaobaoVersionRisk, type XiaobaoVersionRisk } from '@/lib/xiaobao-risk'; import { calcXiaobaoVersionRisk, type XiaobaoVersionRisk } from '@/lib/xiaobao-risk';
import { buildRiskInsightSignature, findLatestRiskInsightForVersion, findPreviousRiskSnapshot, getReusableInsight, requestRiskInsight, shouldRequestRiskInsightWithCooldown } from '@/lib/xiaobao-risk-ai'; import { buildRiskInsightSignature, findPreviousRiskSnapshot, getReusableInsight, requestRiskInsight, shouldRequestRiskInsightWithCacheGate } from '@/lib/xiaobao-risk-ai';
import { buildVersionDailyEvidence, buildXiaobaoWorkItems } from '@/lib/xiaobao-risk-evidence'; import { buildVersionDailyEvidence, buildXiaobaoWorkItems } from '@/lib/xiaobao-risk-evidence';
import { buildRiskSignature, findLatestDailySnapshot, shouldSaveRiskSnapshot } from '@/lib/xiaobao-risk-trend'; import { buildRiskSignature, findLatestDailySnapshot, shouldSaveRiskSnapshot } from '@/lib/xiaobao-risk-trend';
import { filterXiaobaoWarningVersions, sanitizeRiskInsight } from '@/lib/xiaobao-warning-view'; import { filterXiaobaoWarningVersions, sanitizeRiskInsight } from '@/lib/xiaobao-warning-view';
@@ -45,7 +45,7 @@ function XiaobaoWarningContent() {
const { bugs, fetchBugs } = useBugStore(); const { bugs, fetchBugs } = useBugStore();
const { activities, fetchActivities } = useWorkActivityStore(); const { activities, fetchActivities } = useWorkActivityStore();
const { worklogs, fetchWorklogs } = useTaskWorklogStore(); const { worklogs, fetchWorklogs } = useTaskWorklogStore();
const { snapshots, insights, fetchRiskData, saveSnapshot, saveInsight } = useXiaobaoRiskStore(); const { snapshots, insights, riskDataLoaded, fetchRiskData, saveSnapshot, saveInsight } = useXiaobaoRiskStore();
const [selectedRiskId, setSelectedRiskId] = useState<string | null>(null); const [selectedRiskId, setSelectedRiskId] = useState<string | null>(null);
const [filter, setFilter] = useState<RiskFilter>('all'); const [filter, setFilter] = useState<RiskFilter>('all');
const [calculationNow] = useState(() => new Date()); const [calculationNow] = useState(() => new Date());
@@ -142,10 +142,8 @@ function XiaobaoWarningContent() {
useEffect(() => { useEffect(() => {
risks.forEach((risk) => { risks.forEach((risk) => {
if (getReusableInsight(insights, risk)) return;
const previous = findPreviousRiskSnapshot(snapshots, risk.versionId, today); const previous = findPreviousRiskSnapshot(snapshots, risk.versionId, today);
const latestInsight = findLatestRiskInsightForVersion(insights, risk.versionId); if (!shouldRequestRiskInsightWithCacheGate(riskDataLoaded, insights, risk, previous)) return;
if (!shouldRequestRiskInsightWithCooldown(risk, previous, latestInsight)) return;
const signature = buildRiskInsightSignature(risk); const signature = buildRiskInsightSignature(risk);
const key = `${risk.versionId}:${signature}`; const key = `${risk.versionId}:${signature}`;
if (requestedInsightKeysRef.current.has(key)) return; if (requestedInsightKeysRef.current.has(key)) return;
@@ -161,7 +159,7 @@ function XiaobaoWarningContent() {
}).catch(() => {}); }).catch(() => {});
}).catch(() => {}); }).catch(() => {});
}); });
}, [insights, risks, saveInsight, snapshots, today]); }, [insights, riskDataLoaded, risks, saveInsight, snapshots, today]);
const risksWithInsight = useMemo(() => risks.map((risk) => { const risksWithInsight = useMemo(() => risks.map((risk) => {
const cached = getReusableInsight(insights, risk); const cached = getReusableInsight(insights, risk);

View File

@@ -8,6 +8,7 @@ import {
findLatestRiskInsightForVersion, findLatestRiskInsightForVersion,
findPreviousRiskSnapshot, findPreviousRiskSnapshot,
shouldRequestRiskInsight, shouldRequestRiskInsight,
shouldRequestRiskInsightWithCacheGate,
shouldRequestRiskInsightWithCooldown, shouldRequestRiskInsightWithCooldown,
} from './xiaobao-risk-ai'; } from './xiaobao-risk-ai';
import type { XiaobaoRiskInsightCacheItem } from './xiaobao-risk-cache'; import type { XiaobaoRiskInsightCacheItem } from './xiaobao-risk-cache';
@@ -243,6 +244,41 @@ test('shouldRequestRiskInsightWithCooldown bypasses cooldown when risk level esc
assert.equal(shouldRequestRiskInsightWithCooldown(current, snapshot({ riskScore: 50 }), previousInsight, new Date('2026-06-29T11:00:00.000Z')), true); assert.equal(shouldRequestRiskInsightWithCooldown(current, snapshot({ riskScore: 50 }), previousInsight, new Date('2026-06-29T11:00:00.000Z')), true);
}); });
test('shouldRequestRiskInsightWithCacheGate waits for cache loading before AI requests', () => {
const current = risk({ riskLevel: 'at_risk', riskScore: 82 });
assert.equal(
shouldRequestRiskInsightWithCacheGate(false, [], current, snapshot({ riskScore: 45 }), new Date('2026-06-29T11:00:00.000Z')),
false,
);
});
test('shouldRequestRiskInsightWithCacheGate reuses unchanged cached insight after cache loading', () => {
const current = risk({ riskLevel: 'at_risk', riskScore: 82 });
const cached = insight({
riskSignature: buildRiskInsightSignature(current),
generatedAt: '2026-06-29T02:00:00.000Z',
});
assert.equal(
shouldRequestRiskInsightWithCacheGate(true, [cached], current, snapshot({ riskScore: 45 }), new Date('2026-06-29T11:00:00.000Z')),
false,
);
});
test('shouldRequestRiskInsightWithCacheGate allows changed risk facts after cache loading and cooldown', () => {
const current = risk({ riskLevel: 'at_risk', riskScore: 82 });
const previousInsight = insight({
riskSignature: buildRiskInsightSignature(risk({ riskLevel: 'at_risk', riskScore: 62 })),
generatedAt: '2026-06-29T02:00:00.000Z',
});
assert.equal(
shouldRequestRiskInsightWithCacheGate(true, [previousInsight], current, snapshot({ riskScore: 45 }), new Date('2026-06-29T11:00:00.000Z')),
true,
);
});
test('buildRiskInsightSignature uses all open bugs, not only critical bugs', () => { test('buildRiskInsightSignature uses all open bugs, not only critical bugs', () => {
const base = buildRiskInsightSignature(risk({ const base = buildRiskInsightSignature(risk({
signals: { ...risk().signals, openBugCount: 1, criticalBugCount: 0 }, signals: { ...risk().signals, openBugCount: 1, criticalBugCount: 0 },

View File

@@ -90,6 +90,19 @@ export function shouldRequestRiskInsightWithCooldown(
return nowTime - generatedAt >= RISK_INSIGHT_COOLDOWN_MS; return nowTime - generatedAt >= RISK_INSIGHT_COOLDOWN_MS;
} }
export function shouldRequestRiskInsightWithCacheGate(
riskCacheLoaded: boolean,
cache: XiaobaoRiskInsightCacheItem[],
current: XiaobaoVersionRisk,
previous?: RiskInsightPrevious,
now: Date = new Date(),
): boolean {
if (!riskCacheLoaded) return false;
if (getReusableInsight(cache, current)) return false;
const latestInsight = findLatestRiskInsightForVersion(cache, current.versionId);
return shouldRequestRiskInsightWithCooldown(current, previous, latestInsight, now);
}
export function buildRiskInsightSignature(risk: XiaobaoVersionRisk): string { export function buildRiskInsightSignature(risk: XiaobaoVersionRisk): string {
return JSON.stringify({ return JSON.stringify({
versionId: risk.versionId, versionId: risk.versionId,

View File

@@ -13,6 +13,7 @@ import type { XiaobaoRiskSnapshot } from '@/lib/xiaobao-risk-trend';
interface XiaobaoRiskState { interface XiaobaoRiskState {
snapshots: XiaobaoRiskSnapshot[]; snapshots: XiaobaoRiskSnapshot[];
insights: XiaobaoRiskInsightCacheItem[]; insights: XiaobaoRiskInsightCacheItem[];
riskDataLoaded: boolean;
error?: string; error?: string;
fetchRiskData: () => Promise<void>; fetchRiskData: () => Promise<void>;
saveSnapshot: (item: XiaobaoRiskSnapshot) => Promise<void>; saveSnapshot: (item: XiaobaoRiskSnapshot) => Promise<void>;
@@ -41,13 +42,16 @@ let insightSaveQueue: Promise<void> = Promise.resolve();
export const useXiaobaoRiskStore = create<XiaobaoRiskState>((set, get) => ({ export const useXiaobaoRiskStore = create<XiaobaoRiskState>((set, get) => ({
snapshots: [], snapshots: [],
insights: [], insights: [],
riskDataLoaded: false,
error: undefined, error: undefined,
fetchRiskData: async () => { fetchRiskData: async () => {
set({ riskDataLoaded: false });
const [snapshots, insights] = await Promise.all([loadSnapshots(), loadInsights()]); const [snapshots, insights] = await Promise.all([loadSnapshots(), loadInsights()]);
set({ set({
...(snapshots ? { snapshots } : {}), ...(snapshots ? { snapshots } : {}),
...(insights ? { insights } : {}), ...(insights ? { insights } : {}),
riskDataLoaded: snapshots !== null && insights !== null,
error: snapshots === null || insights === null ? '小宝预警缓存加载失败' : undefined, error: snapshots === null || insights === null ? '小宝预警缓存加载失败' : undefined,
}); });
}, },