From db72a420f54fb487c1dc9a912a70b8c14ad44294 Mon Sep 17 00:00:00 2001 From: Script Generator Date: Mon, 29 Jun 2026 18:37:39 +0800 Subject: [PATCH] =?UTF-8?q?feat(=E5=B0=8F=E5=AE=9D=E9=A2=84=E8=AD=A6):=20?= =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E8=B6=8B=E5=8A=BF=E4=B8=8E=E8=A7=A3=E8=AF=BB?= =?UTF-8?q?=E7=BC=93=E5=AD=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/web/lib/xiaobao-risk-cache.test.ts | 70 +++++++++++++++++++++++++ apps/web/lib/xiaobao-risk-cache.ts | 47 +++++++++++++++++ apps/web/lib/xiaobao-risk.ts | 2 + apps/web/stores/useXiaobaoRiskStore.ts | 55 +++++++++++++++++++ 4 files changed, 174 insertions(+) create mode 100644 apps/web/lib/xiaobao-risk-cache.test.ts create mode 100644 apps/web/lib/xiaobao-risk-cache.ts create mode 100644 apps/web/stores/useXiaobaoRiskStore.ts diff --git a/apps/web/lib/xiaobao-risk-cache.test.ts b/apps/web/lib/xiaobao-risk-cache.test.ts new file mode 100644 index 0000000..4fa5c22 --- /dev/null +++ b/apps/web/lib/xiaobao-risk-cache.test.ts @@ -0,0 +1,70 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; +import type { XiaobaoRiskSnapshot } from './xiaobao-risk-trend'; +import { + findCachedInsight, + upsertDailySnapshot, + upsertInsight, + type XiaobaoRiskInsightCacheItem, +} from './xiaobao-risk-cache'; + +const insightA: XiaobaoRiskInsightCacheItem = { + versionId: 'v-1', + riskSignature: 'sig-a', + insight: { + summary: '版本风险上升', + why: ['剩余工作较多'], + forecast: '可能延期 2 天', + suggestedActions: ['压缩低优先级范围'], + ownerHints: ['请项目负责人确认排期'], + generatedAt: '2026-06-29T08:00:00.000Z', + }, + generatedAt: '2026-06-29T08:00:00.000Z', +}; + +test('findCachedInsight returns matching signature only', () => { + const rows: XiaobaoRiskInsightCacheItem[] = [ + insightA, + { ...insightA, versionId: 'v-2', riskSignature: 'sig-a' }, + { ...insightA, versionId: 'v-1', riskSignature: 'sig-b' }, + ]; + + assert.equal(findCachedInsight(rows, 'v-1', 'sig-a'), insightA); + assert.equal(findCachedInsight(rows, 'v-1', 'sig-b')?.versionId, 'v-1'); + assert.equal(findCachedInsight(rows, 'v-2', 'sig-b'), undefined); +}); + +test('upsertDailySnapshot keeps one snapshot per version and date', () => { + const existing: XiaobaoRiskSnapshot = { + versionId: 'v-1', + date: '2026-06-29', + riskScore: 40, + riskLevel: 'attention', + openBugCount: 1, + failedTestCount: 0, + blockedCount: 0, + silentRiskCount: 0, + confidence: 80, + createdAt: '2026-06-29T08:00:00.000Z', + }; + const replacement: XiaobaoRiskSnapshot = { ...existing, riskScore: 72, createdAt: '2026-06-29T09:00:00.000Z' }; + const otherDay: XiaobaoRiskSnapshot = { ...existing, date: '2026-06-28', createdAt: '2026-06-28T09:00:00.000Z' }; + + const result = upsertDailySnapshot([existing, otherDay], replacement); + + assert.deepEqual(result, [replacement, otherDay]); +}); + +test('upsertInsight replaces existing version signature pair', () => { + const replacement: XiaobaoRiskInsightCacheItem = { + ...insightA, + insight: { ...insightA.insight, summary: '已重新生成' }, + generatedAt: '2026-06-29T09:00:00.000Z', + }; + const otherSignature: XiaobaoRiskInsightCacheItem = { ...insightA, riskSignature: 'sig-b' }; + const otherVersion: XiaobaoRiskInsightCacheItem = { ...insightA, versionId: 'v-2' }; + + const result = upsertInsight([insightA, otherSignature, otherVersion], replacement); + + assert.deepEqual(result, [replacement, otherSignature, otherVersion]); +}); diff --git a/apps/web/lib/xiaobao-risk-cache.ts b/apps/web/lib/xiaobao-risk-cache.ts new file mode 100644 index 0000000..691ac44 --- /dev/null +++ b/apps/web/lib/xiaobao-risk-cache.ts @@ -0,0 +1,47 @@ +import type { XiaobaoRiskSnapshot } from './xiaobao-risk-trend'; + +export interface XiaobaoRiskInsight { + summary: string; + why: string[]; + forecast: string; + recommendedReleaseWindow?: string; + suggestedActions: string[]; + ownerHints: string[]; + generatedAt: string; +} + +export interface XiaobaoRiskInsightCacheItem { + versionId: string; + riskSignature: string; + insight: XiaobaoRiskInsight; + generatedAt: string; + providerInfo?: { providerId?: string; model?: string }; +} + +export function findCachedInsight( + rows: XiaobaoRiskInsightCacheItem[], + versionId: string, + riskSignature: string, +): XiaobaoRiskInsightCacheItem | undefined { + return rows.find((row) => row.versionId === versionId && row.riskSignature === riskSignature); +} + +export function upsertInsight( + rows: XiaobaoRiskInsightCacheItem[], + item: XiaobaoRiskInsightCacheItem, +): XiaobaoRiskInsightCacheItem[] { + return [ + item, + ...rows.filter((row) => row.versionId !== item.versionId || row.riskSignature !== item.riskSignature), + ]; +} + +export function upsertDailySnapshot( + rows: XiaobaoRiskSnapshot[], + item: XiaobaoRiskSnapshot, +): XiaobaoRiskSnapshot[] { + return [ + item, + ...rows.filter((row) => row.versionId !== item.versionId || row.date !== item.date), + ]; +} diff --git a/apps/web/lib/xiaobao-risk.ts b/apps/web/lib/xiaobao-risk.ts index 1716610..8197766 100644 --- a/apps/web/lib/xiaobao-risk.ts +++ b/apps/web/lib/xiaobao-risk.ts @@ -3,6 +3,7 @@ import type { DevTask } from './dev-task'; import { STATUS_PROGRESS, getEstimateHours } from './dev-task'; import type { TestCase } from './test-case'; import { getTestCaseEstimateHours } from './test-case'; +import type { XiaobaoRiskInsight } from './xiaobao-risk-cache'; import type { VersionDailyEvidence } from './xiaobao-risk-evidence'; import { summarizeRiskTrendWithCurrent, type XiaobaoRiskSnapshot } from './xiaobao-risk-trend'; import { WORK_HOURS, addWorkHours } from './work-hours'; @@ -61,6 +62,7 @@ export interface XiaobaoVersionRisk { dailyEvidence?: VersionDailyEvidence; signals: XiaobaoRiskSignals; currentSnapshot: XiaobaoRiskSnapshot; + aiInsight?: XiaobaoRiskInsight; trend: { direction: 'up' | 'down' | 'flat' | 'unknown'; delta: number; diff --git a/apps/web/stores/useXiaobaoRiskStore.ts b/apps/web/stores/useXiaobaoRiskStore.ts new file mode 100644 index 0000000..3878803 --- /dev/null +++ b/apps/web/stores/useXiaobaoRiskStore.ts @@ -0,0 +1,55 @@ +'use client'; +import { create } from 'zustand'; +import { loadServerData, saveServerData } from '@/lib/server-data'; +import { + upsertDailySnapshot, + upsertInsight, + type XiaobaoRiskInsightCacheItem, +} from '@/lib/xiaobao-risk-cache'; +import type { XiaobaoRiskSnapshot } from '@/lib/xiaobao-risk-trend'; + +interface XiaobaoRiskState { + snapshots: XiaobaoRiskSnapshot[]; + insights: XiaobaoRiskInsightCacheItem[]; + fetchRiskData: () => Promise; + saveSnapshot: (item: XiaobaoRiskSnapshot) => Promise; + saveInsight: (item: XiaobaoRiskInsightCacheItem) => Promise; +} + +async function loadSnapshots(): Promise { + try { + const rows = await loadServerData('xiaobao-risk-snapshots'); + return Array.isArray(rows) ? rows : []; + } catch {} + return []; +} + +async function loadInsights(): Promise { + try { + const rows = await loadServerData('xiaobao-risk-insights'); + return Array.isArray(rows) ? rows : []; + } catch {} + return []; +} + +export const useXiaobaoRiskStore = create((set, get) => ({ + snapshots: [], + insights: [], + + fetchRiskData: async () => { + const [snapshots, insights] = await Promise.all([loadSnapshots(), loadInsights()]); + set({ snapshots, insights }); + }, + + saveSnapshot: async (item) => { + const snapshots = upsertDailySnapshot(get().snapshots, item); + set({ snapshots }); + await saveServerData('xiaobao-risk-snapshots', snapshots); + }, + + saveInsight: async (item) => { + const insights = upsertInsight(get().insights, item); + set({ insights }); + await saveServerData('xiaobao-risk-insights', insights); + }, +}));