fix(小宝预警): 保证建议不为空并显示更新中
This commit is contained in:
128
apps/web/lib/xiaobao-risk-suggestion.ts
Normal file
128
apps/web/lib/xiaobao-risk-suggestion.ts
Normal file
@@ -0,0 +1,128 @@
|
||||
import { buildRiskInsightSignature, findLatestRiskInsightForVersion, getReusableInsight } from './xiaobao-risk-ai';
|
||||
import type { XiaobaoRiskInsight, XiaobaoRiskInsightCacheItem } from './xiaobao-risk-cache';
|
||||
import type { RiskReason, XiaobaoVersionRisk } from './xiaobao-risk';
|
||||
import { sanitizeRiskInsight } from './xiaobao-warning-view';
|
||||
|
||||
interface AttachXiaobaoRiskSuggestionInput {
|
||||
insights: XiaobaoRiskInsightCacheItem[];
|
||||
pendingInsightKeys: string[];
|
||||
now?: Date;
|
||||
}
|
||||
|
||||
const RISK_LEVEL_LABEL: Record<XiaobaoVersionRisk['riskLevel'], string> = {
|
||||
on_track: '按期',
|
||||
attention: '需关注',
|
||||
at_risk: '有风险',
|
||||
likely_delayed: '大概率延期',
|
||||
blocked: '阻塞',
|
||||
};
|
||||
|
||||
export function buildXiaobaoRiskInsightPendingKey(risk: XiaobaoVersionRisk): string {
|
||||
return `${risk.versionId}:${buildRiskInsightSignature(risk)}`;
|
||||
}
|
||||
|
||||
export function attachXiaobaoRiskSuggestion(
|
||||
risk: XiaobaoVersionRisk,
|
||||
input: AttachXiaobaoRiskSuggestionInput,
|
||||
): XiaobaoVersionRisk {
|
||||
const pendingKey = buildXiaobaoRiskInsightPendingKey(risk);
|
||||
const isUpdating = input.pendingInsightKeys.includes(pendingKey);
|
||||
const reusable = getReusableInsight(input.insights, risk);
|
||||
if (reusable) {
|
||||
return {
|
||||
...risk,
|
||||
aiInsight: sanitizeRiskInsight(reusable.insight),
|
||||
aiInsightSource: 'current_ai',
|
||||
aiInsightUpdating: false,
|
||||
};
|
||||
}
|
||||
|
||||
const latest = findLatestRiskInsightForVersion(input.insights, risk.versionId);
|
||||
if (latest) {
|
||||
return {
|
||||
...risk,
|
||||
aiInsight: sanitizeRiskInsight(latest.insight),
|
||||
aiInsightSource: 'previous_ai',
|
||||
aiInsightUpdating: isUpdating,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
...risk,
|
||||
aiInsight: buildRuleBasedRiskInsight(risk, input.now),
|
||||
aiInsightSource: 'rule',
|
||||
aiInsightUpdating: isUpdating,
|
||||
};
|
||||
}
|
||||
|
||||
export function buildRuleBasedRiskInsight(risk: XiaobaoVersionRisk, now = new Date()): XiaobaoRiskInsight {
|
||||
const firstReason = risk.reasons[0];
|
||||
const why = risk.reasons.length > 0
|
||||
? risk.reasons.slice(0, 4).map((reason) => reason.detail || reason.title)
|
||||
: [risk.trend.summary || '当前版本存在规则层识别到的发布风险。'];
|
||||
const forecastDate = formatDate(risk.forecastReleaseDate);
|
||||
const expectedDate = formatDate(risk.expectedReleaseDate);
|
||||
const forecast = risk.delayDays > 0
|
||||
? `按当前剩余工作量和风险信号,预计较期望发版日期${expectedDate ? `(${expectedDate})` : ''}延期约 ${risk.delayDays} 天,可发窗口约为 ${forecastDate || '重新评估后确认'}。`
|
||||
: `按当前规则预测,版本仍有风险需要收敛;预计可发窗口为 ${forecastDate || expectedDate || '待补齐计划后确认'}。`;
|
||||
|
||||
return sanitizeRiskInsight({
|
||||
summary: `${risk.versionName} 当前风险分 ${risk.riskScore},状态为${RISK_LEVEL_LABEL[risk.riskLevel]}。${firstReason?.title ? `主要关注:${firstReason.title}。` : ''}`,
|
||||
why,
|
||||
forecast,
|
||||
recommendedReleaseWindow: buildReleaseWindow(risk),
|
||||
suggestedActions: buildRuleActions(risk),
|
||||
ownerHints: buildOwnerHints(risk),
|
||||
generatedAt: now.toISOString(),
|
||||
});
|
||||
}
|
||||
|
||||
function buildReleaseWindow(risk: XiaobaoVersionRisk): string | undefined {
|
||||
const forecastDate = formatDate(risk.forecastReleaseDate);
|
||||
if (!forecastDate) return undefined;
|
||||
if (risk.delayDays > 0) return `建议优先以 ${forecastDate} 作为新的候选发版窗口,并在关键风险关闭后再确认。`;
|
||||
return `建议继续以 ${forecastDate} 附近作为候选发版窗口,但需要先完成当前风险项收敛。`;
|
||||
}
|
||||
|
||||
function buildRuleActions(risk: XiaobaoVersionRisk): string[] {
|
||||
const actions = risk.reasons.flatMap(reasonToActions);
|
||||
const unique = Array.from(new Set(actions));
|
||||
if (unique.length > 0) return unique.slice(0, 4);
|
||||
return ['确认剩余任务、测试失败、Bug 和阻塞项的负责人及预计完成时间。'];
|
||||
}
|
||||
|
||||
function reasonToActions(reason: RiskReason): string[] {
|
||||
switch (reason.key) {
|
||||
case 'critical_bug':
|
||||
return ['优先收敛 P1 或致命 Bug,明确修复人、复测人和关闭时间。'];
|
||||
case 'failed_test':
|
||||
return ['安排测试负责人复测失败用例,并同步失败原因和修复进展。'];
|
||||
case 'blocked_work':
|
||||
return ['先解除阻塞项,无法当天解除的需要明确替代方案或延期影响。'];
|
||||
case 'forecast_delay':
|
||||
case 'remaining_work':
|
||||
return ['按剩余工作量重排今天到发版日前的完成顺序,先处理影响发版的主链路事项。'];
|
||||
case 'silent_risk':
|
||||
return ['要求未更新的负责人补充今日日报或活动证据,避免静默风险继续扩大。'];
|
||||
default:
|
||||
return reason.detail ? [`跟进:${reason.detail}`] : [];
|
||||
}
|
||||
}
|
||||
|
||||
function buildOwnerHints(risk: XiaobaoVersionRisk): string[] {
|
||||
const hints: string[] = [];
|
||||
if (risk.signals.unfinishedCount > 0) hints.push(`仍有 ${risk.signals.unfinishedCount} 个未完成事项,需要逐项确认负责人。`);
|
||||
if (risk.signals.openBugCount > 0) hints.push(`仍有 ${risk.signals.openBugCount} 个未关闭 Bug,需要确认修复和复测节奏。`);
|
||||
if (risk.signals.silentRiskCount > 0) hints.push(`存在 ${risk.signals.silentRiskCount} 个静默风险,需要负责人补充进展。`);
|
||||
return hints;
|
||||
}
|
||||
|
||||
function formatDate(value: string | null | undefined): string | undefined {
|
||||
if (!value) return undefined;
|
||||
const raw = value.trim();
|
||||
if (!raw) return undefined;
|
||||
if (/^\d{4}-\d{2}-\d{2}/.test(raw)) return raw.slice(0, 10);
|
||||
const time = new Date(raw).getTime();
|
||||
if (!Number.isFinite(time)) return undefined;
|
||||
return new Date(time).toISOString().slice(0, 10);
|
||||
}
|
||||
Reference in New Issue
Block a user