feat(ai): 优化拆解重跑与结果展示

This commit is contained in:
Script Generator
2026-06-26 11:01:11 +08:00
parent 55e24442ab
commit 56e0fe562d
36 changed files with 1342 additions and 186 deletions

View File

@@ -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 必须返回空数组');
});
});

View File

@@ -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;
}
}

View File

@@ -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)

View File

@@ -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'],
},
},
},