feat(ai-image): AI生图开发

This commit is contained in:
2026-09-27 22:40:52 +08:00
parent e670d06ca6
commit a29dd92676
105 changed files with 9943 additions and 361 deletions
+139 -2
View File
@@ -6,7 +6,9 @@ import {
Get,
Param,
Post,
Query,
Req,
Res,
} from '@nestjs/common';
import {
ApiBearerAuth,
@@ -19,6 +21,11 @@ import {
createMediaAccessUrlRequestSchema,
createMediaUploadIntentRequestSchema,
importProductImageRequestSchema,
createAspectRatioDerivativeRequestSchema,
createWatermarkDerivativeRequestSchema,
createMediaAiEditRequestSchema,
listProductImageTasksQuerySchema,
type CreateMediaAccessUrlRequest,
type Workspace,
} from '@atlas/contracts';
import { z } from 'zod';
@@ -26,6 +33,11 @@ import { ApiError } from '../common/api-error.js';
import type { AuthenticatedRequest } from '../auth/auth.guard.js';
import { CurrentWorkspace } from '../workspace/current-workspace.decorator.js';
import { MediaService } from './media.service.js';
import { AspectRatioDerivativeService } from './aspect-ratio-derivative.service.js';
import { WatermarkDerivativeService } from './watermark-derivative.service.js';
import { MediaAiEditService } from './media-ai-edit.service.js';
import { ProductImageTaskService } from './product-image-task.service.js';
import { ProductImageDerivativeHistoryService } from './product-image-derivative-history.service.js';
@ApiTags('Media')
@ApiBearerAuth('access-token')
@@ -33,8 +45,132 @@ import { MediaService } from './media.service.js';
export class MediaController {
public constructor(
@Inject(MediaService) private readonly media: MediaService,
@Inject(AspectRatioDerivativeService) private readonly derivatives: AspectRatioDerivativeService,
@Inject(WatermarkDerivativeService) private readonly watermarkDerivatives: WatermarkDerivativeService,
@Inject(MediaAiEditService) private readonly aiEdits: MediaAiEditService,
@Inject(ProductImageTaskService) private readonly productImageTasks: ProductImageTaskService,
@Inject(ProductImageDerivativeHistoryService) private readonly derivativeHistory: ProductImageDerivativeHistoryService,
) {}
@Get('products/:productId/image-derivatives')
public async listProductImageDerivatives(
@Param('productId') productId: string,
@CurrentWorkspace() workspace: Workspace,
) {
return { data: await this.derivativeHistory.list(workspace.id, uuid('product_id', productId)) };
}
@Get('products/:productId/image-tasks')
public async listProductImageTasks(
@Param('productId') productId: string,
@CurrentWorkspace() workspace: Workspace,
@Query() query: unknown,
) {
return this.productImageTasks.list(workspace.id, uuid('product_id', productId),
parse(listProductImageTasksQuerySchema, query));
}
@Post('media-assets/:assetId/ai-edits')
public async createAiEdit(
@Param('assetId') assetId: string,
@Body() body: unknown,
@Headers('idempotency-key') key: string | undefined,
@Req() request: AuthenticatedRequest,
@CurrentWorkspace() workspace: Workspace,
@Res({ passthrough: true }) response: { status: (code: number) => void },
) {
const result = await this.aiEdits.create(
workspace.id, request.auth.userId, uuid('asset_id', assetId),
idempotencyKey(key), parse(createMediaAiEditRequestSchema, body),
);
response.status(result.status);
return result.body;
}
@Get('media-ai-edits/:editId')
public async aiEdit(
@Param('editId') editId: string,
@CurrentWorkspace() workspace: Workspace,
) {
return { data: await this.aiEdits.read(workspace.id, uuid('edit_id', editId)) };
}
@Get('media-ai-edits')
public async listAiEdits(
@Query('product_id') productId: string,
@CurrentWorkspace() workspace: Workspace,
) {
return { data: await this.aiEdits.list(workspace.id, uuid('product_id', productId)) };
}
@Post('media-ai-edits/:editId/cancel')
public async cancelAiEdit(
@Param('editId') editId: string,
@Headers('idempotency-key') key: string | undefined,
@Req() request: AuthenticatedRequest,
@CurrentWorkspace() workspace: Workspace,
) {
return (await this.aiEdits.cancel(
workspace.id, request.auth.userId, uuid('edit_id', editId), idempotencyKey(key),
)).body;
}
@Post('media-assets/:assetId/watermark-derivatives')
public async createWatermarkDerivative(
@Param('assetId') assetId: string,
@Body() body: unknown,
@Headers('idempotency-key') key: string | undefined,
@Req() request: AuthenticatedRequest,
@CurrentWorkspace() workspace: Workspace,
@Res({ passthrough: true }) response: { status: (code: number) => void },
) {
const result = await this.watermarkDerivatives.create(
workspace.id,
request.auth.userId,
uuid('asset_id', assetId),
idempotencyKey(key),
parse(createWatermarkDerivativeRequestSchema, body),
);
response.status(result.status);
return result.body;
}
@Get('media-watermark-derivatives/:derivativeId')
public async watermarkDerivative(
@Param('derivativeId') derivativeId: string,
@CurrentWorkspace() workspace: Workspace,
) {
return { data: await this.watermarkDerivatives.read(workspace.id, uuid('derivative_id', derivativeId)) };
}
@Post('media-assets/:assetId/aspect-ratio-derivatives')
public async createAspectRatioDerivative(
@Param('assetId') assetId: string,
@Body() body: unknown,
@Headers('idempotency-key') key: string | undefined,
@Req() request: AuthenticatedRequest,
@CurrentWorkspace() workspace: Workspace,
@Res({ passthrough: true }) response: { status: (code: number) => void },
) {
const result = await this.derivatives.create(
workspace.id,
request.auth.userId,
uuid('asset_id', assetId),
idempotencyKey(key),
parse(createAspectRatioDerivativeRequestSchema, body),
);
response.status(result.status);
return result.body;
}
@Get('media-derivatives/:derivativeId')
public async aspectRatioDerivative(
@Param('derivativeId') derivativeId: string,
@CurrentWorkspace() workspace: Workspace,
) {
return { data: await this.derivatives.read(workspace.id, uuid('derivative_id', derivativeId)) };
}
@Get('media-assets/product-uploads')
@ApiOkResponse({ description: '最近上传的商品图片及短时预览地址' })
public async productUploads(@CurrentWorkspace() workspace: Workspace) {
@@ -102,17 +238,18 @@ export class MediaController {
}
@Post('media-assets/:assetId/access-urls')
@ApiOkResponse({ description: '已签发短时预览 URL' })
@ApiOkResponse({ description: '已签发短时预览或下载 URL' })
public async accessUrl(
@Param('assetId') assetId: string,
@Body() body: unknown,
@CurrentWorkspace() workspace: Workspace,
) {
parse(createMediaAccessUrlRequestSchema, body);
const input = parse(createMediaAccessUrlRequestSchema, body) as CreateMediaAccessUrlRequest;
return {
data: await this.media.accessUrl(
workspace.id,
uuid('asset_id', assetId),
input.purpose,
),
};
}