feat(ai): complete v3.2 risk watch agent

This commit is contained in:
2026-07-08 19:57:38 +08:00
parent deebc5404a
commit e949d0f5d3
10 changed files with 445 additions and 30 deletions

View File

@@ -17,7 +17,9 @@ import { loadV22XiaobaoWarnings, type V22XiaobaoWarningSummary } from '@/lib/v22
import { buildVersionDataScopeMap } from '@/lib/version-data-scope';
import {
buildXiaobaoRisksFromV22Summaries,
collectV22XiaobaoLatestInsights,
filterV22XiaobaoSummariesForVisibleVersions,
mergeV22XiaobaoLatestInsights,
shouldLoadXiaobaoAppDataFallback,
type V22XiaobaoSummaryLoadState,
} from '@/lib/xiaobao-v22-summary';
@@ -54,6 +56,11 @@ export function useXiaobaoWarningRisks({ loadRiskCache = false }: { loadRiskCach
const today = useMemo(() => new Date().toISOString().slice(0, 10), []);
const v22UserId = user?.name || user?.id || '';
const shouldLoadFallback = shouldLoadXiaobaoAppDataFallback(v22SummaryState);
const relationInsights = useMemo(() => collectV22XiaobaoLatestInsights(v22Summaries), [v22Summaries]);
const mergedInsights = useMemo(
() => mergeV22XiaobaoLatestInsights(relationInsights, insights),
[insights, relationInsights],
);
useEffect(() => { fetchOverview(); }, [fetchOverview]);
useEffect(() => {
@@ -178,7 +185,7 @@ export function useXiaobaoWarningRisks({ loadRiskCache = false }: { loadRiskCach
return {
risks,
snapshots,
insights,
insights: mergedInsights,
pendingInsightKeys,
insightRequestAttempts,
riskDataLoaded,

View File

@@ -5,6 +5,7 @@ import type { Priority } from './derive';
import type { Requirement, RequirementStatus, SourceType } from './requirement';
import type { TestCase, TestCaseStatus } from './test-case';
import type { VersionDataScope } from './version-data-scope';
import type { XiaobaoRiskInsightCacheItem } from './xiaobao-risk-cache';
import type {
PlanTask,
VersionPlan,
@@ -14,6 +15,11 @@ import type {
type V22DateValue = string | Date | null | undefined;
type V22XiaobaoRiskInsightCacheItem = Omit<XiaobaoRiskInsightCacheItem, 'generatedAt' | 'insight'> & {
generatedAt?: V22DateValue;
insight: XiaobaoRiskInsightCacheItem['insight'] & { generatedAt?: V22DateValue };
};
interface V22VersionRow {
id: string;
productId: string;
@@ -188,6 +194,7 @@ export interface V22XiaobaoWarningSummary {
forecastReleaseDate?: string;
riskSignature: string;
summary: unknown;
latestInsight?: XiaobaoRiskInsightCacheItem;
dirty?: boolean;
recomputedAt?: string;
updatedAt?: string;
@@ -231,10 +238,11 @@ export async function loadV22WorkspaceData(userId: string): Promise<V22Workspace
export async function loadV22XiaobaoWarnings(
query: V22XiaobaoWarningQuery,
): Promise<V22XiaobaoWarningSummary[]> {
const response = await api.get<Array<Omit<V22XiaobaoWarningSummary, 'forecastReleaseDate' | 'recomputedAt' | 'updatedAt'> & {
const response = await api.get<Array<Omit<V22XiaobaoWarningSummary, 'forecastReleaseDate' | 'recomputedAt' | 'updatedAt' | 'latestInsight'> & {
forecastReleaseDate?: V22DateValue;
recomputedAt?: V22DateValue;
updatedAt?: V22DateValue;
latestInsight?: V22XiaobaoRiskInsightCacheItem;
}>>(`/v2.2/xiaobao-warning?${queryString({
userId: query.userId,
manager: query.manager ? 'true' : undefined,
@@ -244,6 +252,7 @@ export async function loadV22XiaobaoWarnings(
forecastReleaseDate: optionalIso(row.forecastReleaseDate),
recomputedAt: optionalIso(row.recomputedAt),
updatedAt: optionalIso(row.updatedAt),
latestInsight: mapLatestXiaobaoInsight(row.latestInsight),
}));
}
@@ -421,6 +430,18 @@ function optionalIso(value: V22DateValue): string | undefined {
return trimmed;
}
function mapLatestXiaobaoInsight(value?: V22XiaobaoRiskInsightCacheItem): XiaobaoRiskInsightCacheItem | undefined {
if (!value) return undefined;
return {
...value,
generatedAt: optionalIso(value.generatedAt) ?? new Date(0).toISOString(),
insight: {
...value.insight,
generatedAt: optionalIso(value.insight.generatedAt) ?? value.insight.generatedAt ?? new Date(0).toISOString(),
},
};
}
function splitCsv(value?: string | null): string[] {
if (!value) return [];
return value.split(',').map((item) => item.trim()).filter(Boolean);

View File

@@ -5,7 +5,9 @@ import type { VersionWithContext } from './derive';
import type { V22XiaobaoWarningSummary } from './v22-api';
import {
buildXiaobaoRisksFromV22Summaries,
collectV22XiaobaoLatestInsights,
filterV22XiaobaoSummariesForVisibleVersions,
mergeV22XiaobaoLatestInsights,
shouldLoadXiaobaoAppDataFallback,
} from './xiaobao-v22-summary';
@@ -165,6 +167,69 @@ test('filterV22XiaobaoSummariesForVisibleVersions keeps only visible version sum
assert.deepEqual(result.map((item) => item.versionId), ['visible-1', 'visible-2']);
});
test('collectV22XiaobaoLatestInsights exposes relation-backed AI insights', () => {
const result = collectV22XiaobaoLatestInsights([
summary({
versionId: 'version-1',
latestInsight: {
versionId: 'version-1',
riskSignature: 'signature-1',
generatedAt: '2026-07-08T10:00:00.000Z',
providerInfo: { model: 'claude-test' },
insight: {
summary: '高风险版本需要处理阻塞。',
why: ['存在阻塞项'],
forecast: '预计延期 1 天。',
suggestedActions: ['先解除阻塞'],
ownerHints: ['确认阻塞负责人'],
generatedAt: '2026-07-08T10:00:00.000Z',
},
},
}),
summary({ versionId: 'version-2' }),
]);
assert.equal(result.length, 1);
assert.equal(result[0].versionId, 'version-1');
assert.equal(result[0].riskSignature, 'signature-1');
assert.equal(result[0].insight.summary, '高风险版本需要处理阻塞。');
assert.equal(result[0].providerInfo?.model, 'claude-test');
});
test('mergeV22XiaobaoLatestInsights prefers relation-backed insights over legacy cache duplicates', () => {
const relationInsight = {
versionId: 'version-1',
riskSignature: 'signature-1',
generatedAt: '2026-07-08T10:00:00.000Z',
insight: {
summary: '关系表新解读。',
why: ['new'],
forecast: 'new forecast',
suggestedActions: ['new action'],
ownerHints: ['new owner'],
generatedAt: '2026-07-08T10:00:00.000Z',
},
};
const legacyInsight = {
versionId: 'version-1',
riskSignature: 'signature-1',
generatedAt: '2026-07-08T09:00:00.000Z',
insight: {
summary: '旧缓存解读。',
why: ['old'],
forecast: 'old forecast',
suggestedActions: ['old action'],
ownerHints: ['old owner'],
generatedAt: '2026-07-08T09:00:00.000Z',
},
};
const result = mergeV22XiaobaoLatestInsights([relationInsight], [legacyInsight]);
assert.equal(result.length, 1);
assert.equal(result[0].insight.summary, '关系表新解读。');
});
function summary(patch: Partial<V22XiaobaoWarningSummary> = {}): V22XiaobaoWarningSummary {
return {
versionId: 'version-1',

View File

@@ -1,5 +1,6 @@
import type { VersionWithContext } from './derive';
import type { V22XiaobaoWarningSummary } from './v22-api';
import type { XiaobaoRiskInsightCacheItem } from './xiaobao-risk-cache';
import type {
RiskReason,
SilentRisk,
@@ -37,6 +38,25 @@ export function filterV22XiaobaoSummariesForVisibleVersions(
return summaries.filter((summary) => visibleVersionIds.has(summary.versionId));
}
export function collectV22XiaobaoLatestInsights(
summaries: V22XiaobaoWarningSummary[],
): XiaobaoRiskInsightCacheItem[] {
return summaries
.map((summary) => summary.latestInsight)
.filter((item): item is XiaobaoRiskInsightCacheItem => Boolean(item));
}
export function mergeV22XiaobaoLatestInsights(
relationInsights: XiaobaoRiskInsightCacheItem[],
legacyInsights: XiaobaoRiskInsightCacheItem[],
): XiaobaoRiskInsightCacheItem[] {
const relationKeys = new Set(relationInsights.map((item) => insightKey(item)));
return [
...relationInsights,
...legacyInsights.filter((item) => !relationKeys.has(insightKey(item))),
];
}
export function buildXiaobaoRisksFromV22Summaries(
input: BuildXiaobaoRisksFromV22SummariesInput,
): XiaobaoVersionRisk[] {
@@ -118,6 +138,10 @@ function getRiskPayload(summary: unknown): Record<string, unknown> {
return Object.keys(nested).length > 0 ? nested : root;
}
function insightKey(item: XiaobaoRiskInsightCacheItem): string {
return `${item.versionId}::${item.riskSignature}`;
}
function buildCurrentSnapshot(input: {
payload: Record<string, unknown>;
row: V22XiaobaoWarningSummary;