Files
ftb-project-management/apps/web/lib/xiaobao-warning-view.ts
2026-06-30 10:23:01 +08:00

81 lines
2.9 KiB
TypeScript

import type { VersionWithContext } from './derive';
import type { XiaobaoRiskInsight } from './xiaobao-risk-cache';
import type { XiaobaoVersionRisk } from './xiaobao-risk';
import { WORK_HOURS } from './work-hours';
const UNFINISHED_VERSION_STATUSES = new Set(['planned', 'developing', 'paused']);
const HIGH_RISK_LEVELS = new Set<XiaobaoVersionRisk['riskLevel']>(['at_risk', 'likely_delayed', 'blocked']);
const PAGE_REFRESH_ADVICE_PATTERNS = [
/(刷新|重新加载|重载).*(页面|浏览器|小宝|预警)/i,
/(页面|浏览器|小宝|预警).*(刷新|重新加载|重载)/i,
/(手动|主动).*(触发|刷新).*(更新|预警|分析)/i,
/(manual|manually).*(refresh|reload|trigger)/i,
/(refresh|reload).*(page|browser|xiaobao|warning)/i,
];
export interface XiaobaoWarningVersionFilter {
canManage: boolean;
userName?: string;
}
export type XiaobaoWarningRiskFilter = 'all' | 'attention' | 'high';
export interface XiaobaoWarningRiskListFilter {
riskFilter?: XiaobaoWarningRiskFilter;
productId?: string;
projectId?: string;
}
export function filterXiaobaoWarningVersions(
versions: VersionWithContext[],
filter: XiaobaoWarningVersionFilter,
): VersionWithContext[] {
return versions.filter((version) => {
if (!UNFINISHED_VERSION_STATUSES.has(version.status)) return false;
if (filter.canManage) return true;
if (!filter.userName) return false;
return (version.members ?? []).some((member) => member.name === filter.userName);
});
}
export function filterXiaobaoRiskWarnings(
risks: XiaobaoVersionRisk[],
filter: XiaobaoWarningRiskListFilter = {},
): XiaobaoVersionRisk[] {
return risks.filter((risk) => {
if (risk.riskLevel === 'on_track') return false;
if (filter.productId && risk.productId !== filter.productId) return false;
if (filter.projectId && risk.projectId !== filter.projectId) return false;
if (filter.riskFilter === 'attention') return true;
if (filter.riskFilter === 'high') return HIGH_RISK_LEVELS.has(risk.riskLevel);
return true;
});
}
export function getXiaobaoWarningRiskCount(risks: XiaobaoVersionRisk[]): number {
return filterXiaobaoRiskWarnings(risks).length;
}
export function formatRemainingWork(hours: number): string {
const safeHours = Number.isFinite(hours) && hours > 0 ? hours : 0;
const days = safeHours / WORK_HOURS.hoursPerDay;
return `${formatNumber(safeHours)}h / ${formatNumber(days)}`;
}
export function sanitizeRiskInsight(insight: XiaobaoRiskInsight): XiaobaoRiskInsight {
return {
...insight,
suggestedActions: insight.suggestedActions.filter((action) => !isPageRefreshAdvice(action)),
ownerHints: insight.ownerHints.filter((hint) => !isPageRefreshAdvice(hint)),
};
}
function formatNumber(value: number): string {
if (Number.isInteger(value)) return String(value);
return value.toFixed(1).replace(/\.0$/, '');
}
function isPageRefreshAdvice(text: string): boolean {
return PAGE_REFRESH_ADVICE_PATTERNS.some((pattern) => pattern.test(text));
}