fix lint
Some checks failed
continuous-integration/drone/push Build is failing

This commit is contained in:
2026-01-15 02:36:24 +03:00
parent dea0676169
commit 2e46cc41a1
42 changed files with 940 additions and 301 deletions

View File

@ -1,5 +1,10 @@
import { Controller, Post, Get, Delete, Body, Param } from '@nestjs/common';
import { AiService, EstimateResult, SpecificationResult, SpecificationHistoryItem } from './ai.service';
import {
AiService,
EstimateResult,
SpecificationResult,
SpecificationHistoryItem,
} from './ai.service';
import { EstimateIdeaDto, GenerateSpecificationDto } from './dto';
@Controller('ai')

View File

@ -8,7 +8,9 @@ import { SpecificationHistory } from '../ideas/entities/specification-history.en
import { Comment } from '../comments/entities/comment.entity';
@Module({
imports: [TypeOrmModule.forFeature([Idea, TeamMember, SpecificationHistory, Comment])],
imports: [
TypeOrmModule.forFeature([Idea, TeamMember, SpecificationHistory, Comment]),
],
controllers: [AiController],
providers: [AiService],
exports: [AiService],

View File

@ -36,6 +36,21 @@ export interface SpecificationHistoryItem {
createdAt: Date;
}
interface AiProxyResponse {
choices: {
message: {
content: string;
};
}[];
}
interface ParsedEstimate {
totalHours?: number;
complexity?: string;
breakdown?: RoleEstimate[];
recommendations?: string[];
}
@Injectable()
export class AiService {
private readonly logger = new Logger(AiService.name);
@ -103,7 +118,9 @@ export class AiService {
};
}
async getSpecificationHistory(ideaId: string): Promise<SpecificationHistoryItem[]> {
async getSpecificationHistory(
ideaId: string,
): Promise<SpecificationHistoryItem[]> {
const history = await this.specificationHistoryRepository.find({
where: { ideaId },
order: { createdAt: 'DESC' },
@ -120,18 +137,26 @@ export class AiService {
async deleteSpecificationHistoryItem(historyId: string): Promise<void> {
const result = await this.specificationHistoryRepository.delete(historyId);
if (result.affected === 0) {
throw new HttpException('Запись истории не найдена', HttpStatus.NOT_FOUND);
throw new HttpException(
'Запись истории не найдена',
HttpStatus.NOT_FOUND,
);
}
}
async restoreSpecificationFromHistory(historyId: string): Promise<SpecificationResult> {
async restoreSpecificationFromHistory(
historyId: string,
): Promise<SpecificationResult> {
const historyItem = await this.specificationHistoryRepository.findOne({
where: { id: historyId },
relations: ['idea'],
});
if (!historyItem) {
throw new HttpException('Запись истории не найдена', HttpStatus.NOT_FOUND);
throw new HttpException(
'Запись истории не найдена',
HttpStatus.NOT_FOUND,
);
}
const idea = historyItem.idea;
@ -194,14 +219,21 @@ export class AiService {
await this.ideaRepository.update(ideaId, {
estimatedHours: result.totalHours,
complexity: result.complexity,
estimateDetails: { breakdown: result.breakdown, recommendations: result.recommendations },
estimateDetails: {
breakdown: result.breakdown,
recommendations: result.recommendations,
},
estimatedAt: result.estimatedAt,
});
return result;
}
private buildPrompt(idea: Idea, teamMembers: TeamMember[], comments: Comment[]): string {
private buildPrompt(
idea: Idea,
teamMembers: TeamMember[],
comments: Comment[],
): string {
const teamInfo = teamMembers
.map((m) => {
const prod = m.productivity;
@ -211,12 +243,13 @@ export class AiService {
const rolesSummary = this.getRolesSummary(teamMembers);
const commentsSection = comments.length > 0
? `## Комментарии к идее
const commentsSection =
comments.length > 0
? `## Комментарии к идее
${comments.map((c, i) => `${i + 1}. ${c.author ? `**${c.author}:** ` : ''}${c.text}`).join('\n')}
`
: '';
: '';
return `Ты — эксперт по оценке трудозатрат разработки программного обеспечения.
@ -269,12 +302,13 @@ ${rolesSummary}
}
private buildSpecificationPrompt(idea: Idea, comments: Comment[]): string {
const commentsSection = comments.length > 0
? `## Комментарии к идее
const commentsSection =
comments.length > 0
? `## Комментарии к идее
${comments.map((c, i) => `${i + 1}. ${c.author ? `**${c.author}:** ` : ''}${c.text}`).join('\n')}
`
: '';
: '';
return `Ты — опытный бизнес-аналитик и технический писатель.
@ -345,13 +379,14 @@ ${commentsSection}## Требования к ТЗ
);
}
const data = await response.json();
const data = (await response.json()) as AiProxyResponse;
return data.choices[0].message.content;
} catch (error) {
} catch (error: unknown) {
if (error instanceof HttpException) {
throw error;
}
this.logger.error(`AI Proxy call failed: ${error.message}`);
const message = error instanceof Error ? error.message : 'Unknown error';
this.logger.error(`AI Proxy call failed: ${message}`);
throw new HttpException(
'Не удалось подключиться к AI сервису',
HttpStatus.SERVICE_UNAVAILABLE,
@ -374,20 +409,33 @@ ${commentsSection}## Требования к ТЗ
}
cleanJson = cleanJson.trim();
const parsed = JSON.parse(cleanJson);
const parsed = JSON.parse(cleanJson) as ParsedEstimate;
const validComplexities = [
'trivial',
'simple',
'medium',
'complex',
'veryComplex',
] as const;
const complexity = validComplexities.includes(
parsed.complexity as (typeof validComplexities)[number],
)
? (parsed.complexity as EstimateResult['complexity'])
: 'medium';
return {
ideaId: idea.id,
ideaTitle: idea.title,
totalHours: Number(parsed.totalHours) || 0,
complexity: parsed.complexity || 'medium',
complexity,
breakdown: Array.isArray(parsed.breakdown) ? parsed.breakdown : [],
recommendations: Array.isArray(parsed.recommendations)
? parsed.recommendations
: [],
estimatedAt: new Date(),
};
} catch (error) {
} catch {
this.logger.error(`Failed to parse AI response: ${aiResponse}`);
throw new HttpException(
'Не удалось разобрать ответ AI',

View File

@ -18,7 +18,10 @@ export class CommentsService {
});
}
async create(ideaId: string, createCommentDto: CreateCommentDto): Promise<Comment> {
async create(
ideaId: string,
createCommentDto: CreateCommentDto,
): Promise<Comment> {
const comment = this.commentsRepository.create({
...createCommentDto,
ideaId,

View File

@ -73,7 +73,13 @@ export class Idea {
order: number;
// AI-оценка
@Column({ name: 'estimated_hours', type: 'decimal', precision: 10, scale: 2, nullable: true })
@Column({
name: 'estimated_hours',
type: 'decimal',
precision: 10,
scale: 2,
nullable: true,
})
estimatedHours: number | null;
@Column({ type: 'varchar', length: 20, nullable: true })
@ -89,7 +95,11 @@ export class Idea {
@Column({ type: 'text', nullable: true })
specification: string | null;
@Column({ name: 'specification_generated_at', type: 'timestamp', nullable: true })
@Column({
name: 'specification_generated_at',
type: 'timestamp',
nullable: true,
})
specificationGeneratedAt: Date | null;
@CreateDateColumn({ name: 'created_at' })

View File

@ -16,7 +16,9 @@ export class CreateCommentsTable1736899200000 implements MigrationInterface {
CONSTRAINT "FK_comments_idea_id" FOREIGN KEY ("idea_id") REFERENCES "ideas"("id") ON DELETE CASCADE
)
`);
await queryRunner.query(`CREATE INDEX "IDX_comments_idea_id" ON "comments" ("idea_id")`);
await queryRunner.query(
`CREATE INDEX "IDX_comments_idea_id" ON "comments" ("idea_id")`,
);
}
public async down(queryRunner: QueryRunner): Promise<void> {

View File

@ -84,7 +84,9 @@ export class CreateRolesTable1736899400000 implements MigrationInterface {
`);
// 5. Удаляем foreign key и role_id
await queryRunner.query(`ALTER TABLE "team_members" DROP CONSTRAINT "FK_team_members_role"`);
await queryRunner.query(
`ALTER TABLE "team_members" DROP CONSTRAINT "FK_team_members_role"`,
);
await queryRunner.query(`ALTER TABLE "team_members" DROP COLUMN "role_id"`);
// 6. Удаляем таблицу roles

View File

@ -11,10 +11,10 @@ import { Role } from './role.entity';
// Матрица производительности: время в часах на задачи разной сложности
export interface ProductivityMatrix {
trivial: number; // < 1 часа
simple: number; // 1-4 часа
medium: number; // 4-16 часов
complex: number; // 16-40 часов
trivial: number; // < 1 часа
simple: number; // 1-4 часа
medium: number; // 4-16 часов
complex: number; // 16-40 часов
veryComplex: number; // > 40 часов
}
@ -33,7 +33,16 @@ export class TeamMember {
@Column({ name: 'role_id', type: 'uuid' })
roleId: string;
@Column({ type: 'jsonb', default: { trivial: 1, simple: 4, medium: 12, complex: 32, veryComplex: 60 } })
@Column({
type: 'jsonb',
default: {
trivial: 1,
simple: 4,
medium: 12,
complex: 32,
veryComplex: 60,
},
})
productivity: ProductivityMatrix;
@CreateDateColumn({ name: 'created_at' })

View File

@ -1,4 +1,8 @@
import { Injectable, NotFoundException, ConflictException } from '@nestjs/common';
import {
Injectable,
NotFoundException,
ConflictException,
} from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { Role } from './entities/role.entity';
@ -31,7 +35,9 @@ export class RolesService {
where: { name: createDto.name },
});
if (existing) {
throw new ConflictException(`Role with name "${createDto.name}" already exists`);
throw new ConflictException(
`Role with name "${createDto.name}" already exists`,
);
}
const maxSortOrder = await this.roleRepository
@ -54,7 +60,9 @@ export class RolesService {
where: { name: updateDto.name },
});
if (existing) {
throw new ConflictException(`Role with name "${updateDto.name}" already exists`);
throw new ConflictException(
`Role with name "${updateDto.name}" already exists`,
);
}
}

View File

@ -39,7 +39,9 @@ export class TeamService {
where: { id: createDto.roleId },
});
if (!role) {
throw new NotFoundException(`Role with ID "${createDto.roleId}" not found`);
throw new NotFoundException(
`Role with ID "${createDto.roleId}" not found`,
);
}
const member = this.teamRepository.create(createDto);
@ -47,7 +49,10 @@ export class TeamService {
return this.findOne(saved.id);
}
async update(id: string, updateDto: UpdateTeamMemberDto): Promise<TeamMember> {
async update(
id: string,
updateDto: UpdateTeamMemberDto,
): Promise<TeamMember> {
const member = await this.findOne(id);
if (updateDto.roleId) {
@ -55,7 +60,9 @@ export class TeamService {
where: { id: updateDto.roleId },
});
if (!role) {
throw new NotFoundException(`Role with ID "${updateDto.roleId}" not found`);
throw new NotFoundException(
`Role with ID "${updateDto.roleId}" not found`,
);
}
}
@ -69,7 +76,9 @@ export class TeamService {
await this.teamRepository.remove(member);
}
async getSummary(): Promise<{ roleId: string; label: string; count: number }[]> {
async getSummary(): Promise<
{ roleId: string; label: string; count: number }[]
> {
// Получаем все роли
const roles = await this.roleRepository.find({
order: { sortOrder: 'ASC' },
@ -87,7 +96,10 @@ export class TeamService {
return roles.map((role) => ({
roleId: role.id,
label: role.label,
count: parseInt(result.find((r) => r.roleId === role.id)?.count ?? '0', 10),
count: parseInt(
result.find((r) => r.roleId === role.id)?.count ?? '0',
10,
),
}));
}
}