feat(小宝预警): 增加 AI 风险解读接口
This commit is contained in:
@@ -1,7 +1,13 @@
|
||||
import { Body, Controller, Post } from '@nestjs/common';
|
||||
import { AiService } from './ai.service';
|
||||
import { DecomposeDto } from './dto/decompose.dto';
|
||||
import type { AgentDecomposeResponse, AgentDecomposeError } from '@ftb/shared';
|
||||
import { RiskInterpretDto } from './dto/risk-interpret.dto';
|
||||
import type {
|
||||
AgentDecomposeResponse,
|
||||
AgentDecomposeError,
|
||||
AgentRiskInterpretResponse,
|
||||
AgentRiskInterpretError,
|
||||
} from '@ftb/shared';
|
||||
|
||||
@Controller('ai')
|
||||
export class AiController {
|
||||
@@ -11,4 +17,9 @@ export class AiController {
|
||||
async decompose(@Body() dto: DecomposeDto): Promise<AgentDecomposeResponse | AgentDecomposeError> {
|
||||
return this.aiService.decompose(dto);
|
||||
}
|
||||
|
||||
@Post('risk-interpret')
|
||||
async interpretRisk(@Body() dto: RiskInterpretDto): Promise<AgentRiskInterpretResponse | AgentRiskInterpretError> {
|
||||
return this.aiService.interpretRisk(dto);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,230 @@
|
||||
import { AiService } from './ai.service';
|
||||
import type { AiGatewayService } from './ai-gateway.service';
|
||||
import { DECOMPOSE_SYSTEM_PROMPT, DECOMPOSE_TOOL_INPUT_SCHEMA } from './prompts/decompose';
|
||||
import { RISK_INTERPRET_TOOL_INPUT_SCHEMA } from './prompts/risk-interpret';
|
||||
import { plainToInstance } from 'class-transformer';
|
||||
import { validateSync } from 'class-validator';
|
||||
import { RiskInterpretDto } from './dto/risk-interpret.dto';
|
||||
import type { AgentRiskInterpretRequest } from '@ftb/shared';
|
||||
|
||||
const missingToolUseMessage =
|
||||
'Anthropic 未通过 tool_use 返回结果;stop_reason=tool_use;content=text("⚠️ 上游模型未返回任何内容。可能原因:触发了安全策略、上游限流、或模型对当前输入直接结束。")';
|
||||
|
||||
function createRiskRequest(patch: Partial<AgentRiskInterpretRequest> = {}): AgentRiskInterpretRequest {
|
||||
return {
|
||||
versionId: 'ver-1',
|
||||
versionName: 'V1.0',
|
||||
riskScore: 70,
|
||||
riskLevel: 'at_risk',
|
||||
delayDays: 1,
|
||||
confidence: 80,
|
||||
signals: {
|
||||
unfinishedCount: 4,
|
||||
openBugCount: 3,
|
||||
criticalBugCount: 3,
|
||||
failedTestCount: 1,
|
||||
blockedCount: 0,
|
||||
silentRiskCount: 0,
|
||||
daysToExpectedRelease: 1,
|
||||
},
|
||||
trendSummary: 'risk rising',
|
||||
reasons: [{ key: 'critical_bug', label: 'Critical Bug', severity: 'critical', detail: '3 P1 bugs' }],
|
||||
silentRisks: [],
|
||||
dailyEvidence: {
|
||||
todayDeliveries: [],
|
||||
todayProgress: [],
|
||||
todayRisks: ['tests failed'],
|
||||
progressNotes: [],
|
||||
needsProgressItems: [],
|
||||
recentActivityCount: 1,
|
||||
},
|
||||
...patch,
|
||||
};
|
||||
}
|
||||
|
||||
describe('AiService', () => {
|
||||
it('interprets version risk through the active provider', async () => {
|
||||
const callTool = jest.fn().mockResolvedValue({
|
||||
toolName: 'submit_risk_interpretation',
|
||||
toolInput: {
|
||||
summary: '预计延期',
|
||||
why: ['P1 Bug 增加'],
|
||||
forecast: '预计 2026-07-04 可发',
|
||||
recommendedReleaseWindow: '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({
|
||||
versionId: 'ver-1',
|
||||
versionName: 'V1.0',
|
||||
riskScore: 70,
|
||||
riskLevel: 'at_risk',
|
||||
delayDays: 1,
|
||||
confidence: 80,
|
||||
signals: {
|
||||
unfinishedCount: 4,
|
||||
openBugCount: 3,
|
||||
criticalBugCount: 3,
|
||||
failedTestCount: 1,
|
||||
blockedCount: 0,
|
||||
silentRiskCount: 0,
|
||||
daysToExpectedRelease: 1,
|
||||
},
|
||||
trendSummary: '风险上升',
|
||||
reasons: [{ key: 'critical_bug', label: '关键 Bug', severity: 'critical', detail: '3 个 P1 Bug' }],
|
||||
silentRisks: [],
|
||||
dailyEvidence: {
|
||||
todayDeliveries: [],
|
||||
todayProgress: [],
|
||||
todayRisks: ['测试不通过'],
|
||||
progressNotes: [],
|
||||
needsProgressItems: [],
|
||||
recentActivityCount: 1,
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.ok).toBe(true);
|
||||
if (result.ok) expect(result.result.summary).toBe('预计延期');
|
||||
expect(callTool).toHaveBeenCalledWith(expect.objectContaining({
|
||||
forceTool: true,
|
||||
maxTokens: 3000,
|
||||
tool: expect.objectContaining({ name: 'submit_risk_interpretation' }),
|
||||
}));
|
||||
expect(callTool.mock.calls[0][0].userPrompt).toContain('风险上升');
|
||||
});
|
||||
|
||||
it('returns PARSE_ERROR when risk provider returns malformed payload', async () => {
|
||||
const gateway = {
|
||||
getActiveProvider: jest.fn().mockResolvedValue({
|
||||
callTool: jest.fn().mockResolvedValue({
|
||||
toolName: 'submit_risk_interpretation',
|
||||
toolInput: null,
|
||||
rawModel: 'test-model',
|
||||
inputTokens: 10,
|
||||
outputTokens: 20,
|
||||
}),
|
||||
}),
|
||||
getActiveModel: jest.fn().mockResolvedValue('test-model'),
|
||||
} as unknown as AiGatewayService;
|
||||
const service = new AiService(gateway);
|
||||
|
||||
const result = await service.interpretRisk(createRiskRequest());
|
||||
|
||||
expect(result).toMatchObject({ ok: false, code: 'PARSE_ERROR' });
|
||||
});
|
||||
|
||||
it('returns PARSE_ERROR when risk provider returns the wrong tool name', async () => {
|
||||
const gateway = {
|
||||
getActiveProvider: jest.fn().mockResolvedValue({
|
||||
callTool: jest.fn().mockResolvedValue({
|
||||
toolName: 'submit_decompose',
|
||||
toolInput: {
|
||||
summary: 'wrong tool',
|
||||
why: ['bad'],
|
||||
forecast: 'bad',
|
||||
suggestedActions: ['bad'],
|
||||
ownerHints: ['bad'],
|
||||
generatedAt: '2026-06-29T01:00:00.000Z',
|
||||
},
|
||||
rawModel: 'test-model',
|
||||
inputTokens: 10,
|
||||
outputTokens: 20,
|
||||
}),
|
||||
}),
|
||||
getActiveModel: jest.fn().mockResolvedValue('test-model'),
|
||||
} as unknown as AiGatewayService;
|
||||
const service = new AiService(gateway);
|
||||
|
||||
const result = await service.interpretRisk(createRiskRequest());
|
||||
|
||||
expect(result).toMatchObject({ ok: false, code: 'PARSE_ERROR' });
|
||||
});
|
||||
|
||||
it('returns PARSE_ERROR when risk insight arrays contain non-string values', async () => {
|
||||
const gateway = {
|
||||
getActiveProvider: jest.fn().mockResolvedValue({
|
||||
callTool: jest.fn().mockResolvedValue({
|
||||
toolName: 'submit_risk_interpretation',
|
||||
toolInput: {
|
||||
summary: 'bad array',
|
||||
why: [123],
|
||||
forecast: 'bad',
|
||||
suggestedActions: ['bad'],
|
||||
ownerHints: ['bad'],
|
||||
generatedAt: '2026-06-29T01:00:00.000Z',
|
||||
},
|
||||
rawModel: 'test-model',
|
||||
inputTokens: 10,
|
||||
outputTokens: 20,
|
||||
}),
|
||||
}),
|
||||
getActiveModel: jest.fn().mockResolvedValue('test-model'),
|
||||
} as unknown as AiGatewayService;
|
||||
const service = new AiService(gateway);
|
||||
|
||||
const result = await service.interpretRisk(createRiskRequest());
|
||||
|
||||
expect(result).toMatchObject({ ok: false, code: 'PARSE_ERROR' });
|
||||
});
|
||||
|
||||
it('validates risk daily evidence array items as strings', () => {
|
||||
const dto = plainToInstance(RiskInterpretDto, {
|
||||
...createRiskRequest(),
|
||||
dailyEvidence: {
|
||||
todayDeliveries: [],
|
||||
todayProgress: [],
|
||||
todayRisks: [123],
|
||||
progressNotes: [],
|
||||
needsProgressItems: [],
|
||||
recentActivityCount: 1,
|
||||
},
|
||||
});
|
||||
|
||||
const errors = validateSync(dto);
|
||||
|
||||
expect(JSON.stringify(errors)).toContain('todayRisks');
|
||||
});
|
||||
|
||||
it('requires risk signals and daily evidence objects in DTO validation', () => {
|
||||
const { signals: _signals, dailyEvidence: _dailyEvidence, ...payload } = createRiskRequest();
|
||||
const dto = plainToInstance(RiskInterpretDto, payload);
|
||||
|
||||
const errors = validateSync(dto);
|
||||
const serialized = JSON.stringify(errors);
|
||||
|
||||
expect(serialized).toContain('signals');
|
||||
expect(serialized).toContain('dailyEvidence');
|
||||
});
|
||||
|
||||
it('forbids extra fields in risk interpretation tool schema', () => {
|
||||
expect((RISK_INTERPRET_TOOL_INPUT_SCHEMA as any).additionalProperties).toBe(false);
|
||||
});
|
||||
|
||||
it('returns PARSE_ERROR when provider reports invalid tool arguments', async () => {
|
||||
const gateway = {
|
||||
getActiveProvider: jest.fn().mockResolvedValue({
|
||||
callTool: jest.fn().mockRejectedValue(new Error('OpenAI tool_call arguments is not valid JSON')),
|
||||
}),
|
||||
getActiveModel: jest.fn().mockResolvedValue('test-model'),
|
||||
} as unknown as AiGatewayService;
|
||||
const service = new AiService(gateway);
|
||||
|
||||
const result = await service.interpretRisk(createRiskRequest());
|
||||
|
||||
expect(result).toMatchObject({ ok: false, code: 'PARSE_ERROR' });
|
||||
});
|
||||
|
||||
it('retries once with shorter prototype context when Anthropic returns no tool_use', async () => {
|
||||
const callTool = jest
|
||||
.fn()
|
||||
|
||||
@@ -6,6 +6,12 @@ import {
|
||||
DECOMPOSE_TOOL_DESCRIPTION,
|
||||
DECOMPOSE_TOOL_INPUT_SCHEMA,
|
||||
} from './prompts/decompose';
|
||||
import {
|
||||
RISK_INTERPRET_SYSTEM_PROMPT,
|
||||
RISK_INTERPRET_TOOL_NAME,
|
||||
RISK_INTERPRET_TOOL_DESCRIPTION,
|
||||
RISK_INTERPRET_TOOL_INPUT_SCHEMA,
|
||||
} from './prompts/risk-interpret';
|
||||
import { buildPrototypeContext } from './prototype-context';
|
||||
import type {
|
||||
AgentDecomposeRequest,
|
||||
@@ -13,6 +19,10 @@ import type {
|
||||
AgentDecomposeError,
|
||||
AgentDecomposeResult,
|
||||
AgentDecomposeTarget,
|
||||
AgentRiskInsight,
|
||||
AgentRiskInterpretError,
|
||||
AgentRiskInterpretRequest,
|
||||
AgentRiskInterpretResponse,
|
||||
} from '@ftb/shared';
|
||||
|
||||
const DECOMPOSE_CONTEXT_CHAR_STEPS = [1800, 1200, 800] as const;
|
||||
@@ -129,6 +139,119 @@ export class AiService {
|
||||
};
|
||||
}
|
||||
|
||||
async interpretRisk(req: AgentRiskInterpretRequest): Promise<AgentRiskInterpretResponse | AgentRiskInterpretError> {
|
||||
const startedAt = Date.now();
|
||||
let provider;
|
||||
let model: string;
|
||||
try {
|
||||
provider = await this.gateway.getActiveProvider();
|
||||
model = await this.gateway.getActiveModel();
|
||||
} catch (e: any) {
|
||||
return {
|
||||
ok: false,
|
||||
error: e?.message || '未配置 AI 提供商',
|
||||
code: 'NO_PROVIDER',
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
const toolResp = await provider.callTool({
|
||||
systemPrompt: RISK_INTERPRET_SYSTEM_PROMPT,
|
||||
userPrompt: `请基于以下小宝预警规则结果生成解释,禁止编造新事实:\n${JSON.stringify(req, null, 2)}`,
|
||||
tool: {
|
||||
name: RISK_INTERPRET_TOOL_NAME,
|
||||
description: RISK_INTERPRET_TOOL_DESCRIPTION,
|
||||
inputSchema: RISK_INTERPRET_TOOL_INPUT_SCHEMA,
|
||||
},
|
||||
forceTool: true,
|
||||
maxTokens: 3000,
|
||||
model,
|
||||
});
|
||||
const result = this.parseRiskInsightToolResponse(toolResp);
|
||||
if (!result) {
|
||||
return {
|
||||
ok: false,
|
||||
error: 'AI 返回结构不符合预期',
|
||||
code: 'PARSE_ERROR',
|
||||
};
|
||||
}
|
||||
return {
|
||||
ok: true,
|
||||
result,
|
||||
meta: {
|
||||
model: toolResp.rawModel || model,
|
||||
inputTokens: toolResp.inputTokens,
|
||||
outputTokens: toolResp.outputTokens,
|
||||
durationMs: Date.now() - startedAt,
|
||||
},
|
||||
};
|
||||
} catch (e: any) {
|
||||
if (this.isProviderParseError(e)) {
|
||||
return {
|
||||
ok: false,
|
||||
error: e?.message || 'AI 返回结构不符合预期',
|
||||
code: 'PARSE_ERROR',
|
||||
};
|
||||
}
|
||||
this.logger.error(`AI 风险解读调用失败: ${e.message}`);
|
||||
return {
|
||||
ok: false,
|
||||
error: `AI 服务调用失败:${e.message}`,
|
||||
code: 'API_ERROR',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private parseRiskInsightToolResponse(toolResp: any): AgentRiskInsight | null {
|
||||
if (!toolResp || toolResp.toolName !== RISK_INTERPRET_TOOL_NAME || !this.isRecord(toolResp.toolInput)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const input = toolResp.toolInput;
|
||||
const summary = this.readString(input.summary);
|
||||
const forecast = this.readString(input.forecast);
|
||||
const generatedAt = this.readString(input.generatedAt);
|
||||
const why = this.readStringArray(input.why);
|
||||
const suggestedActions = this.readStringArray(input.suggestedActions);
|
||||
const ownerHints = this.readStringArray(input.ownerHints);
|
||||
if (!summary || !forecast || !generatedAt || !why || !suggestedActions || !ownerHints) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const insight: AgentRiskInsight = {
|
||||
summary,
|
||||
why,
|
||||
forecast,
|
||||
suggestedActions,
|
||||
ownerHints,
|
||||
generatedAt,
|
||||
};
|
||||
if (input.recommendedReleaseWindow !== undefined) {
|
||||
const recommendedReleaseWindow = this.readString(input.recommendedReleaseWindow);
|
||||
if (!recommendedReleaseWindow) return null;
|
||||
insight.recommendedReleaseWindow = recommendedReleaseWindow;
|
||||
}
|
||||
return insight;
|
||||
}
|
||||
|
||||
private readString(value: unknown): string | null {
|
||||
return typeof value === 'string' ? value : null;
|
||||
}
|
||||
|
||||
private readStringArray(value: unknown): string[] | null {
|
||||
if (!Array.isArray(value) || value.some((item) => typeof item !== 'string')) return null;
|
||||
return value;
|
||||
}
|
||||
|
||||
private isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
||||
}
|
||||
|
||||
private isProviderParseError(error: any): boolean {
|
||||
const message = String(error?.message || '');
|
||||
return message.includes('tool_call arguments') && message.includes('JSON');
|
||||
}
|
||||
|
||||
private async fetchPrototype(url: string): Promise<string> {
|
||||
if (!url || !url.startsWith('http')) {
|
||||
throw new Error('原型链接无效');
|
||||
|
||||
142
apps/server/src/modules/ai/dto/risk-interpret.dto.ts
Normal file
142
apps/server/src/modules/ai/dto/risk-interpret.dto.ts
Normal file
@@ -0,0 +1,142 @@
|
||||
import { Type } from 'class-transformer';
|
||||
import { IsArray, IsDefined, IsIn, IsNumber, IsObject, IsOptional, IsString, ValidateNested } from 'class-validator';
|
||||
|
||||
class RiskReasonDto {
|
||||
@IsString()
|
||||
key!: string;
|
||||
|
||||
@IsString()
|
||||
label!: string;
|
||||
|
||||
@IsIn(['low', 'medium', 'high', 'critical'])
|
||||
severity!: 'low' | 'medium' | 'high' | 'critical';
|
||||
|
||||
@IsString()
|
||||
detail!: string;
|
||||
}
|
||||
|
||||
class SilentRiskDto {
|
||||
@IsString()
|
||||
key!: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
days?: number;
|
||||
|
||||
@IsString()
|
||||
detail!: string;
|
||||
}
|
||||
|
||||
class RiskDailyEvidenceDto {
|
||||
@IsArray()
|
||||
@IsString({ each: true })
|
||||
todayDeliveries!: string[];
|
||||
|
||||
@IsArray()
|
||||
@IsString({ each: true })
|
||||
todayProgress!: string[];
|
||||
|
||||
@IsArray()
|
||||
@IsString({ each: true })
|
||||
todayRisks!: string[];
|
||||
|
||||
@IsArray()
|
||||
@IsString({ each: true })
|
||||
progressNotes!: string[];
|
||||
|
||||
@IsArray()
|
||||
@IsString({ each: true })
|
||||
needsProgressItems!: string[];
|
||||
|
||||
@IsNumber()
|
||||
recentActivityCount!: number;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
lastActivityAt?: string;
|
||||
}
|
||||
|
||||
class RiskSignalsDto {
|
||||
@IsNumber()
|
||||
unfinishedCount!: number;
|
||||
|
||||
@IsNumber()
|
||||
openBugCount!: number;
|
||||
|
||||
@IsNumber()
|
||||
criticalBugCount!: number;
|
||||
|
||||
@IsNumber()
|
||||
failedTestCount!: number;
|
||||
|
||||
@IsNumber()
|
||||
blockedCount!: number;
|
||||
|
||||
@IsNumber()
|
||||
silentRiskCount!: number;
|
||||
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
daysToExpectedRelease?: number;
|
||||
}
|
||||
|
||||
export class RiskInterpretDto {
|
||||
@IsString()
|
||||
versionId!: string;
|
||||
|
||||
@IsString()
|
||||
versionName!: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
productName?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
projectName?: string;
|
||||
|
||||
@IsNumber()
|
||||
riskScore!: number;
|
||||
|
||||
@IsIn(['on_track', 'attention', 'at_risk', 'likely_delayed', 'blocked'])
|
||||
riskLevel!: 'on_track' | 'attention' | 'at_risk' | 'likely_delayed' | 'blocked';
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
expectedReleaseDate?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
forecastReleaseDate?: string;
|
||||
|
||||
@IsNumber()
|
||||
delayDays!: number;
|
||||
|
||||
@IsNumber()
|
||||
confidence!: number;
|
||||
|
||||
@IsDefined()
|
||||
@IsObject()
|
||||
@ValidateNested()
|
||||
@Type(() => RiskSignalsDto)
|
||||
signals!: RiskSignalsDto;
|
||||
|
||||
@IsString()
|
||||
trendSummary!: string;
|
||||
|
||||
@IsArray()
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => RiskReasonDto)
|
||||
reasons!: RiskReasonDto[];
|
||||
|
||||
@IsArray()
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => SilentRiskDto)
|
||||
silentRisks!: SilentRiskDto[];
|
||||
|
||||
@IsDefined()
|
||||
@IsObject()
|
||||
@ValidateNested()
|
||||
@Type(() => RiskDailyEvidenceDto)
|
||||
dailyEvidence!: RiskDailyEvidenceDto;
|
||||
}
|
||||
32
apps/server/src/modules/ai/prompts/risk-interpret.ts
Normal file
32
apps/server/src/modules/ai/prompts/risk-interpret.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
export const RISK_INTERPRET_TOOL_NAME = 'submit_risk_interpretation';
|
||||
export const RISK_INTERPRET_TOOL_DESCRIPTION = '返回小宝预警的结构化解释';
|
||||
|
||||
export const RISK_INTERPRET_SYSTEM_PROMPT = `你是 FTB 项目管理系统中的“小宝预警”解释助手。
|
||||
|
||||
你只能解释系统给出的规则预测结果和证据,不能编造不存在的数据。
|
||||
你需要用项目经理能理解的中文说明:
|
||||
1. 当前能不能按期发版;
|
||||
2. 为什么有风险;
|
||||
3. 预计延期或建议发版窗口;
|
||||
4. 需要优先处理的动作;
|
||||
5. 哪些负责人或角色需要关注。
|
||||
|
||||
输入中的 signals 是规则引擎压缩后的结构化风险信号,优先用它判断 Bug、测试失败、阻塞、静默风险和临近发版的变化。
|
||||
如果 confidence 低,必须提醒“预测可信度较低,需补充数据”。
|
||||
|
||||
输出必须通过 submit_risk_interpretation 工具返回,不要输出自然语言正文或 Markdown。`;
|
||||
|
||||
export const RISK_INTERPRET_TOOL_INPUT_SCHEMA = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
summary: { type: 'string' },
|
||||
why: { type: 'array', items: { type: 'string' } },
|
||||
forecast: { type: 'string' },
|
||||
recommendedReleaseWindow: { type: 'string' },
|
||||
suggestedActions: { type: 'array', items: { type: 'string' } },
|
||||
ownerHints: { type: 'array', items: { type: 'string' } },
|
||||
generatedAt: { type: 'string' },
|
||||
},
|
||||
required: ['summary', 'why', 'forecast', 'suggestedActions', 'ownerHints', 'generatedAt'],
|
||||
additionalProperties: false,
|
||||
};
|
||||
@@ -114,6 +114,74 @@ export interface AgentDecomposeError {
|
||||
code: 'PROTOTYPE_FETCH_FAILED' | 'EMPTY_REQUIREMENTS' | 'API_ERROR' | 'PARSE_ERROR' | 'NO_PROVIDER' | 'UNKNOWN';
|
||||
}
|
||||
|
||||
export interface AgentRiskReason {
|
||||
key: string;
|
||||
label: string;
|
||||
severity: 'low' | 'medium' | 'high' | 'critical';
|
||||
detail: string;
|
||||
}
|
||||
|
||||
export interface AgentRiskInterpretRequest {
|
||||
versionId: string;
|
||||
versionName: string;
|
||||
productName?: string;
|
||||
projectName?: string;
|
||||
riskScore: number;
|
||||
riskLevel: 'on_track' | 'attention' | 'at_risk' | 'likely_delayed' | 'blocked';
|
||||
expectedReleaseDate?: string | null;
|
||||
forecastReleaseDate?: string;
|
||||
delayDays: number;
|
||||
confidence: number;
|
||||
signals: {
|
||||
unfinishedCount: number;
|
||||
openBugCount: number;
|
||||
criticalBugCount: number;
|
||||
failedTestCount: number;
|
||||
blockedCount: number;
|
||||
silentRiskCount: number;
|
||||
daysToExpectedRelease?: number;
|
||||
};
|
||||
trendSummary: string;
|
||||
reasons: AgentRiskReason[];
|
||||
silentRisks: Array<{ key: string; days?: number; detail: string }>;
|
||||
dailyEvidence: {
|
||||
todayDeliveries: string[];
|
||||
todayProgress: string[];
|
||||
todayRisks: string[];
|
||||
progressNotes: string[];
|
||||
needsProgressItems: string[];
|
||||
recentActivityCount: number;
|
||||
lastActivityAt?: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface AgentRiskInsight {
|
||||
summary: string;
|
||||
why: string[];
|
||||
forecast: string;
|
||||
recommendedReleaseWindow?: string;
|
||||
suggestedActions: string[];
|
||||
ownerHints: string[];
|
||||
generatedAt: string;
|
||||
}
|
||||
|
||||
export interface AgentRiskInterpretResponse {
|
||||
ok: true;
|
||||
result: AgentRiskInsight;
|
||||
meta: {
|
||||
model: string;
|
||||
inputTokens: number;
|
||||
outputTokens: number;
|
||||
durationMs: number;
|
||||
};
|
||||
}
|
||||
|
||||
export interface AgentRiskInterpretError {
|
||||
ok: false;
|
||||
error: string;
|
||||
code: 'API_ERROR' | 'PARSE_ERROR' | 'NO_PROVIDER' | 'UNKNOWN';
|
||||
}
|
||||
|
||||
/* ───────────────────────── AI Provider 配置 ─────────────────────────── */
|
||||
|
||||
export type AiProviderFormat = 'anthropic' | 'openai';
|
||||
|
||||
Reference in New Issue
Block a user