fix(小宝预警): 过滤无效页面刷新建议
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
import { AiService } from './ai.service';
|
import { AiService } from './ai.service';
|
||||||
import type { AiGatewayService } from './ai-gateway.service';
|
import type { AiGatewayService } from './ai-gateway.service';
|
||||||
import { DECOMPOSE_SYSTEM_PROMPT, DECOMPOSE_TOOL_INPUT_SCHEMA } from './prompts/decompose';
|
import { DECOMPOSE_SYSTEM_PROMPT, DECOMPOSE_TOOL_INPUT_SCHEMA } from './prompts/decompose';
|
||||||
import { RISK_INTERPRET_TOOL_INPUT_SCHEMA } from './prompts/risk-interpret';
|
import { RISK_INTERPRET_SYSTEM_PROMPT, RISK_INTERPRET_TOOL_INPUT_SCHEMA } from './prompts/risk-interpret';
|
||||||
import { plainToInstance } from 'class-transformer';
|
import { plainToInstance } from 'class-transformer';
|
||||||
import { validateSync } from 'class-validator';
|
import { validateSync } from 'class-validator';
|
||||||
import { RiskInterpretDto } from './dto/risk-interpret.dto';
|
import { RiskInterpretDto } from './dto/risk-interpret.dto';
|
||||||
@@ -104,6 +104,38 @@ describe('AiService', () => {
|
|||||||
expect(callTool.mock.calls[0][0].userPrompt).toContain('风险上升');
|
expect(callTool.mock.calls[0][0].userPrompt).toContain('风险上升');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('removes invalid page refresh suggestions from risk interpretation results', async () => {
|
||||||
|
const callTool = jest.fn().mockResolvedValue({
|
||||||
|
toolName: 'submit_risk_interpretation',
|
||||||
|
toolInput: {
|
||||||
|
summary: '预计延期',
|
||||||
|
why: ['P1 Bug 增加'],
|
||||||
|
forecast: '预计 2026-07-04 可发',
|
||||||
|
suggestedActions: [
|
||||||
|
'手动刷新小宝预警页面,等待风险自动更新',
|
||||||
|
'优先修复 P1 Bug 并安排测试复测',
|
||||||
|
],
|
||||||
|
ownerHints: ['测试负责人同步失败用例'],
|
||||||
|
generatedAt: '2026-06-29T01:00:00.000Z',
|
||||||
|
},
|
||||||
|
rawModel: 'test-model',
|
||||||
|
inputTokens: 10,
|
||||||
|
outputTokens: 20,
|
||||||
|
});
|
||||||
|
const gateway = {
|
||||||
|
getActiveProvider: jest.fn().mockResolvedValue({ callTool }),
|
||||||
|
getActiveModel: jest.fn().mockResolvedValue('test-model'),
|
||||||
|
} as unknown as AiGatewayService;
|
||||||
|
const service = new AiService(gateway);
|
||||||
|
|
||||||
|
const result = await service.interpretRisk(createRiskRequest());
|
||||||
|
|
||||||
|
expect(result.ok).toBe(true);
|
||||||
|
if (result.ok) {
|
||||||
|
expect(result.result.suggestedActions).toEqual(['优先修复 P1 Bug 并安排测试复测']);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
it('returns PARSE_ERROR when risk provider returns malformed payload', async () => {
|
it('returns PARSE_ERROR when risk provider returns malformed payload', async () => {
|
||||||
const gateway = {
|
const gateway = {
|
||||||
getActiveProvider: jest.fn().mockResolvedValue({
|
getActiveProvider: jest.fn().mockResolvedValue({
|
||||||
@@ -211,6 +243,11 @@ describe('AiService', () => {
|
|||||||
expect((RISK_INTERPRET_TOOL_INPUT_SCHEMA as any).additionalProperties).toBe(false);
|
expect((RISK_INTERPRET_TOOL_INPUT_SCHEMA as any).additionalProperties).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('tells the risk model not to suggest page refresh or manual update triggers', () => {
|
||||||
|
expect(RISK_INTERPRET_SYSTEM_PROMPT).toContain('不要建议刷新页面');
|
||||||
|
expect(RISK_INTERPRET_SYSTEM_PROMPT).toContain('系统会自动更新');
|
||||||
|
});
|
||||||
|
|
||||||
it('returns PARSE_ERROR when provider reports invalid tool arguments', async () => {
|
it('returns PARSE_ERROR when provider reports invalid tool arguments', async () => {
|
||||||
const gateway = {
|
const gateway = {
|
||||||
getActiveProvider: jest.fn().mockResolvedValue({
|
getActiveProvider: jest.fn().mockResolvedValue({
|
||||||
|
|||||||
@@ -29,6 +29,13 @@ const DECOMPOSE_CONTEXT_CHAR_STEPS = [1800, 1200, 800] as const;
|
|||||||
const PROTOTYPE_FETCH_TIMEOUT_MS = 15000;
|
const PROTOTYPE_FETCH_TIMEOUT_MS = 15000;
|
||||||
const PROTOTYPE_FETCH_ATTEMPTS = 3;
|
const PROTOTYPE_FETCH_ATTEMPTS = 3;
|
||||||
const PROTOTYPE_FETCH_RETRY_DELAY_MS = 500;
|
const PROTOTYPE_FETCH_RETRY_DELAY_MS = 500;
|
||||||
|
const PAGE_REFRESH_ADVICE_PATTERNS = [
|
||||||
|
/(刷新|重新加载|重载).*(页面|浏览器|小宝|预警)/i,
|
||||||
|
/(页面|浏览器|小宝|预警).*(刷新|重新加载|重载)/i,
|
||||||
|
/(手动|主动).*(触发|刷新).*(更新|预警|分析)/i,
|
||||||
|
/(manual|manually).*(refresh|reload|trigger)/i,
|
||||||
|
/(refresh|reload).*(page|browser|xiaobao|warning)/i,
|
||||||
|
];
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class AiService {
|
export class AiService {
|
||||||
@@ -222,8 +229,8 @@ export class AiService {
|
|||||||
summary,
|
summary,
|
||||||
why,
|
why,
|
||||||
forecast,
|
forecast,
|
||||||
suggestedActions,
|
suggestedActions: this.filterPageRefreshAdvice(suggestedActions),
|
||||||
ownerHints,
|
ownerHints: this.filterPageRefreshAdvice(ownerHints),
|
||||||
generatedAt,
|
generatedAt,
|
||||||
};
|
};
|
||||||
if (input.recommendedReleaseWindow !== undefined) {
|
if (input.recommendedReleaseWindow !== undefined) {
|
||||||
@@ -234,6 +241,14 @@ export class AiService {
|
|||||||
return insight;
|
return insight;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private filterPageRefreshAdvice(items: string[]): string[] {
|
||||||
|
return items.filter((item) => !this.isPageRefreshAdvice(item));
|
||||||
|
}
|
||||||
|
|
||||||
|
private isPageRefreshAdvice(text: string): boolean {
|
||||||
|
return PAGE_REFRESH_ADVICE_PATTERNS.some((pattern) => pattern.test(text));
|
||||||
|
}
|
||||||
|
|
||||||
private readString(value: unknown): string | null {
|
private readString(value: unknown): string | null {
|
||||||
return typeof value === 'string' ? value : null;
|
return typeof value === 'string' ? value : null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ export const RISK_INTERPRET_SYSTEM_PROMPT = `你是 FTB 项目管理系统中的
|
|||||||
|
|
||||||
输入中的 signals 是规则引擎压缩后的结构化风险信号,优先用它判断 Bug、测试失败、阻塞、静默风险和临近发版的变化。
|
输入中的 signals 是规则引擎压缩后的结构化风险信号,优先用它判断 Bug、测试失败、阻塞、静默风险和临近发版的变化。
|
||||||
如果 confidence 低,必须提醒“预测可信度较低,需补充数据”。
|
如果 confidence 低,必须提醒“预测可信度较低,需补充数据”。
|
||||||
|
系统会自动更新小宝预警结果,用户不需要手动触发页面刷新或重新打开页面。
|
||||||
|
suggestedActions 只能给业务动作,例如修复 Bug、补充日报、复测失败用例、解除阻塞、调整排期或确认负责人;不要建议刷新页面、手动触发更新、重新加载浏览器或等待页面自动更新。
|
||||||
|
|
||||||
输出必须通过 submit_risk_interpretation 工具返回,不要输出自然语言正文或 Markdown。`;
|
输出必须通过 submit_risk_interpretation 工具返回,不要输出自然语言正文或 Markdown。`;
|
||||||
|
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ import { calcXiaobaoVersionRisk, type XiaobaoVersionRisk } from '@/lib/xiaobao-r
|
|||||||
import { buildRiskInsightSignature, findLatestRiskInsightForVersion, findPreviousRiskSnapshot, getReusableInsight, requestRiskInsight, shouldRequestRiskInsightWithCooldown } from '@/lib/xiaobao-risk-ai';
|
import { buildRiskInsightSignature, findLatestRiskInsightForVersion, findPreviousRiskSnapshot, getReusableInsight, requestRiskInsight, shouldRequestRiskInsightWithCooldown } 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 } from '@/lib/xiaobao-warning-view';
|
import { filterXiaobaoWarningVersions, sanitizeRiskInsight } from '@/lib/xiaobao-warning-view';
|
||||||
|
|
||||||
type RiskFilter = 'all' | 'attention' | 'high';
|
type RiskFilter = 'all' | 'attention' | 'high';
|
||||||
|
|
||||||
@@ -155,7 +155,7 @@ function XiaobaoWarningContent() {
|
|||||||
saveInsight({
|
saveInsight({
|
||||||
versionId: risk.versionId,
|
versionId: risk.versionId,
|
||||||
riskSignature: signature,
|
riskSignature: signature,
|
||||||
insight: response.result,
|
insight: sanitizeRiskInsight(response.result),
|
||||||
generatedAt: new Date().toISOString(),
|
generatedAt: new Date().toISOString(),
|
||||||
providerInfo: { model: response.meta.model },
|
providerInfo: { model: response.meta.model },
|
||||||
}).catch(() => {});
|
}).catch(() => {});
|
||||||
@@ -165,7 +165,7 @@ function XiaobaoWarningContent() {
|
|||||||
|
|
||||||
const risksWithInsight = useMemo(() => risks.map((risk) => {
|
const risksWithInsight = useMemo(() => risks.map((risk) => {
|
||||||
const cached = getReusableInsight(insights, risk);
|
const cached = getReusableInsight(insights, risk);
|
||||||
return cached ? { ...risk, aiInsight: cached.insight } : risk;
|
return cached ? { ...risk, aiInsight: sanitizeRiskInsight(cached.insight) } : risk;
|
||||||
}), [insights, risks]);
|
}), [insights, risks]);
|
||||||
|
|
||||||
const filteredRisks = useMemo(() => risksWithInsight.filter((risk) => {
|
const filteredRisks = useMemo(() => risksWithInsight.filter((risk) => {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import assert from 'node:assert/strict';
|
import assert from 'node:assert/strict';
|
||||||
import test from 'node:test';
|
import test from 'node:test';
|
||||||
import type { VersionWithContext } from './derive';
|
import type { VersionWithContext } from './derive';
|
||||||
import { filterXiaobaoWarningVersions, formatRemainingWork } from './xiaobao-warning-view';
|
import { filterXiaobaoWarningVersions, formatRemainingWork, sanitizeRiskInsight } from './xiaobao-warning-view';
|
||||||
|
|
||||||
function version(patch: Partial<VersionWithContext> = {}): VersionWithContext {
|
function version(patch: Partial<VersionWithContext> = {}): VersionWithContext {
|
||||||
return {
|
return {
|
||||||
@@ -52,3 +52,19 @@ test('formatRemainingWork keeps hours and adds work-day conversion', () => {
|
|||||||
assert.equal(formatRemainingWork(12), '12h / 1.5天');
|
assert.equal(formatRemainingWork(12), '12h / 1.5天');
|
||||||
assert.equal(formatRemainingWork(0.5), '0.5h / 0.1天');
|
assert.equal(formatRemainingWork(0.5), '0.5h / 0.1天');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('sanitizeRiskInsight filters invalid page refresh suggested actions', () => {
|
||||||
|
const result = sanitizeRiskInsight({
|
||||||
|
summary: '风险上升',
|
||||||
|
why: ['P1 Bug 增加'],
|
||||||
|
forecast: '预计延期 1 天',
|
||||||
|
suggestedActions: [
|
||||||
|
'手动触发页面刷新,等待小宝预警自动更新',
|
||||||
|
'优先处理 3 个 P1 Bug,并同步测试负责人复测',
|
||||||
|
],
|
||||||
|
ownerHints: ['研发负责人协调修复顺序'],
|
||||||
|
generatedAt: '2026-06-30T10:00:00.000Z',
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.deepEqual(result.suggestedActions, ['优先处理 3 个 P1 Bug,并同步测试负责人复测']);
|
||||||
|
});
|
||||||
|
|||||||
@@ -1,7 +1,15 @@
|
|||||||
import type { VersionWithContext } from './derive';
|
import type { VersionWithContext } from './derive';
|
||||||
|
import type { XiaobaoRiskInsight } from './xiaobao-risk-cache';
|
||||||
import { WORK_HOURS } from './work-hours';
|
import { WORK_HOURS } from './work-hours';
|
||||||
|
|
||||||
const UNFINISHED_VERSION_STATUSES = new Set(['planned', 'developing', 'paused']);
|
const UNFINISHED_VERSION_STATUSES = new Set(['planned', 'developing', 'paused']);
|
||||||
|
const PAGE_REFRESH_ADVICE_PATTERNS = [
|
||||||
|
/(刷新|重新加载|重载).*(页面|浏览器|小宝|预警)/i,
|
||||||
|
/(页面|浏览器|小宝|预警).*(刷新|重新加载|重载)/i,
|
||||||
|
/(手动|主动).*(触发|刷新).*(更新|预警|分析)/i,
|
||||||
|
/(manual|manually).*(refresh|reload|trigger)/i,
|
||||||
|
/(refresh|reload).*(page|browser|xiaobao|warning)/i,
|
||||||
|
];
|
||||||
|
|
||||||
export interface XiaobaoWarningVersionFilter {
|
export interface XiaobaoWarningVersionFilter {
|
||||||
canManage: boolean;
|
canManage: boolean;
|
||||||
@@ -26,7 +34,19 @@ export function formatRemainingWork(hours: number): string {
|
|||||||
return `${formatNumber(safeHours)}h / ${formatNumber(days)}天`;
|
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 {
|
function formatNumber(value: number): string {
|
||||||
if (Number.isInteger(value)) return String(value);
|
if (Number.isInteger(value)) return String(value);
|
||||||
return value.toFixed(1).replace(/\.0$/, '');
|
return value.toFixed(1).replace(/\.0$/, '');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isPageRefreshAdvice(text: string): boolean {
|
||||||
|
return PAGE_REFRESH_ADVICE_PATTERNS.some((pattern) => pattern.test(text));
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user