paginate app operations and refine runtime settings layout
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/push/woodpecker Pipeline was successful
This commit is contained in:
@@ -13,6 +13,7 @@ import { LauncherModule } from './launcher/launcher.module';
|
||||
import { LicensesModule } from './licenses/licenses.module';
|
||||
import { NoticesModule } from './notices/notices.module';
|
||||
import { OrganizationsModule } from './organizations/organizations.module';
|
||||
import { PageAgentModule } from './page-agent/page-agent.module';
|
||||
import { PrismaModule } from './prisma/prisma.module';
|
||||
import { RuntimeModule } from './runtime/runtime.module';
|
||||
import { UsersModule } from './users/users.module';
|
||||
@@ -39,6 +40,7 @@ import { WoodpeckerModule } from './woodpecker/woodpecker.module';
|
||||
LicensesModule,
|
||||
NoticesModule,
|
||||
OrganizationsModule,
|
||||
PageAgentModule,
|
||||
PrismaModule,
|
||||
RuntimeModule,
|
||||
UsersModule,
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
CreateAppReleaseDto,
|
||||
CreateBuildJobDto,
|
||||
CreateDeploymentJobDto,
|
||||
DecommissionAppDto,
|
||||
CreateMigrationJobDto,
|
||||
CreateRuntimeEnvProfileDto,
|
||||
CreateRuntimeEnvVariableDto,
|
||||
@@ -35,6 +36,14 @@ export class AppsController {
|
||||
return this.appsService.deleteApp(appId);
|
||||
}
|
||||
|
||||
@Post(':appId/decommission')
|
||||
decommissionApp(
|
||||
@Param('appId') appId: string,
|
||||
@Body() body: DecommissionAppDto,
|
||||
) {
|
||||
return this.appsService.decommissionApp(appId, body);
|
||||
}
|
||||
|
||||
@Post(':appId/components')
|
||||
createComponent(
|
||||
@Param('appId') appId: string,
|
||||
@@ -62,8 +71,14 @@ export class AppsController {
|
||||
}
|
||||
|
||||
@Get('builds')
|
||||
listBuildJobs(@Query('appKey') appKey?: string) {
|
||||
return this.appsService.listBuildJobs(appKey);
|
||||
listBuildJobs(
|
||||
@Query('appKey') appKey?: string,
|
||||
@Query('page') page?: string,
|
||||
@Query('limit') limit?: string,
|
||||
@Query('since') since?: string,
|
||||
@Query('status') status?: string,
|
||||
) {
|
||||
return this.appsService.listBuildJobs(appKey, page, limit, since, status);
|
||||
}
|
||||
|
||||
@Post('builds')
|
||||
@@ -103,8 +118,13 @@ export class AppsController {
|
||||
}
|
||||
|
||||
@Get('releases')
|
||||
listReleases(@Query('appKey') appKey?: string) {
|
||||
return this.appsService.listReleases(appKey);
|
||||
listReleases(
|
||||
@Query('appKey') appKey?: string,
|
||||
@Query('page') page?: string,
|
||||
@Query('limit') limit?: string,
|
||||
@Query('since') since?: string,
|
||||
) {
|
||||
return this.appsService.listReleases(appKey, page, limit, since);
|
||||
}
|
||||
|
||||
@Post('releases')
|
||||
@@ -113,8 +133,13 @@ export class AppsController {
|
||||
}
|
||||
|
||||
@Get('deployments')
|
||||
listDeployments(@Query('appKey') appKey?: string) {
|
||||
return this.appsService.listDeployments(appKey);
|
||||
listDeployments(
|
||||
@Query('appKey') appKey?: string,
|
||||
@Query('page') page?: string,
|
||||
@Query('limit') limit?: string,
|
||||
@Query('since') since?: string,
|
||||
) {
|
||||
return this.appsService.listDeployments(appKey, page, limit, since);
|
||||
}
|
||||
|
||||
@Post('deployments')
|
||||
@@ -123,8 +148,13 @@ export class AppsController {
|
||||
}
|
||||
|
||||
@Get('migrations')
|
||||
listMigrationJobs() {
|
||||
return this.appsService.listMigrationJobs();
|
||||
listMigrationJobs(
|
||||
@Query('appId') appId?: string,
|
||||
@Query('page') page?: string,
|
||||
@Query('limit') limit?: string,
|
||||
@Query('since') since?: string,
|
||||
) {
|
||||
return this.appsService.listMigrationJobs(appId, page, limit, since);
|
||||
}
|
||||
|
||||
@Post('migrations')
|
||||
|
||||
+366
-12
@@ -17,6 +17,7 @@ import {
|
||||
CreateAppReleaseDto,
|
||||
CreateBuildJobDto,
|
||||
CreateDeploymentJobDto,
|
||||
DecommissionAppDto,
|
||||
CreateMigrationJobDto,
|
||||
CreateRuntimeEnvProfileDto,
|
||||
CreateRuntimeEnvVariableDto,
|
||||
@@ -78,6 +79,15 @@ export class AppsService {
|
||||
}
|
||||
|
||||
async deleteApp(appId: string) {
|
||||
return this.decommissionApp(appId, {
|
||||
unlinkSources: true,
|
||||
scaleDownWorkloads: false,
|
||||
deleteKubernetesResources: false,
|
||||
deletePersistentVolumes: false,
|
||||
});
|
||||
}
|
||||
|
||||
async decommissionApp(appId: string, body: DecommissionAppDto = {}) {
|
||||
const app = await this.prisma.app.findUnique({
|
||||
where: { id: appId },
|
||||
include: {
|
||||
@@ -90,12 +100,46 @@ export class AppsService {
|
||||
|
||||
if (!app) throw new NotFoundException('앱을 찾을 수 없습니다.');
|
||||
|
||||
const logs: string[] = [`[app] decommission ${app.name}`];
|
||||
const namespace = app.namespace ? normalizeNamespace(app.namespace) : '';
|
||||
|
||||
if (namespace && (body.scaleDownWorkloads || body.deleteKubernetesResources || body.deletePersistentVolumes)) {
|
||||
const exists = await this.namespaceExists(namespace);
|
||||
if (!exists) {
|
||||
logs.push(`[skip] namespace ${namespace} not found`);
|
||||
} else {
|
||||
if (body.scaleDownWorkloads) {
|
||||
await this.scaleDownAppWorkloads(namespace, app.components, logs);
|
||||
}
|
||||
if (body.deleteKubernetesResources) {
|
||||
await this.deleteAppKubernetesResources(namespace, app.components, logs);
|
||||
}
|
||||
if (body.deletePersistentVolumes) {
|
||||
await this.deleteAppPersistentVolumeClaims(namespace, app.components, logs);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (body.unlinkSources) {
|
||||
const result = await this.prisma.appComponent.updateMany({
|
||||
where: { appId },
|
||||
data: {
|
||||
repository: null,
|
||||
branch: null,
|
||||
buildTarget: null,
|
||||
},
|
||||
});
|
||||
logs.push(`[db] unlinked ${result.count} component source settings`);
|
||||
}
|
||||
|
||||
await this.prisma.app.delete({ where: { id: appId } });
|
||||
logs.push('[db] app deleted');
|
||||
|
||||
return {
|
||||
deleted: true,
|
||||
appId,
|
||||
namespace: app.namespace,
|
||||
logs: logs.filter(Boolean).join('\n'),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -250,7 +294,15 @@ export class AppsService {
|
||||
return this.listComponentBuildEnv(componentId);
|
||||
}
|
||||
|
||||
listReleases(appKey?: string) {
|
||||
async listReleases(appKey?: string, page?: string, limit?: string, since?: string) {
|
||||
const pagination = parsePagination(page, limit);
|
||||
const sinceDate = parseSinceDate(since);
|
||||
const where: Prisma.AppReleaseWhereInput = {
|
||||
...(appKey ? { app: { key: appKey } } : {}),
|
||||
...(sinceDate ? { createdAt: { gte: sinceDate } } : {}),
|
||||
};
|
||||
|
||||
if (!pagination && !sinceDate) {
|
||||
return this.prisma.appRelease.findMany({
|
||||
where: appKey ? { app: { key: appKey } } : undefined,
|
||||
orderBy: { createdAt: 'desc' },
|
||||
@@ -258,6 +310,36 @@ export class AppsService {
|
||||
});
|
||||
}
|
||||
|
||||
const pageNumber = pagination?.page ?? 1;
|
||||
const pageLimit = pagination?.limit ?? 20;
|
||||
const [items, total] = await this.prisma.$transaction([
|
||||
this.prisma.appRelease.findMany({
|
||||
where,
|
||||
orderBy: { createdAt: 'desc' },
|
||||
skip: (pageNumber - 1) * pageLimit,
|
||||
take: pageLimit,
|
||||
include: { app: true },
|
||||
}),
|
||||
this.prisma.appRelease.count({ where }),
|
||||
]);
|
||||
const usedReleases = await this.prisma.appRelease.findMany({
|
||||
where: appKey ? { app: { key: appKey } } : undefined,
|
||||
select: {
|
||||
artifact: true,
|
||||
imageTags: true,
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
releases: items,
|
||||
total,
|
||||
page: pageNumber,
|
||||
limit: pageLimit,
|
||||
usedBuildIds: getUsedReleaseBuildIds(usedReleases),
|
||||
usedValues: getUsedReleaseValues(usedReleases),
|
||||
};
|
||||
}
|
||||
|
||||
async createRelease(body: CreateAppReleaseDto) {
|
||||
const app = await this.prisma.app.findUnique({
|
||||
where: { id: body.appId },
|
||||
@@ -666,17 +748,58 @@ export class AppsService {
|
||||
return data.login;
|
||||
}
|
||||
|
||||
listDeployments(appKey?: string) {
|
||||
async listDeployments(appKey?: string, page?: string, limit?: string, since?: string) {
|
||||
const pagination = parsePagination(page, limit);
|
||||
const sinceDate = parseSinceDate(since);
|
||||
const where: Prisma.DeploymentJobWhereInput = {
|
||||
...(appKey ? { app: { key: appKey } } : {}),
|
||||
...(sinceDate ? { createdAt: { gte: sinceDate } } : {}),
|
||||
};
|
||||
const include = {
|
||||
app: true,
|
||||
release: true,
|
||||
};
|
||||
|
||||
if (!pagination && !sinceDate) {
|
||||
return this.prisma.deploymentJob.findMany({
|
||||
where: appKey ? { app: { key: appKey } } : undefined,
|
||||
orderBy: { createdAt: 'desc' },
|
||||
include: {
|
||||
app: true,
|
||||
release: true,
|
||||
},
|
||||
include,
|
||||
});
|
||||
}
|
||||
|
||||
const pageNumber = pagination?.page ?? 1;
|
||||
const pageLimit = pagination?.limit ?? 20;
|
||||
const [items, total] = await this.prisma.$transaction([
|
||||
this.prisma.deploymentJob.findMany({
|
||||
where,
|
||||
orderBy: { createdAt: 'desc' },
|
||||
skip: (pageNumber - 1) * pageLimit,
|
||||
take: pageLimit,
|
||||
include,
|
||||
}),
|
||||
this.prisma.deploymentJob.count({ where }),
|
||||
]);
|
||||
const deployedJobs = await this.prisma.deploymentJob.findMany({
|
||||
where: {
|
||||
...(appKey ? { app: { key: appKey } } : {}),
|
||||
status: 'SUCCEEDED',
|
||||
releaseId: { not: null },
|
||||
},
|
||||
select: { releaseId: true },
|
||||
});
|
||||
|
||||
return {
|
||||
deployments: items,
|
||||
total,
|
||||
page: pageNumber,
|
||||
limit: pageLimit,
|
||||
deployedReleaseIds: deployedJobs
|
||||
.map((job) => job.releaseId)
|
||||
.filter((releaseId): releaseId is string => Boolean(releaseId)),
|
||||
};
|
||||
}
|
||||
|
||||
async createDeploymentJob(body: CreateDeploymentJobDto) {
|
||||
const namespace = normalizeNamespace(body.namespace);
|
||||
const environment = body.environment.trim();
|
||||
@@ -907,13 +1030,42 @@ export class AppsService {
|
||||
}
|
||||
}
|
||||
|
||||
listMigrationJobs() {
|
||||
async listMigrationJobs(appId?: string, page?: string, limit?: string, since?: string) {
|
||||
const pagination = parsePagination(page, limit);
|
||||
const sinceDate = parseSinceDate(since);
|
||||
const where: Prisma.RuntimeControlJobWhereInput = {
|
||||
action: 'MIGRATION',
|
||||
...(appId ? { request: { path: ['appId'], equals: appId } } : {}),
|
||||
...(sinceDate ? { createdAt: { gte: sinceDate } } : {}),
|
||||
};
|
||||
|
||||
if (!pagination && !sinceDate && !appId) {
|
||||
return this.prisma.runtimeControlJob.findMany({
|
||||
where: { action: 'MIGRATION' },
|
||||
orderBy: { createdAt: 'desc' },
|
||||
});
|
||||
}
|
||||
|
||||
const pageNumber = pagination?.page ?? 1;
|
||||
const pageLimit = pagination?.limit ?? 20;
|
||||
const [items, total] = await this.prisma.$transaction([
|
||||
this.prisma.runtimeControlJob.findMany({
|
||||
where,
|
||||
orderBy: { createdAt: 'desc' },
|
||||
skip: (pageNumber - 1) * pageLimit,
|
||||
take: pageLimit,
|
||||
}),
|
||||
this.prisma.runtimeControlJob.count({ where }),
|
||||
]);
|
||||
|
||||
return {
|
||||
jobs: items,
|
||||
total,
|
||||
page: pageNumber,
|
||||
limit: pageLimit,
|
||||
};
|
||||
}
|
||||
|
||||
async createMigrationJob(body: CreateMigrationJobDto) {
|
||||
const namespace = normalizeNamespace(body.namespace);
|
||||
const command = body.command.trim();
|
||||
@@ -1043,20 +1195,54 @@ export class AppsService {
|
||||
}
|
||||
}
|
||||
|
||||
listBuildJobs(appKey?: string) {
|
||||
return this.prisma.buildJob.findMany({
|
||||
where: appKey ? { component: { app: { key: appKey } } } : undefined,
|
||||
orderBy: { createdAt: 'desc' },
|
||||
include: {
|
||||
async listBuildJobs(appKey?: string, page?: string, limit?: string, since?: string, status?: string) {
|
||||
const pagination = parsePagination(page, limit);
|
||||
const sinceDate = parseSinceDate(since);
|
||||
const where: Prisma.BuildJobWhereInput = {
|
||||
...(appKey ? { component: { app: { key: appKey } } } : {}),
|
||||
...(sinceDate ? { createdAt: { gte: sinceDate } } : {}),
|
||||
...(status ? { status: normalizeJobStatus(status) } : {}),
|
||||
};
|
||||
const include = {
|
||||
component: {
|
||||
include: {
|
||||
app: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
if (!pagination && !sinceDate) {
|
||||
return this.prisma.buildJob.findMany({
|
||||
where: {
|
||||
...(appKey ? { component: { app: { key: appKey } } } : {}),
|
||||
...(status ? { status: normalizeJobStatus(status) } : {}),
|
||||
},
|
||||
orderBy: { createdAt: 'desc' },
|
||||
include,
|
||||
});
|
||||
}
|
||||
|
||||
const pageNumber = pagination?.page ?? 1;
|
||||
const pageLimit = pagination?.limit ?? 20;
|
||||
const [items, total] = await this.prisma.$transaction([
|
||||
this.prisma.buildJob.findMany({
|
||||
where,
|
||||
orderBy: { createdAt: 'desc' },
|
||||
skip: (pageNumber - 1) * pageLimit,
|
||||
take: pageLimit,
|
||||
include,
|
||||
}),
|
||||
this.prisma.buildJob.count({ where }),
|
||||
]);
|
||||
|
||||
return {
|
||||
jobs: items,
|
||||
total,
|
||||
page: pageNumber,
|
||||
limit: pageLimit,
|
||||
};
|
||||
}
|
||||
|
||||
listRuntimeProfiles(appId?: string) {
|
||||
return this.prisma.runtimeEnvProfile.findMany({
|
||||
where: appId ? { appId } : undefined,
|
||||
@@ -1716,6 +1902,121 @@ export class AppsService {
|
||||
}
|
||||
}
|
||||
|
||||
private async scaleDownAppWorkloads(
|
||||
namespace: string,
|
||||
components: Array<{ name: string; type: string; workloadType?: string | null }>,
|
||||
logs: string[],
|
||||
) {
|
||||
for (const component of components) {
|
||||
const name = sanitizeK8sName(component.name);
|
||||
if (!name) continue;
|
||||
|
||||
const workloadType = normalizeWorkloadType(component.workloadType, component.type);
|
||||
if (workloadType === 'Job') continue;
|
||||
const resource = workloadType === 'StatefulSet' ? 'statefulset' : 'deployment';
|
||||
|
||||
await this.tryKubectl(logs, `[scale] ${resource}/${name} replicas=0`, [
|
||||
'scale',
|
||||
`${resource}/${name}`,
|
||||
'--replicas=0',
|
||||
'-n',
|
||||
namespace,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
private async deleteAppKubernetesResources(
|
||||
namespace: string,
|
||||
components: Array<{ name: string; type: string; workloadType?: string | null }>,
|
||||
logs: string[],
|
||||
) {
|
||||
for (const component of components) {
|
||||
const name = sanitizeK8sName(component.name);
|
||||
if (!name) continue;
|
||||
|
||||
const workloadType = normalizeWorkloadType(component.workloadType, component.type);
|
||||
if (workloadType !== 'Job') {
|
||||
const workload = workloadType === 'StatefulSet' ? 'statefulset' : 'deployment';
|
||||
await this.tryKubectl(logs, `[delete] ${workload}/${name}`, [
|
||||
'delete',
|
||||
workload,
|
||||
name,
|
||||
'-n',
|
||||
namespace,
|
||||
'--ignore-not-found=true',
|
||||
]);
|
||||
}
|
||||
|
||||
for (const resource of ['service', 'ingress', 'configmap', 'secret']) {
|
||||
await this.tryKubectl(logs, `[delete] ${resource}/${name}`, [
|
||||
'delete',
|
||||
resource,
|
||||
name,
|
||||
'-n',
|
||||
namespace,
|
||||
'--ignore-not-found=true',
|
||||
]);
|
||||
}
|
||||
|
||||
await this.tryKubectl(logs, `[delete] configmap/${name}-env`, [
|
||||
'delete',
|
||||
'configmap',
|
||||
`${name}-env`,
|
||||
'-n',
|
||||
namespace,
|
||||
'--ignore-not-found=true',
|
||||
]);
|
||||
await this.tryKubectl(logs, `[delete] secret/${name}-secret`, [
|
||||
'delete',
|
||||
'secret',
|
||||
`${name}-secret`,
|
||||
'-n',
|
||||
namespace,
|
||||
'--ignore-not-found=true',
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
private async deleteAppPersistentVolumeClaims(
|
||||
namespace: string,
|
||||
components: Array<{ name: string }>,
|
||||
logs: string[],
|
||||
) {
|
||||
const output = await this.kubectl(['get', 'pvc', '-n', namespace, '-o', 'json']);
|
||||
const parsed = JSON.parse(output) as { items?: Array<{ metadata?: { name?: string } }> };
|
||||
const names = new Set(components.map((component) => sanitizeK8sName(component.name)).filter(Boolean));
|
||||
const claims = (parsed.items ?? [])
|
||||
.map((item) => item.metadata?.name ?? '')
|
||||
.filter((claimName) => Array.from(names).some((name) => claimName === name || claimName.startsWith(`data-${name}-`) || claimName.includes(`-${name}-`)));
|
||||
|
||||
if (claims.length === 0) {
|
||||
logs.push('[pvc] matched persistent volume claims not found');
|
||||
return;
|
||||
}
|
||||
|
||||
for (const claim of claims) {
|
||||
await this.tryKubectl(logs, `[delete] pvc/${claim}`, [
|
||||
'delete',
|
||||
'pvc',
|
||||
claim,
|
||||
'-n',
|
||||
namespace,
|
||||
'--ignore-not-found=true',
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
private async tryKubectl(logs: string[], label: string, args: string[]) {
|
||||
logs.push(label);
|
||||
try {
|
||||
const output = await this.kubectl(args);
|
||||
if (output.trim()) appendOutput(logs, output);
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : 'kubectl failed';
|
||||
logs.push(`[warn] ${label} failed: ${message}`);
|
||||
}
|
||||
}
|
||||
|
||||
private async applyDatabaseResources(input: {
|
||||
namespace: string;
|
||||
appKey: string;
|
||||
@@ -2675,6 +2976,59 @@ function parseReleaseImageTags(value: Prisma.JsonValue | null) {
|
||||
return { images };
|
||||
}
|
||||
|
||||
function getUsedReleaseBuildIds(releases: Array<{ imageTags: Prisma.JsonValue | null }>) {
|
||||
const ids = new Set<string>();
|
||||
for (const release of releases) {
|
||||
const value = release.imageTags;
|
||||
if (!value || typeof value !== 'object' || Array.isArray(value)) continue;
|
||||
const promotion = (value as Record<string, unknown>).promotion;
|
||||
if (!promotion || typeof promotion !== 'object' || Array.isArray(promotion)) continue;
|
||||
const sourceBuildJobIds = (promotion as Record<string, unknown>).sourceBuildJobIds;
|
||||
if (!Array.isArray(sourceBuildJobIds)) continue;
|
||||
for (const id of sourceBuildJobIds) {
|
||||
if (typeof id === 'string') ids.add(id);
|
||||
}
|
||||
}
|
||||
return [...ids];
|
||||
}
|
||||
|
||||
function getUsedReleaseValues(releases: Array<{ artifact: string | null; imageTags: Prisma.JsonValue | null }>) {
|
||||
const values = new Set<string>();
|
||||
for (const release of releases) {
|
||||
if (release.artifact) values.add(release.artifact);
|
||||
const parsed = parseReleaseImageTags(release.imageTags);
|
||||
for (const image of Object.values(parsed.images)) {
|
||||
if (image) values.add(image);
|
||||
}
|
||||
if (release.imageTags && typeof release.imageTags === 'object' && !Array.isArray(release.imageTags)) {
|
||||
const artifacts = (release.imageTags as Record<string, unknown>).artifacts;
|
||||
if (artifacts && typeof artifacts === 'object' && !Array.isArray(artifacts)) {
|
||||
for (const artifact of Object.values(artifacts)) {
|
||||
if (typeof artifact === 'string' && artifact) values.add(artifact);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return [...values];
|
||||
}
|
||||
|
||||
function normalizeJobStatus(status: string) {
|
||||
return status.trim().toUpperCase() as Prisma.EnumJobStatusFilter['equals'];
|
||||
}
|
||||
|
||||
function parsePagination(page?: string, limit?: string) {
|
||||
if (!page && !limit) return null;
|
||||
const pageNumber = Math.max(1, Number.parseInt(page || '1', 10) || 1);
|
||||
const pageLimit = Math.min(100, Math.max(1, Number.parseInt(limit || '20', 10) || 20));
|
||||
return { page: pageNumber, limit: pageLimit };
|
||||
}
|
||||
|
||||
function parseSinceDate(value?: string) {
|
||||
if (!value) return null;
|
||||
const date = new Date(value);
|
||||
return Number.isNaN(date.getTime()) ? null : date;
|
||||
}
|
||||
|
||||
|
||||
async function gitRefExists(workDir: string, ref: string, gitBin = 'git') {
|
||||
try {
|
||||
|
||||
@@ -21,6 +21,28 @@ export class CreateAppDto {
|
||||
status?: AppStatus;
|
||||
}
|
||||
|
||||
export class DecommissionAppDto {
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
unlinkSources?: boolean;
|
||||
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
deleteRuntimeSettings?: boolean;
|
||||
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
scaleDownWorkloads?: boolean;
|
||||
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
deleteKubernetesResources?: boolean;
|
||||
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
deletePersistentVolumes?: boolean;
|
||||
}
|
||||
|
||||
export class CreateAppComponentDto {
|
||||
@IsString()
|
||||
name!: string;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { RequestMethod, ValidationPipe } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { NestFactory } from '@nestjs/core';
|
||||
import { json } from 'express';
|
||||
|
||||
import { AppModule } from './app.module';
|
||||
|
||||
@@ -13,6 +14,7 @@ async function bootstrap() {
|
||||
'http://localhost:5173,http://localhost:5174',
|
||||
);
|
||||
|
||||
app.use(json({ limit: '2mb' }));
|
||||
app.setGlobalPrefix('api', {
|
||||
exclude: [
|
||||
{ path: 'health', method: RequestMethod.GET },
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
import { Body, Controller, Post } from '@nestjs/common';
|
||||
|
||||
import { PageAgentService } from './page-agent.service';
|
||||
|
||||
@Controller('page-agent/llm')
|
||||
export class PageAgentController {
|
||||
constructor(private readonly pageAgentService: PageAgentService) {}
|
||||
|
||||
@Post()
|
||||
createCompletion(@Body() body: Record<string, unknown>) {
|
||||
return this.pageAgentService.createChatCompletion(body);
|
||||
}
|
||||
|
||||
@Post('chat/completions')
|
||||
createChatCompletion(@Body() body: Record<string, unknown>) {
|
||||
return this.pageAgentService.createChatCompletion(body);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
|
||||
import { PageAgentController } from './page-agent.controller';
|
||||
import { PageAgentService } from './page-agent.service';
|
||||
|
||||
@Module({
|
||||
controllers: [PageAgentController],
|
||||
providers: [PageAgentService],
|
||||
})
|
||||
export class PageAgentModule {}
|
||||
@@ -0,0 +1,40 @@
|
||||
import { BadGatewayException, Injectable, InternalServerErrorException } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
|
||||
@Injectable()
|
||||
export class PageAgentService {
|
||||
constructor(private readonly configService: ConfigService) {}
|
||||
|
||||
async createChatCompletion(body: Record<string, unknown>) {
|
||||
const apiKey = this.configService.get<string>('OPENAI_API_KEY');
|
||||
if (!apiKey) {
|
||||
throw new InternalServerErrorException('OPENAI_API_KEY가 설정되어 있지 않습니다.');
|
||||
}
|
||||
|
||||
const requestBody = {
|
||||
...body,
|
||||
model: typeof body.model === 'string' && body.model ? body.model : 'gpt-4.1-mini',
|
||||
temperature: body.temperature ?? 0,
|
||||
};
|
||||
|
||||
const response = await fetch('https://api.openai.com/v1/chat/completions', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `Bearer ${apiKey}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(requestBody),
|
||||
});
|
||||
|
||||
const result = await response.json().catch(() => null);
|
||||
if (!response.ok) {
|
||||
throw new BadGatewayException({
|
||||
error: 'LLM request failed',
|
||||
status: response.status,
|
||||
detail: result,
|
||||
});
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user