feat(ai): 优化拆解重跑与结果展示
This commit is contained in:
@@ -147,9 +147,48 @@ describe('AiService', () => {
|
||||
expect(testCaseRequired).toContain('categoryCode');
|
||||
});
|
||||
|
||||
it('requires estimateHours for test case drafts', () => {
|
||||
it('requires aiEstimateHours for dev task and test case drafts', () => {
|
||||
const devRequired = (DECOMPOSE_TOOL_INPUT_SCHEMA.properties.devTaskDrafts as any).items.required;
|
||||
const testCaseRequired = (DECOMPOSE_TOOL_INPUT_SCHEMA.properties.testCaseDrafts as any).items.required;
|
||||
|
||||
expect(testCaseRequired).toContain('estimateHours');
|
||||
expect(devRequired).toContain('aiEstimateHours');
|
||||
expect(devRequired).not.toContain('estimateHours');
|
||||
expect(testCaseRequired).toContain('aiEstimateHours');
|
||||
expect(testCaseRequired).not.toContain('estimateHours');
|
||||
});
|
||||
|
||||
it('passes decomposition target into the model prompt', async () => {
|
||||
const callTool = jest.fn().mockResolvedValue({
|
||||
toolName: 'submit_decompose',
|
||||
toolInput: {
|
||||
report: { matched: [], reqOnly: ['req-1'], noteOnly: [], ambiguous: [] },
|
||||
devTaskDrafts: [],
|
||||
testCaseDrafts: [],
|
||||
},
|
||||
inputTokens: 100,
|
||||
outputTokens: 20,
|
||||
rawModel: 'claude-test',
|
||||
});
|
||||
|
||||
const gateway = {
|
||||
getActiveProvider: jest.fn().mockResolvedValue({ callTool }),
|
||||
getActiveModel: jest.fn().mockResolvedValue('claude-test'),
|
||||
} as unknown as AiGatewayService;
|
||||
|
||||
const service = new AiService(gateway);
|
||||
(service as any).fetchPrototype = jest.fn().mockResolvedValue('QY0001:手机号登录');
|
||||
|
||||
await service.decompose({
|
||||
prototypeUrl: 'https://example.com/prototype',
|
||||
requirements: [{ id: 'req-1', code: 'REQ001', title: '手机登录' }],
|
||||
members: [{ name: '张三', role: 'frontend' }],
|
||||
versionId: 'version-1',
|
||||
planId: 'plan-1',
|
||||
target: 'dev_tasks',
|
||||
});
|
||||
|
||||
const userPrompt = callTool.mock.calls[0][0].userPrompt;
|
||||
expect(userPrompt).toContain('本次只重新拆解开发任务');
|
||||
expect(userPrompt).toContain('testCaseDrafts 必须返回空数组');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -12,6 +12,7 @@ import type {
|
||||
AgentDecomposeResponse,
|
||||
AgentDecomposeError,
|
||||
AgentDecomposeResult,
|
||||
AgentDecomposeTarget,
|
||||
} from '@ftb/shared';
|
||||
|
||||
const DECOMPOSE_CONTEXT_CHAR_STEPS = [1800, 1200, 800] as const;
|
||||
@@ -107,7 +108,7 @@ export class AiService {
|
||||
};
|
||||
}
|
||||
|
||||
const result = toolResp.toolInput as AgentDecomposeResult;
|
||||
const result = this.applyTargetToResult(toolResp.toolInput as AgentDecomposeResult, req.target);
|
||||
if (!result.report || !Array.isArray(result.devTaskDrafts) || !Array.isArray(result.testCaseDrafts)) {
|
||||
return {
|
||||
ok: false,
|
||||
@@ -195,6 +196,7 @@ export class AiService {
|
||||
.join('\n');
|
||||
|
||||
const memberList = req.members.map((m) => `- ${m.name} (${m.role})`).join('\n');
|
||||
const targetInstruction = this.buildTargetInstruction(req.target ?? 'all');
|
||||
|
||||
return `${isRetry ? '## 重试说明\n上一次 AI 服务未返回工具结果,本次已缩短原型上下文。仍然必须只通过 submit_decompose 工具返回结果。\n\n' : ''}## 产品方案原型
|
||||
|
||||
@@ -215,9 +217,36 @@ ${reqList || '(无)'}
|
||||
|
||||
${memberList || '(无)'}
|
||||
|
||||
## 本次拆解目标
|
||||
|
||||
${targetInstruction}
|
||||
|
||||
## 你的任务
|
||||
|
||||
按系统提示词的规则,拆解出开发任务草案、测试用例草案、对账报告。
|
||||
通过 tool 调用 submit_decompose 返回结果。`;
|
||||
}
|
||||
|
||||
private buildTargetInstruction(target: AgentDecomposeTarget): string {
|
||||
if (target === 'dev_tasks') {
|
||||
return '本次只重新拆解开发任务;devTaskDrafts 正常返回,testCaseDrafts 必须返回空数组。';
|
||||
}
|
||||
if (target === 'test_cases') {
|
||||
return '本次只重新拆解测试用例;testCaseDrafts 正常返回,devTaskDrafts 必须返回空数组。';
|
||||
}
|
||||
return '本次同时拆解开发任务和测试用例;devTaskDrafts 与 testCaseDrafts 都按实际需要返回。';
|
||||
}
|
||||
|
||||
private applyTargetToResult(
|
||||
result: AgentDecomposeResult,
|
||||
target: AgentDecomposeTarget = 'all',
|
||||
): AgentDecomposeResult {
|
||||
if (target === 'dev_tasks') {
|
||||
return { ...result, testCaseDrafts: [] };
|
||||
}
|
||||
if (target === 'test_cases') {
|
||||
return { ...result, devTaskDrafts: [] };
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { IsString, IsArray, ValidateNested, IsOptional } from 'class-validator';
|
||||
import { IsString, IsArray, ValidateNested, IsOptional, IsIn } from 'class-validator';
|
||||
import { Type } from 'class-transformer';
|
||||
|
||||
export class DecomposeReqMemberDto {
|
||||
@@ -34,6 +34,10 @@ export class DecomposeDto {
|
||||
@IsString()
|
||||
planId!: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsIn(['all', 'dev_tasks', 'test_cases'])
|
||||
target?: 'all' | 'dev_tasks' | 'test_cases';
|
||||
|
||||
@IsArray()
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => DecomposeReqRequirementDto)
|
||||
|
||||
@@ -38,15 +38,18 @@ export const DECOMPOSE_SYSTEM_PROMPT = `你是 FTB 项目管理系统的产品
|
||||
- 不输出数据库 categoryId
|
||||
- 不输出推荐负责人(用户后续手填)
|
||||
|
||||
5. 工时估算(小时,按团队使用 AI 辅助研发/测试估算,必须偏严格)
|
||||
5. AI 工时估算(字段名 aiEstimateHours,单位小时)
|
||||
- aiEstimateHours 只代表 AI 对工作量的判断,不代表负责人计划排期
|
||||
- 不要输出 estimateHours、预计开始时间、预计截止时间
|
||||
- 简单前端字段、文案、展示调整: 0.25-0.5h
|
||||
- 简单前端交互,如拖拽排序 UI、开关、筛选项: 0.5-1h
|
||||
- 拖拽排序并需要持久化接口: 1-1.5h
|
||||
- 简单 CRUD 接口: 0.75-1.5h
|
||||
- 数据库字段/索引调整: 0.5h
|
||||
- 中等业务规则变更: 1.5-3h
|
||||
- 简单功能测试用例执行: 0.25-0.5h
|
||||
- API/异常/兼容性测试用例执行: 0.5-1h
|
||||
- 简单前端交互,如拖拽排序 UI、开关、筛选项: 0.25-0.5h
|
||||
- 拖拽排序并需要持久化接口: 0.75-1h
|
||||
- 简单 CRUD 接口: 0.5-1h
|
||||
- 数据库字段/索引调整: 0.25-0.5h
|
||||
- 中等业务规则变更: 1-2h
|
||||
- 简单功能测试用例执行: 0.1-0.3h
|
||||
- API/异常测试用例执行: 0.2-0.5h
|
||||
- 兼容性测试用例执行: 0.3-0.75h
|
||||
- 只有跨端同步、复杂权限、历史数据迁移、强一致性、复杂兼容性时,才允许超过上述区间
|
||||
|
||||
6. 标题:中文动词开头,简洁
|
||||
@@ -133,7 +136,7 @@ export const DECOMPOSE_TOOL_INPUT_SCHEMA = {
|
||||
],
|
||||
},
|
||||
priority: { type: 'string', enum: ['P0', 'P1', 'P2', 'P3'] },
|
||||
estimateHours: { type: 'number' },
|
||||
aiEstimateHours: { type: 'number' },
|
||||
references: {
|
||||
type: 'array',
|
||||
items: {
|
||||
@@ -148,7 +151,7 @@ export const DECOMPOSE_TOOL_INPUT_SCHEMA = {
|
||||
minItems: 1,
|
||||
},
|
||||
},
|
||||
required: ['title', 'categoryCode', 'priority', 'estimateHours', 'references'],
|
||||
required: ['title', 'categoryCode', 'priority', 'aiEstimateHours', 'references'],
|
||||
},
|
||||
},
|
||||
testCaseDrafts: {
|
||||
@@ -163,7 +166,7 @@ export const DECOMPOSE_TOOL_INPUT_SCHEMA = {
|
||||
enum: ['test_functional', 'test_api', 'test_exception', 'test_compatibility'],
|
||||
},
|
||||
priority: { type: 'string', enum: ['P0', 'P1', 'P2', 'P3'] },
|
||||
estimateHours: { type: 'number' },
|
||||
aiEstimateHours: { type: 'number' },
|
||||
references: {
|
||||
type: 'array',
|
||||
items: {
|
||||
@@ -178,7 +181,7 @@ export const DECOMPOSE_TOOL_INPUT_SCHEMA = {
|
||||
minItems: 1,
|
||||
},
|
||||
},
|
||||
required: ['title', 'description', 'categoryCode', 'priority', 'estimateHours', 'references'],
|
||||
required: ['title', 'description', 'categoryCode', 'priority', 'aiEstimateHours', 'references'],
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user