feat: 添加图片拼接功能和相关API文档更新
- 在应用中引入ImageCompositionController和ImageCompositionService - 实现图片拼接功能,支持多种排列方式和输出格式 - 更新Swagger文档,添加图片拼接API描述和示例 - 修改模板执行API描述,简化审核流程说明
This commit is contained in:
@@ -95,7 +95,7 @@
|
|||||||
},
|
},
|
||||||
"/api/v1/templates/code/{code}/execute": {
|
"/api/v1/templates/code/{code}/execute": {
|
||||||
"post": {
|
"post": {
|
||||||
"description": "\n 根据模板代码执行AI生成任务,支持同步和异步两种审核模式:\n - 同步模式:立即返回审核结果,若通过则开始执行,返回 executionId 和 taskId\n - 异步模式:返回 executionId 和 auditTaskId,等待审核回调完成后开始执行\n \n ID说明:\n - executionId: 数据库执行记录主键,用于查询执行状态\n - taskId: 外部服务(N8N)任务ID,同步模式时返回\n - auditTaskId: 审核服务任务ID,异步模式时返回\n ",
|
"description": "\n 根据模板代码执行AI生成任务,直接执行模板无需后端审核。\n \n 参数说明:\n - imageUrl: 输入图片URL(必需)\n - auditTaskId: 前端审核任务ID(可选),用于追踪审核记录\n \n 返回ID说明:\n - executionId: 数据库执行记录主键,用于查询执行状态\n - taskId: 外部服务(N8N)任务ID,用于追踪模板执行状态\n - auditTaskId: 前端传入的审核任务ID\n ",
|
||||||
"operationId": "TemplateController_executeTemplateByCode",
|
"operationId": "TemplateController_executeTemplateByCode",
|
||||||
"parameters": [
|
"parameters": [
|
||||||
{
|
{
|
||||||
@@ -131,7 +131,9 @@
|
|||||||
"data": {
|
"data": {
|
||||||
"executionId": 123,
|
"executionId": 123,
|
||||||
"taskId": "n8n_task_456",
|
"taskId": "n8n_task_456",
|
||||||
"status": "processing"
|
"auditTaskId": "audit_task_789",
|
||||||
|
"status": "processing",
|
||||||
|
"message": "模板执行已启动"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -158,20 +160,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"403": {
|
|
||||||
"description": "图片审核未通过",
|
|
||||||
"content": {
|
|
||||||
"application/json": {
|
|
||||||
"schema": {
|
|
||||||
"example": {
|
|
||||||
"code": 403,
|
|
||||||
"message": "图片审核未通过: 包含不当内容",
|
|
||||||
"data": null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"429": {
|
"429": {
|
||||||
"description": "任务数量限制",
|
"description": "任务数量限制",
|
||||||
"content": {
|
"content": {
|
||||||
@@ -1094,52 +1082,95 @@
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"/api/v1/enhanced/templates/code/{code}/execute": {
|
"/api/v1/image-composition/compose": {
|
||||||
"post": {
|
"post": {
|
||||||
"description": "统一异步模式:先提交审核,然后通过回调继续执行模板。支持实时状态查询。",
|
"description": "将多张图片按指定位置拼接到一个画布上,支持层级、透明度等设置",
|
||||||
"operationId": "EnhancedTemplateController_executeTemplateByCode",
|
"operationId": "ImageCompositionController_composeImages",
|
||||||
"parameters": [
|
"parameters": [],
|
||||||
{
|
"requestBody": {
|
||||||
"name": "code",
|
"required": true,
|
||||||
"required": true,
|
"description": "图片拼接配置 - 简化版本",
|
||||||
"in": "path",
|
"content": {
|
||||||
"schema": {
|
"application/json": {
|
||||||
"type": "string"
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/ImageCompositionDto"
|
||||||
|
},
|
||||||
|
"examples": {
|
||||||
|
"horizontal": {
|
||||||
|
"summary": "水平排列示例",
|
||||||
|
"value": {
|
||||||
|
"images": [
|
||||||
|
"https://picsum.photos/300/200",
|
||||||
|
"https://picsum.photos/300/200",
|
||||||
|
"https://picsum.photos/300/200"
|
||||||
|
],
|
||||||
|
"direction": "horizontal",
|
||||||
|
"outputFormat": "jpeg",
|
||||||
|
"maxWidth": 1920,
|
||||||
|
"maxHeight": 1080,
|
||||||
|
"quality": 80
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"vertical": {
|
||||||
|
"summary": "垂直排列示例",
|
||||||
|
"value": {
|
||||||
|
"images": [
|
||||||
|
"https://picsum.photos/200/300",
|
||||||
|
"https://picsum.photos/200/300",
|
||||||
|
"https://picsum.photos/200/300"
|
||||||
|
],
|
||||||
|
"direction": "vertical",
|
||||||
|
"outputFormat": "webp",
|
||||||
|
"maxWidth": 800,
|
||||||
|
"maxHeight": 1200,
|
||||||
|
"quality": 85
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"grid": {
|
||||||
|
"summary": "网格排列示例",
|
||||||
|
"value": {
|
||||||
|
"images": [
|
||||||
|
"https://picsum.photos/200/200",
|
||||||
|
"https://picsum.photos/200/200",
|
||||||
|
"https://picsum.photos/200/200",
|
||||||
|
"https://picsum.photos/200/200",
|
||||||
|
"https://picsum.photos/200/200"
|
||||||
|
],
|
||||||
|
"direction": "grid",
|
||||||
|
"outputFormat": "jpeg",
|
||||||
|
"maxWidth": 1024,
|
||||||
|
"maxHeight": 1024,
|
||||||
|
"quality": 75
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
|
||||||
"responses": {
|
|
||||||
"201": {
|
|
||||||
"description": ""
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"summary": "异步执行模板 (增强版)",
|
|
||||||
"tags": [
|
|
||||||
"AI模板系统 (增强版)"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"/api/v1/enhanced/templates/{executionId}/status": {
|
|
||||||
"get": {
|
|
||||||
"operationId": "EnhancedTemplateController_getExecutionStatus",
|
|
||||||
"parameters": [
|
|
||||||
{
|
|
||||||
"name": "executionId",
|
|
||||||
"required": true,
|
|
||||||
"in": "path",
|
|
||||||
"schema": {
|
|
||||||
"type": "number"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"responses": {
|
"responses": {
|
||||||
"200": {
|
"200": {
|
||||||
"description": ""
|
"description": "图片拼接成功",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/ImageCompositionResponseDto"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"description": "请求参数错误"
|
||||||
|
},
|
||||||
|
"401": {
|
||||||
|
"description": "未授权访问"
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "服务器内部错误"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"summary": "查询执行状态 (增强版)",
|
"summary": "图片拼接",
|
||||||
"tags": [
|
"tags": [
|
||||||
"AI模板系统 (增强版)"
|
"图片处理"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -1778,7 +1809,7 @@
|
|||||||
],
|
],
|
||||||
"servers": [
|
"servers": [
|
||||||
{
|
{
|
||||||
"url": "http://localhost:3004",
|
"url": "http://localhost:3003",
|
||||||
"description": "开发环境"
|
"description": "开发环境"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -1850,6 +1881,11 @@
|
|||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "输入图片URL",
|
"description": "输入图片URL",
|
||||||
"example": "https://cdn.example.com/upload/image.jpg"
|
"example": "https://cdn.example.com/upload/image.jpg"
|
||||||
|
},
|
||||||
|
"auditTaskId": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "审核任务ID(可选)",
|
||||||
|
"example": "audit_task_12345"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": [
|
"required": [
|
||||||
@@ -2227,6 +2263,115 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"ImageCompositionDto": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"images": {
|
||||||
|
"description": "图片URL或base64数据列表",
|
||||||
|
"example": [
|
||||||
|
"https://picsum.photos/300/200",
|
||||||
|
"https://picsum.photos/300/200",
|
||||||
|
"https://picsum.photos/300/200"
|
||||||
|
],
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"direction": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "排列方向",
|
||||||
|
"enum": [
|
||||||
|
"horizontal",
|
||||||
|
"vertical",
|
||||||
|
"grid"
|
||||||
|
],
|
||||||
|
"example": "horizontal"
|
||||||
|
},
|
||||||
|
"outputFormat": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "输出格式",
|
||||||
|
"enum": [
|
||||||
|
"jpeg",
|
||||||
|
"png",
|
||||||
|
"webp"
|
||||||
|
],
|
||||||
|
"example": "png",
|
||||||
|
"default": "png"
|
||||||
|
},
|
||||||
|
"maxWidth": {
|
||||||
|
"type": "number",
|
||||||
|
"description": "最大输出宽度(像素),超过会等比压缩",
|
||||||
|
"example": 1920,
|
||||||
|
"default": 1920,
|
||||||
|
"minimum": 100,
|
||||||
|
"maximum": 4096
|
||||||
|
},
|
||||||
|
"maxHeight": {
|
||||||
|
"type": "number",
|
||||||
|
"description": "最大输出高度(像素),超过会等比压缩",
|
||||||
|
"example": 1080,
|
||||||
|
"default": 1080,
|
||||||
|
"minimum": 100,
|
||||||
|
"maximum": 4096
|
||||||
|
},
|
||||||
|
"quality": {
|
||||||
|
"type": "number",
|
||||||
|
"description": "图片质量(1-100),仅对JPEG/WebP有效",
|
||||||
|
"example": 80,
|
||||||
|
"default": 80,
|
||||||
|
"minimum": 1,
|
||||||
|
"maximum": 100
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"images",
|
||||||
|
"direction"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"ImageCompositionResponseDto": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"imageUrl": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "拼接后的图片URL",
|
||||||
|
"example": "https://s3.amazonaws.com/bucket/composed-image-1234567890.png"
|
||||||
|
},
|
||||||
|
"format": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "图片格式",
|
||||||
|
"example": "png"
|
||||||
|
},
|
||||||
|
"size": {
|
||||||
|
"type": "number",
|
||||||
|
"description": "图片大小(字节)",
|
||||||
|
"example": 12345
|
||||||
|
},
|
||||||
|
"width": {
|
||||||
|
"type": "number",
|
||||||
|
"description": "画布宽度",
|
||||||
|
"example": 800
|
||||||
|
},
|
||||||
|
"height": {
|
||||||
|
"type": "number",
|
||||||
|
"description": "画布高度",
|
||||||
|
"example": 600
|
||||||
|
},
|
||||||
|
"processingTime": {
|
||||||
|
"type": "number",
|
||||||
|
"description": "处理耗时(毫秒)",
|
||||||
|
"example": 150
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"imageUrl",
|
||||||
|
"format",
|
||||||
|
"size",
|
||||||
|
"width",
|
||||||
|
"height",
|
||||||
|
"processingTime"
|
||||||
|
]
|
||||||
|
},
|
||||||
"PlatformType": {
|
"PlatformType": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"enum": [
|
"enum": [
|
||||||
|
|||||||
@@ -41,9 +41,11 @@
|
|||||||
"axios": "^1.11.0",
|
"axios": "^1.11.0",
|
||||||
"class-transformer": "^0.5.1",
|
"class-transformer": "^0.5.1",
|
||||||
"class-validator": "^0.14.2",
|
"class-validator": "^0.14.2",
|
||||||
|
"form-data": "^4.0.4",
|
||||||
"mysql2": "^3.9.7",
|
"mysql2": "^3.9.7",
|
||||||
"reflect-metadata": "^0.2.2",
|
"reflect-metadata": "^0.2.2",
|
||||||
"rxjs": "^7.8.1",
|
"rxjs": "^7.8.1",
|
||||||
|
"sharp": "^0.34.4",
|
||||||
"typeorm": "^0.3.20"
|
"typeorm": "^0.3.20"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@@ -55,6 +57,7 @@
|
|||||||
"@types/express": "^5.0.0",
|
"@types/express": "^5.0.0",
|
||||||
"@types/jest": "^30.0.0",
|
"@types/jest": "^30.0.0",
|
||||||
"@types/node": "^22.10.7",
|
"@types/node": "^22.10.7",
|
||||||
|
"@types/sharp": "^0.32.0",
|
||||||
"@types/supertest": "^6.0.2",
|
"@types/supertest": "^6.0.2",
|
||||||
"eslint": "^9.18.0",
|
"eslint": "^9.18.0",
|
||||||
"eslint-config-prettier": "^10.0.1",
|
"eslint-config-prettier": "^10.0.1",
|
||||||
|
|||||||
346
pnpm-lock.yaml
generated
346
pnpm-lock.yaml
generated
@@ -32,12 +32,6 @@ importers:
|
|||||||
'@nestjs/typeorm':
|
'@nestjs/typeorm':
|
||||||
specifier: ^11.0.0
|
specifier: ^11.0.0
|
||||||
version: 11.0.0(@nestjs/common@11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.6)(reflect-metadata@0.2.2)(rxjs@7.8.2)(typeorm@0.3.26(mysql2@3.14.4)(reflect-metadata@0.2.2)(ts-node@10.9.2(@types/node@22.18.0)(typescript@5.9.2)))
|
version: 11.0.0(@nestjs/common@11.1.6(class-transformer@0.5.1)(class-validator@0.14.2)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.6)(reflect-metadata@0.2.2)(rxjs@7.8.2)(typeorm@0.3.26(mysql2@3.14.4)(reflect-metadata@0.2.2)(ts-node@10.9.2(@types/node@22.18.0)(typescript@5.9.2)))
|
||||||
'@open-dy/open_api_credential':
|
|
||||||
specifier: ^1.0.0
|
|
||||||
version: 1.0.0
|
|
||||||
'@open-dy/open_api_sdk':
|
|
||||||
specifier: 1.1.2
|
|
||||||
version: 1.1.2
|
|
||||||
axios:
|
axios:
|
||||||
specifier: ^1.11.0
|
specifier: ^1.11.0
|
||||||
version: 1.11.0
|
version: 1.11.0
|
||||||
@@ -47,6 +41,9 @@ importers:
|
|||||||
class-validator:
|
class-validator:
|
||||||
specifier: ^0.14.2
|
specifier: ^0.14.2
|
||||||
version: 0.14.2
|
version: 0.14.2
|
||||||
|
form-data:
|
||||||
|
specifier: ^4.0.4
|
||||||
|
version: 4.0.4
|
||||||
mysql2:
|
mysql2:
|
||||||
specifier: ^3.9.7
|
specifier: ^3.9.7
|
||||||
version: 3.14.4
|
version: 3.14.4
|
||||||
@@ -56,6 +53,9 @@ importers:
|
|||||||
rxjs:
|
rxjs:
|
||||||
specifier: ^7.8.1
|
specifier: ^7.8.1
|
||||||
version: 7.8.2
|
version: 7.8.2
|
||||||
|
sharp:
|
||||||
|
specifier: ^0.34.4
|
||||||
|
version: 0.34.4
|
||||||
typeorm:
|
typeorm:
|
||||||
specifier: ^0.3.20
|
specifier: ^0.3.20
|
||||||
version: 0.3.26(mysql2@3.14.4)(reflect-metadata@0.2.2)(ts-node@10.9.2(@types/node@22.18.0)(typescript@5.9.2))
|
version: 0.3.26(mysql2@3.14.4)(reflect-metadata@0.2.2)(ts-node@10.9.2(@types/node@22.18.0)(typescript@5.9.2))
|
||||||
@@ -84,6 +84,9 @@ importers:
|
|||||||
'@types/node':
|
'@types/node':
|
||||||
specifier: ^22.10.7
|
specifier: ^22.10.7
|
||||||
version: 22.18.0
|
version: 22.18.0
|
||||||
|
'@types/sharp':
|
||||||
|
specifier: ^0.32.0
|
||||||
|
version: 0.32.0
|
||||||
'@types/supertest':
|
'@types/supertest':
|
||||||
specifier: ^6.0.2
|
specifier: ^6.0.2
|
||||||
version: 6.0.3
|
version: 6.0.3
|
||||||
@@ -132,9 +135,6 @@ importers:
|
|||||||
|
|
||||||
packages:
|
packages:
|
||||||
|
|
||||||
'@alicloud/tea-typescript@1.8.0':
|
|
||||||
resolution: {integrity: sha512-CWXWaquauJf0sW30mgJRVu9aaXyBth5uMBCUc+5vKTK1zlgf3hIqRUjJZbjlwHwQ5y9anwcu18r48nOZb7l2QQ==}
|
|
||||||
|
|
||||||
'@ampproject/remapping@2.3.0':
|
'@ampproject/remapping@2.3.0':
|
||||||
resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==}
|
resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==}
|
||||||
engines: {node: '>=6.0.0'}
|
engines: {node: '>=6.0.0'}
|
||||||
@@ -400,6 +400,132 @@ packages:
|
|||||||
resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==}
|
resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==}
|
||||||
engines: {node: '>=18.18'}
|
engines: {node: '>=18.18'}
|
||||||
|
|
||||||
|
'@img/colour@1.0.0':
|
||||||
|
resolution: {integrity: sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
|
||||||
|
'@img/sharp-darwin-arm64@0.34.4':
|
||||||
|
resolution: {integrity: sha512-sitdlPzDVyvmINUdJle3TNHl+AG9QcwiAMsXmccqsCOMZNIdW2/7S26w0LyU8euiLVzFBL3dXPwVCq/ODnf2vA==}
|
||||||
|
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [darwin]
|
||||||
|
|
||||||
|
'@img/sharp-darwin-x64@0.34.4':
|
||||||
|
resolution: {integrity: sha512-rZheupWIoa3+SOdF/IcUe1ah4ZDpKBGWcsPX6MT0lYniH9micvIU7HQkYTfrx5Xi8u+YqwLtxC/3vl8TQN6rMg==}
|
||||||
|
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [darwin]
|
||||||
|
|
||||||
|
'@img/sharp-libvips-darwin-arm64@1.2.3':
|
||||||
|
resolution: {integrity: sha512-QzWAKo7kpHxbuHqUC28DZ9pIKpSi2ts2OJnoIGI26+HMgq92ZZ4vk8iJd4XsxN+tYfNJxzH6W62X5eTcsBymHw==}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [darwin]
|
||||||
|
|
||||||
|
'@img/sharp-libvips-darwin-x64@1.2.3':
|
||||||
|
resolution: {integrity: sha512-Ju+g2xn1E2AKO6YBhxjj+ACcsPQRHT0bhpglxcEf+3uyPY+/gL8veniKoo96335ZaPo03bdDXMv0t+BBFAbmRA==}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [darwin]
|
||||||
|
|
||||||
|
'@img/sharp-libvips-linux-arm64@1.2.3':
|
||||||
|
resolution: {integrity: sha512-I4RxkXU90cpufazhGPyVujYwfIm9Nk1QDEmiIsaPwdnm013F7RIceaCc87kAH+oUB1ezqEvC6ga4m7MSlqsJvQ==}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@img/sharp-libvips-linux-arm@1.2.3':
|
||||||
|
resolution: {integrity: sha512-x1uE93lyP6wEwGvgAIV0gP6zmaL/a0tGzJs/BIDDG0zeBhMnuUPm7ptxGhUbcGs4okDJrk4nxgrmxpib9g6HpA==}
|
||||||
|
cpu: [arm]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@img/sharp-libvips-linux-ppc64@1.2.3':
|
||||||
|
resolution: {integrity: sha512-Y2T7IsQvJLMCBM+pmPbM3bKT/yYJvVtLJGfCs4Sp95SjvnFIjynbjzsa7dY1fRJX45FTSfDksbTp6AGWudiyCg==}
|
||||||
|
cpu: [ppc64]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@img/sharp-libvips-linux-s390x@1.2.3':
|
||||||
|
resolution: {integrity: sha512-RgWrs/gVU7f+K7P+KeHFaBAJlNkD1nIZuVXdQv6S+fNA6syCcoboNjsV2Pou7zNlVdNQoQUpQTk8SWDHUA3y/w==}
|
||||||
|
cpu: [s390x]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@img/sharp-libvips-linux-x64@1.2.3':
|
||||||
|
resolution: {integrity: sha512-3JU7LmR85K6bBiRzSUc/Ff9JBVIFVvq6bomKE0e63UXGeRw2HPVEjoJke1Yx+iU4rL7/7kUjES4dZ/81Qjhyxg==}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@img/sharp-libvips-linuxmusl-arm64@1.2.3':
|
||||||
|
resolution: {integrity: sha512-F9q83RZ8yaCwENw1GieztSfj5msz7GGykG/BA+MOUefvER69K/ubgFHNeSyUu64amHIYKGDs4sRCMzXVj8sEyw==}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@img/sharp-libvips-linuxmusl-x64@1.2.3':
|
||||||
|
resolution: {integrity: sha512-U5PUY5jbc45ANM6tSJpsgqmBF/VsL6LnxJmIf11kB7J5DctHgqm0SkuXzVWtIY90GnJxKnC/JT251TDnk1fu/g==}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@img/sharp-linux-arm64@0.34.4':
|
||||||
|
resolution: {integrity: sha512-YXU1F/mN/Wu786tl72CyJjP/Ngl8mGHN1hST4BGl+hiW5jhCnV2uRVTNOcaYPs73NeT/H8Upm3y9582JVuZHrQ==}
|
||||||
|
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@img/sharp-linux-arm@0.34.4':
|
||||||
|
resolution: {integrity: sha512-Xyam4mlqM0KkTHYVSuc6wXRmM7LGN0P12li03jAnZ3EJWZqj83+hi8Y9UxZUbxsgsK1qOEwg7O0Bc0LjqQVtxA==}
|
||||||
|
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||||
|
cpu: [arm]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@img/sharp-linux-ppc64@0.34.4':
|
||||||
|
resolution: {integrity: sha512-F4PDtF4Cy8L8hXA2p3TO6s4aDt93v+LKmpcYFLAVdkkD3hSxZzee0rh6/+94FpAynsuMpLX5h+LRsSG3rIciUQ==}
|
||||||
|
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||||
|
cpu: [ppc64]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@img/sharp-linux-s390x@0.34.4':
|
||||||
|
resolution: {integrity: sha512-qVrZKE9Bsnzy+myf7lFKvng6bQzhNUAYcVORq2P7bDlvmF6u2sCmK2KyEQEBdYk+u3T01pVsPrkj943T1aJAsw==}
|
||||||
|
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||||
|
cpu: [s390x]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@img/sharp-linux-x64@0.34.4':
|
||||||
|
resolution: {integrity: sha512-ZfGtcp2xS51iG79c6Vhw9CWqQC8l2Ot8dygxoDoIQPTat/Ov3qAa8qpxSrtAEAJW+UjTXc4yxCjNfxm4h6Xm2A==}
|
||||||
|
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@img/sharp-linuxmusl-arm64@0.34.4':
|
||||||
|
resolution: {integrity: sha512-8hDVvW9eu4yHWnjaOOR8kHVrew1iIX+MUgwxSuH2XyYeNRtLUe4VNioSqbNkB7ZYQJj9rUTT4PyRscyk2PXFKA==}
|
||||||
|
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@img/sharp-linuxmusl-x64@0.34.4':
|
||||||
|
resolution: {integrity: sha512-lU0aA5L8QTlfKjpDCEFOZsTYGn3AEiO6db8W5aQDxj0nQkVrZWmN3ZP9sYKWJdtq3PWPhUNlqehWyXpYDcI9Sg==}
|
||||||
|
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@img/sharp-wasm32@0.34.4':
|
||||||
|
resolution: {integrity: sha512-33QL6ZO/qpRyG7woB/HUALz28WnTMI2W1jgX3Nu2bypqLIKx/QKMILLJzJjI+SIbvXdG9fUnmrxR7vbi1sTBeA==}
|
||||||
|
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||||
|
cpu: [wasm32]
|
||||||
|
|
||||||
|
'@img/sharp-win32-arm64@0.34.4':
|
||||||
|
resolution: {integrity: sha512-2Q250do/5WXTwxW3zjsEuMSv5sUU4Tq9VThWKlU2EYLm4MB7ZeMwF+SFJutldYODXF6jzc6YEOC+VfX0SZQPqA==}
|
||||||
|
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [win32]
|
||||||
|
|
||||||
|
'@img/sharp-win32-ia32@0.34.4':
|
||||||
|
resolution: {integrity: sha512-3ZeLue5V82dT92CNL6rsal6I2weKw1cYu+rGKm8fOCCtJTR2gYeUfY3FqUnIJsMUPIH68oS5jmZ0NiJ508YpEw==}
|
||||||
|
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||||
|
cpu: [ia32]
|
||||||
|
os: [win32]
|
||||||
|
|
||||||
|
'@img/sharp-win32-x64@0.34.4':
|
||||||
|
resolution: {integrity: sha512-xIyj4wpYs8J18sVN3mSQjwrw7fKUqRw+Z5rnHNCy5fYTxigBz81u5mOMPmFumwjcn8+ld1ppptMBCLic1nz6ig==}
|
||||||
|
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [win32]
|
||||||
|
|
||||||
'@inquirer/checkbox@4.2.2':
|
'@inquirer/checkbox@4.2.2':
|
||||||
resolution: {integrity: sha512-E+KExNurKcUJJdxmjglTl141EwxWyAHplvsYJQgSwXf8qiNWkTxTuCCqmhFEmbIXd4zLaGMfQFJ6WrZ7fSeV3g==}
|
resolution: {integrity: sha512-E+KExNurKcUJJdxmjglTl141EwxWyAHplvsYJQgSwXf8qiNWkTxTuCCqmhFEmbIXd4zLaGMfQFJ6WrZ7fSeV3g==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
@@ -816,15 +942,6 @@ packages:
|
|||||||
engines: {node: ^14.18.0 || >=16.10.0, npm: '>=5.10.0'}
|
engines: {node: ^14.18.0 || >=16.10.0, npm: '>=5.10.0'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
'@open-dy/open_api_credential@1.0.0':
|
|
||||||
resolution: {integrity: sha512-MmLrJL2QFYHV3RNJ2/hQEXcjzBCa6mtklW+AJXG+xS/x8cJ7/AgW3wQjvNwJ0oMRyxch1nQ10h9EnOGr1kew0g==}
|
|
||||||
|
|
||||||
'@open-dy/open_api_sdk@1.1.2':
|
|
||||||
resolution: {integrity: sha512-Z+TzDxzFf+25Hw6E3crKLNRrFrN45I+BrfmnxPHm0QpGHO284FmvbEaug1lXq480jD32kPZMjJQR+r5d8LYW9g==}
|
|
||||||
|
|
||||||
'@open-dy/open_api_util@1.0.1':
|
|
||||||
resolution: {integrity: sha512-vgUTsyNqB0mtn0LclEr7J2E0eoXTHym1Xh4hO5gXXGYS4MoZGpu9IbYlkR4LMVUSOmJs5ZhRxmNqvEt4D/5fLg==}
|
|
||||||
|
|
||||||
'@paralleldrive/cuid2@2.2.2':
|
'@paralleldrive/cuid2@2.2.2':
|
||||||
resolution: {integrity: sha512-ZOBkgDwEdoYVlSeRbYYXs0S9MejQofiVYoTbKzy/6GQa39/q5tQU2IX46+shYnUkpEl3wc+J6wRlar7r2EK2xA==}
|
resolution: {integrity: sha512-ZOBkgDwEdoYVlSeRbYYXs0S9MejQofiVYoTbKzy/6GQa39/q5tQU2IX46+shYnUkpEl3wc+J6wRlar7r2EK2xA==}
|
||||||
|
|
||||||
@@ -936,12 +1053,6 @@ packages:
|
|||||||
'@types/mime@1.3.5':
|
'@types/mime@1.3.5':
|
||||||
resolution: {integrity: sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==}
|
resolution: {integrity: sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==}
|
||||||
|
|
||||||
'@types/node@12.20.55':
|
|
||||||
resolution: {integrity: sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==}
|
|
||||||
|
|
||||||
'@types/node@20.19.13':
|
|
||||||
resolution: {integrity: sha512-yCAeZl7a0DxgNVteXFHt9+uyFbqXGy/ShC4BlcHkoE0AfGXYv/BUiplV72DjMYXHDBXFjhvr6DD1NiRVfB4j8g==}
|
|
||||||
|
|
||||||
'@types/node@22.18.0':
|
'@types/node@22.18.0':
|
||||||
resolution: {integrity: sha512-m5ObIqwsUp6BZzyiy4RdZpzWGub9bqLJMvZDD0QMXhxjqMHMENlj+SqF5QxoUwaQNFe+8kz8XM8ZQhqkQPTgMQ==}
|
resolution: {integrity: sha512-m5ObIqwsUp6BZzyiy4RdZpzWGub9bqLJMvZDD0QMXhxjqMHMENlj+SqF5QxoUwaQNFe+8kz8XM8ZQhqkQPTgMQ==}
|
||||||
|
|
||||||
@@ -957,6 +1068,10 @@ packages:
|
|||||||
'@types/serve-static@1.15.8':
|
'@types/serve-static@1.15.8':
|
||||||
resolution: {integrity: sha512-roei0UY3LhpOJvjbIP6ZZFngyLKl5dskOtDhxY5THRSpO+ZI+nzJ+m5yUMzGrp89YRa7lvknKkMYjqQFGwA7Sg==}
|
resolution: {integrity: sha512-roei0UY3LhpOJvjbIP6ZZFngyLKl5dskOtDhxY5THRSpO+ZI+nzJ+m5yUMzGrp89YRa7lvknKkMYjqQFGwA7Sg==}
|
||||||
|
|
||||||
|
'@types/sharp@0.32.0':
|
||||||
|
resolution: {integrity: sha512-OOi3kL+FZDnPhVzsfD37J88FNeZh6gQsGcLc95NbeURRGvmSjeXiDcyWzF2o3yh/gQAUn2uhh/e+CPCa5nwAxw==}
|
||||||
|
deprecated: This is a stub types definition. sharp provides its own type definitions, so you do not need this installed.
|
||||||
|
|
||||||
'@types/stack-utils@2.0.3':
|
'@types/stack-utils@2.0.3':
|
||||||
resolution: {integrity: sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==}
|
resolution: {integrity: sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==}
|
||||||
|
|
||||||
@@ -1076,49 +1191,41 @@ packages:
|
|||||||
resolution: {integrity: sha512-34gw7PjDGB9JgePJEmhEqBhWvCiiWCuXsL9hYphDF7crW7UgI05gyBAi6MF58uGcMOiOqSJ2ybEeCvHcq0BCmQ==}
|
resolution: {integrity: sha512-34gw7PjDGB9JgePJEmhEqBhWvCiiWCuXsL9hYphDF7crW7UgI05gyBAi6MF58uGcMOiOqSJ2ybEeCvHcq0BCmQ==}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
libc: [glibc]
|
|
||||||
|
|
||||||
'@unrs/resolver-binding-linux-arm64-musl@1.11.1':
|
'@unrs/resolver-binding-linux-arm64-musl@1.11.1':
|
||||||
resolution: {integrity: sha512-RyMIx6Uf53hhOtJDIamSbTskA99sPHS96wxVE/bJtePJJtpdKGXO1wY90oRdXuYOGOTuqjT8ACccMc4K6QmT3w==}
|
resolution: {integrity: sha512-RyMIx6Uf53hhOtJDIamSbTskA99sPHS96wxVE/bJtePJJtpdKGXO1wY90oRdXuYOGOTuqjT8ACccMc4K6QmT3w==}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
libc: [musl]
|
|
||||||
|
|
||||||
'@unrs/resolver-binding-linux-ppc64-gnu@1.11.1':
|
'@unrs/resolver-binding-linux-ppc64-gnu@1.11.1':
|
||||||
resolution: {integrity: sha512-D8Vae74A4/a+mZH0FbOkFJL9DSK2R6TFPC9M+jCWYia/q2einCubX10pecpDiTmkJVUH+y8K3BZClycD8nCShA==}
|
resolution: {integrity: sha512-D8Vae74A4/a+mZH0FbOkFJL9DSK2R6TFPC9M+jCWYia/q2einCubX10pecpDiTmkJVUH+y8K3BZClycD8nCShA==}
|
||||||
cpu: [ppc64]
|
cpu: [ppc64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
libc: [glibc]
|
|
||||||
|
|
||||||
'@unrs/resolver-binding-linux-riscv64-gnu@1.11.1':
|
'@unrs/resolver-binding-linux-riscv64-gnu@1.11.1':
|
||||||
resolution: {integrity: sha512-frxL4OrzOWVVsOc96+V3aqTIQl1O2TjgExV4EKgRY09AJ9leZpEg8Ak9phadbuX0BA4k8U5qtvMSQQGGmaJqcQ==}
|
resolution: {integrity: sha512-frxL4OrzOWVVsOc96+V3aqTIQl1O2TjgExV4EKgRY09AJ9leZpEg8Ak9phadbuX0BA4k8U5qtvMSQQGGmaJqcQ==}
|
||||||
cpu: [riscv64]
|
cpu: [riscv64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
libc: [glibc]
|
|
||||||
|
|
||||||
'@unrs/resolver-binding-linux-riscv64-musl@1.11.1':
|
'@unrs/resolver-binding-linux-riscv64-musl@1.11.1':
|
||||||
resolution: {integrity: sha512-mJ5vuDaIZ+l/acv01sHoXfpnyrNKOk/3aDoEdLO/Xtn9HuZlDD6jKxHlkN8ZhWyLJsRBxfv9GYM2utQ1SChKew==}
|
resolution: {integrity: sha512-mJ5vuDaIZ+l/acv01sHoXfpnyrNKOk/3aDoEdLO/Xtn9HuZlDD6jKxHlkN8ZhWyLJsRBxfv9GYM2utQ1SChKew==}
|
||||||
cpu: [riscv64]
|
cpu: [riscv64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
libc: [musl]
|
|
||||||
|
|
||||||
'@unrs/resolver-binding-linux-s390x-gnu@1.11.1':
|
'@unrs/resolver-binding-linux-s390x-gnu@1.11.1':
|
||||||
resolution: {integrity: sha512-kELo8ebBVtb9sA7rMe1Cph4QHreByhaZ2QEADd9NzIQsYNQpt9UkM9iqr2lhGr5afh885d/cB5QeTXSbZHTYPg==}
|
resolution: {integrity: sha512-kELo8ebBVtb9sA7rMe1Cph4QHreByhaZ2QEADd9NzIQsYNQpt9UkM9iqr2lhGr5afh885d/cB5QeTXSbZHTYPg==}
|
||||||
cpu: [s390x]
|
cpu: [s390x]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
libc: [glibc]
|
|
||||||
|
|
||||||
'@unrs/resolver-binding-linux-x64-gnu@1.11.1':
|
'@unrs/resolver-binding-linux-x64-gnu@1.11.1':
|
||||||
resolution: {integrity: sha512-C3ZAHugKgovV5YvAMsxhq0gtXuwESUKc5MhEtjBpLoHPLYM+iuwSj3lflFwK3DPm68660rZ7G8BMcwSro7hD5w==}
|
resolution: {integrity: sha512-C3ZAHugKgovV5YvAMsxhq0gtXuwESUKc5MhEtjBpLoHPLYM+iuwSj3lflFwK3DPm68660rZ7G8BMcwSro7hD5w==}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
libc: [glibc]
|
|
||||||
|
|
||||||
'@unrs/resolver-binding-linux-x64-musl@1.11.1':
|
'@unrs/resolver-binding-linux-x64-musl@1.11.1':
|
||||||
resolution: {integrity: sha512-rV0YSoyhK2nZ4vEswT/QwqzqQXw5I6CjoaYMOX0TqBlWhojUf8P94mvI7nuJTeaCkkds3QE4+zS8Ko+GdXuZtA==}
|
resolution: {integrity: sha512-rV0YSoyhK2nZ4vEswT/QwqzqQXw5I6CjoaYMOX0TqBlWhojUf8P94mvI7nuJTeaCkkds3QE4+zS8Ko+GdXuZtA==}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
libc: [musl]
|
|
||||||
|
|
||||||
'@unrs/resolver-binding-wasm32-wasi@1.11.1':
|
'@unrs/resolver-binding-wasm32-wasi@1.11.1':
|
||||||
resolution: {integrity: sha512-5u4RkfxJm+Ng7IWgkzi3qrFOvLvQYnPBmjmZQ8+szTK/b31fQCnleNl1GgEt7nIsZRIf5PLhPwT0WM+q45x/UQ==}
|
resolution: {integrity: sha512-5u4RkfxJm+Ng7IWgkzi3qrFOvLvQYnPBmjmZQ8+szTK/b31fQCnleNl1GgEt7nIsZRIf5PLhPwT0WM+q45x/UQ==}
|
||||||
@@ -1620,6 +1727,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==}
|
resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==}
|
||||||
engines: {node: '>= 0.8'}
|
engines: {node: '>= 0.8'}
|
||||||
|
|
||||||
|
detect-libc@2.1.1:
|
||||||
|
resolution: {integrity: sha512-ecqj/sy1jcK1uWrwpR67UhYrIFQ+5WlGxth34WquCbamhFA6hkkwiu37o6J5xCHdo1oixJRfVRw+ywV+Hq/0Aw==}
|
||||||
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
detect-newline@3.1.0:
|
detect-newline@3.1.0:
|
||||||
resolution: {integrity: sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==}
|
resolution: {integrity: sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
@@ -2037,9 +2148,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==}
|
resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==}
|
||||||
engines: {node: '>= 0.8'}
|
engines: {node: '>= 0.8'}
|
||||||
|
|
||||||
httpx@2.3.3:
|
|
||||||
resolution: {integrity: sha512-k1qv94u1b6e+XKCxVbLgYlOypVP9MPGpnN5G/vxFf6tDO4V3xpz3d6FUOY/s8NtPgaq5RBVVgSB+7IHpVxMYzw==}
|
|
||||||
|
|
||||||
human-signals@2.1.0:
|
human-signals@2.1.0:
|
||||||
resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==}
|
resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==}
|
||||||
engines: {node: '>=10.17.0'}
|
engines: {node: '>=10.17.0'}
|
||||||
@@ -2872,6 +2980,10 @@ packages:
|
|||||||
engines: {node: '>= 0.10'}
|
engines: {node: '>= 0.10'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
sharp@0.34.4:
|
||||||
|
resolution: {integrity: sha512-FUH39xp3SBPnxWvd5iib1X8XY7J0K0X7d93sie9CJg2PO8/7gmg89Nve6OjItK53/MlAushNNxteBYfM6DEuoA==}
|
||||||
|
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||||
|
|
||||||
shebang-command@2.0.0:
|
shebang-command@2.0.0:
|
||||||
resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==}
|
resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
@@ -3397,13 +3509,6 @@ packages:
|
|||||||
|
|
||||||
snapshots:
|
snapshots:
|
||||||
|
|
||||||
'@alicloud/tea-typescript@1.8.0':
|
|
||||||
dependencies:
|
|
||||||
'@types/node': 12.20.55
|
|
||||||
httpx: 2.3.3
|
|
||||||
transitivePeerDependencies:
|
|
||||||
- supports-color
|
|
||||||
|
|
||||||
'@ampproject/remapping@2.3.0':
|
'@ampproject/remapping@2.3.0':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@jridgewell/gen-mapping': 0.3.13
|
'@jridgewell/gen-mapping': 0.3.13
|
||||||
@@ -3713,6 +3818,94 @@ snapshots:
|
|||||||
|
|
||||||
'@humanwhocodes/retry@0.4.3': {}
|
'@humanwhocodes/retry@0.4.3': {}
|
||||||
|
|
||||||
|
'@img/colour@1.0.0': {}
|
||||||
|
|
||||||
|
'@img/sharp-darwin-arm64@0.34.4':
|
||||||
|
optionalDependencies:
|
||||||
|
'@img/sharp-libvips-darwin-arm64': 1.2.3
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@img/sharp-darwin-x64@0.34.4':
|
||||||
|
optionalDependencies:
|
||||||
|
'@img/sharp-libvips-darwin-x64': 1.2.3
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@img/sharp-libvips-darwin-arm64@1.2.3':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@img/sharp-libvips-darwin-x64@1.2.3':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@img/sharp-libvips-linux-arm64@1.2.3':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@img/sharp-libvips-linux-arm@1.2.3':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@img/sharp-libvips-linux-ppc64@1.2.3':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@img/sharp-libvips-linux-s390x@1.2.3':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@img/sharp-libvips-linux-x64@1.2.3':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@img/sharp-libvips-linuxmusl-arm64@1.2.3':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@img/sharp-libvips-linuxmusl-x64@1.2.3':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@img/sharp-linux-arm64@0.34.4':
|
||||||
|
optionalDependencies:
|
||||||
|
'@img/sharp-libvips-linux-arm64': 1.2.3
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@img/sharp-linux-arm@0.34.4':
|
||||||
|
optionalDependencies:
|
||||||
|
'@img/sharp-libvips-linux-arm': 1.2.3
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@img/sharp-linux-ppc64@0.34.4':
|
||||||
|
optionalDependencies:
|
||||||
|
'@img/sharp-libvips-linux-ppc64': 1.2.3
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@img/sharp-linux-s390x@0.34.4':
|
||||||
|
optionalDependencies:
|
||||||
|
'@img/sharp-libvips-linux-s390x': 1.2.3
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@img/sharp-linux-x64@0.34.4':
|
||||||
|
optionalDependencies:
|
||||||
|
'@img/sharp-libvips-linux-x64': 1.2.3
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@img/sharp-linuxmusl-arm64@0.34.4':
|
||||||
|
optionalDependencies:
|
||||||
|
'@img/sharp-libvips-linuxmusl-arm64': 1.2.3
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@img/sharp-linuxmusl-x64@0.34.4':
|
||||||
|
optionalDependencies:
|
||||||
|
'@img/sharp-libvips-linuxmusl-x64': 1.2.3
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@img/sharp-wasm32@0.34.4':
|
||||||
|
dependencies:
|
||||||
|
'@emnapi/runtime': 1.5.0
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@img/sharp-win32-arm64@0.34.4':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@img/sharp-win32-ia32@0.34.4':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@img/sharp-win32-x64@0.34.4':
|
||||||
|
optional: true
|
||||||
|
|
||||||
'@inquirer/checkbox@4.2.2(@types/node@22.18.0)':
|
'@inquirer/checkbox@4.2.2(@types/node@22.18.0)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@inquirer/core': 10.2.0(@types/node@22.18.0)
|
'@inquirer/core': 10.2.0(@types/node@22.18.0)
|
||||||
@@ -4257,27 +4450,6 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
consola: 3.4.2
|
consola: 3.4.2
|
||||||
|
|
||||||
'@open-dy/open_api_credential@1.0.0':
|
|
||||||
dependencies:
|
|
||||||
axios: 1.11.0
|
|
||||||
transitivePeerDependencies:
|
|
||||||
- debug
|
|
||||||
|
|
||||||
'@open-dy/open_api_sdk@1.1.2':
|
|
||||||
dependencies:
|
|
||||||
'@alicloud/tea-typescript': 1.8.0
|
|
||||||
'@open-dy/open_api_credential': 1.0.0
|
|
||||||
'@open-dy/open_api_util': 1.0.1
|
|
||||||
transitivePeerDependencies:
|
|
||||||
- debug
|
|
||||||
- supports-color
|
|
||||||
|
|
||||||
'@open-dy/open_api_util@1.0.1':
|
|
||||||
dependencies:
|
|
||||||
'@alicloud/tea-typescript': 1.8.0
|
|
||||||
transitivePeerDependencies:
|
|
||||||
- supports-color
|
|
||||||
|
|
||||||
'@paralleldrive/cuid2@2.2.2':
|
'@paralleldrive/cuid2@2.2.2':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@noble/hashes': 1.8.0
|
'@noble/hashes': 1.8.0
|
||||||
@@ -4408,12 +4580,6 @@ snapshots:
|
|||||||
|
|
||||||
'@types/mime@1.3.5': {}
|
'@types/mime@1.3.5': {}
|
||||||
|
|
||||||
'@types/node@12.20.55': {}
|
|
||||||
|
|
||||||
'@types/node@20.19.13':
|
|
||||||
dependencies:
|
|
||||||
undici-types: 6.21.0
|
|
||||||
|
|
||||||
'@types/node@22.18.0':
|
'@types/node@22.18.0':
|
||||||
dependencies:
|
dependencies:
|
||||||
undici-types: 6.21.0
|
undici-types: 6.21.0
|
||||||
@@ -4433,6 +4599,10 @@ snapshots:
|
|||||||
'@types/node': 22.18.0
|
'@types/node': 22.18.0
|
||||||
'@types/send': 0.17.5
|
'@types/send': 0.17.5
|
||||||
|
|
||||||
|
'@types/sharp@0.32.0':
|
||||||
|
dependencies:
|
||||||
|
sharp: 0.34.4
|
||||||
|
|
||||||
'@types/stack-utils@2.0.3': {}
|
'@types/stack-utils@2.0.3': {}
|
||||||
|
|
||||||
'@types/superagent@8.1.9':
|
'@types/superagent@8.1.9':
|
||||||
@@ -5103,6 +5273,8 @@ snapshots:
|
|||||||
|
|
||||||
depd@2.0.0: {}
|
depd@2.0.0: {}
|
||||||
|
|
||||||
|
detect-libc@2.1.1: {}
|
||||||
|
|
||||||
detect-newline@3.1.0: {}
|
detect-newline@3.1.0: {}
|
||||||
|
|
||||||
dezalgo@1.0.4:
|
dezalgo@1.0.4:
|
||||||
@@ -5577,13 +5749,6 @@ snapshots:
|
|||||||
statuses: 2.0.1
|
statuses: 2.0.1
|
||||||
toidentifier: 1.0.1
|
toidentifier: 1.0.1
|
||||||
|
|
||||||
httpx@2.3.3:
|
|
||||||
dependencies:
|
|
||||||
'@types/node': 20.19.13
|
|
||||||
debug: 4.4.1
|
|
||||||
transitivePeerDependencies:
|
|
||||||
- supports-color
|
|
||||||
|
|
||||||
human-signals@2.1.0: {}
|
human-signals@2.1.0: {}
|
||||||
|
|
||||||
iconv-lite@0.6.3:
|
iconv-lite@0.6.3:
|
||||||
@@ -6542,6 +6707,35 @@ snapshots:
|
|||||||
safe-buffer: 5.2.1
|
safe-buffer: 5.2.1
|
||||||
to-buffer: 1.2.1
|
to-buffer: 1.2.1
|
||||||
|
|
||||||
|
sharp@0.34.4:
|
||||||
|
dependencies:
|
||||||
|
'@img/colour': 1.0.0
|
||||||
|
detect-libc: 2.1.1
|
||||||
|
semver: 7.7.2
|
||||||
|
optionalDependencies:
|
||||||
|
'@img/sharp-darwin-arm64': 0.34.4
|
||||||
|
'@img/sharp-darwin-x64': 0.34.4
|
||||||
|
'@img/sharp-libvips-darwin-arm64': 1.2.3
|
||||||
|
'@img/sharp-libvips-darwin-x64': 1.2.3
|
||||||
|
'@img/sharp-libvips-linux-arm': 1.2.3
|
||||||
|
'@img/sharp-libvips-linux-arm64': 1.2.3
|
||||||
|
'@img/sharp-libvips-linux-ppc64': 1.2.3
|
||||||
|
'@img/sharp-libvips-linux-s390x': 1.2.3
|
||||||
|
'@img/sharp-libvips-linux-x64': 1.2.3
|
||||||
|
'@img/sharp-libvips-linuxmusl-arm64': 1.2.3
|
||||||
|
'@img/sharp-libvips-linuxmusl-x64': 1.2.3
|
||||||
|
'@img/sharp-linux-arm': 0.34.4
|
||||||
|
'@img/sharp-linux-arm64': 0.34.4
|
||||||
|
'@img/sharp-linux-ppc64': 0.34.4
|
||||||
|
'@img/sharp-linux-s390x': 0.34.4
|
||||||
|
'@img/sharp-linux-x64': 0.34.4
|
||||||
|
'@img/sharp-linuxmusl-arm64': 0.34.4
|
||||||
|
'@img/sharp-linuxmusl-x64': 0.34.4
|
||||||
|
'@img/sharp-wasm32': 0.34.4
|
||||||
|
'@img/sharp-win32-arm64': 0.34.4
|
||||||
|
'@img/sharp-win32-ia32': 0.34.4
|
||||||
|
'@img/sharp-win32-x64': 0.34.4
|
||||||
|
|
||||||
shebang-command@2.0.0:
|
shebang-command@2.0.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
shebang-regex: 3.0.0
|
shebang-regex: 3.0.0
|
||||||
|
|||||||
@@ -17,6 +17,8 @@ import { UserSubscriptionEntity } from './entities/user-subscription.entity';
|
|||||||
import { AdWatchEntity } from './entities/ad-watch.entity';
|
import { AdWatchEntity } from './entities/ad-watch.entity';
|
||||||
import { N8nTemplateFactoryService } from './services/n8n-template-factory.service';
|
import { N8nTemplateFactoryService } from './services/n8n-template-factory.service';
|
||||||
import { TemplateController } from './controllers/template.controller';
|
import { TemplateController } from './controllers/template.controller';
|
||||||
|
import { ImageCompositionController } from './controllers/image-composition.controller';
|
||||||
|
import { ImageCompositionService } from './services/image-composition.service';
|
||||||
import { PlatformModule } from './platform/platform.module';
|
import { PlatformModule } from './platform/platform.module';
|
||||||
import { ContentModerationModule } from './content-moderation/content-moderation.module';
|
import { ContentModerationModule } from './content-moderation/content-moderation.module';
|
||||||
import { PaymentModule } from './payment/payment.module';
|
import { PaymentModule } from './payment/payment.module';
|
||||||
@@ -54,8 +56,8 @@ import { PaymentModule } from './payment/payment.module';
|
|||||||
ContentModerationModule,
|
ContentModerationModule,
|
||||||
PaymentModule,
|
PaymentModule,
|
||||||
],
|
],
|
||||||
controllers: [AppController, TemplateController],
|
controllers: [AppController, TemplateController, ImageCompositionController],
|
||||||
providers: [TemplateService, TemplateManager, N8nTemplateFactoryService],
|
providers: [TemplateService, TemplateManager, N8nTemplateFactoryService, ImageCompositionService],
|
||||||
exports: [N8nTemplateFactoryService],
|
exports: [N8nTemplateFactoryService],
|
||||||
})
|
})
|
||||||
export class AppModule {}
|
export class AppModule {}
|
||||||
|
|||||||
@@ -0,0 +1,95 @@
|
|||||||
|
import { Test, TestingModule } from '@nestjs/testing';
|
||||||
|
import { HttpService } from '@nestjs/axios';
|
||||||
|
import { ImageCompositionController } from '../image-composition.controller';
|
||||||
|
import { ImageCompositionService } from '../../services/image-composition.service';
|
||||||
|
import {
|
||||||
|
ImageCompositionDto,
|
||||||
|
ArrangementDirection,
|
||||||
|
OutputFormat,
|
||||||
|
} from '../../dto/image-composition.dto';
|
||||||
|
|
||||||
|
describe('ImageCompositionController', () => {
|
||||||
|
let controller: ImageCompositionController;
|
||||||
|
let service: ImageCompositionService;
|
||||||
|
|
||||||
|
const mockHttpService = {
|
||||||
|
get: jest.fn(),
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
const module: TestingModule = await Test.createTestingModule({
|
||||||
|
controllers: [ImageCompositionController],
|
||||||
|
providers: [
|
||||||
|
ImageCompositionService,
|
||||||
|
{
|
||||||
|
provide: HttpService,
|
||||||
|
useValue: mockHttpService,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}).compile();
|
||||||
|
|
||||||
|
controller = module.get<ImageCompositionController>(
|
||||||
|
ImageCompositionController,
|
||||||
|
);
|
||||||
|
service = module.get<ImageCompositionService>(ImageCompositionService);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be defined', () => {
|
||||||
|
expect(controller).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('composeImages', () => {
|
||||||
|
it('should validate minimum requirements', async () => {
|
||||||
|
const invalidDto: ImageCompositionDto = {
|
||||||
|
images: [], // 空数组应该抛出错误
|
||||||
|
direction: ArrangementDirection.HORIZONTAL,
|
||||||
|
outputFormat: OutputFormat.PNG,
|
||||||
|
};
|
||||||
|
|
||||||
|
await expect(controller.composeImages(invalidDto)).rejects.toThrow(
|
||||||
|
'至少需要提供一张图片',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should validate image count limit', async () => {
|
||||||
|
const images = Array.from(
|
||||||
|
{ length: 25 },
|
||||||
|
(_, i) => `https://example.com/test${i}.jpg`,
|
||||||
|
);
|
||||||
|
|
||||||
|
const invalidDto: ImageCompositionDto = {
|
||||||
|
images, // 超过20张图片限制
|
||||||
|
direction: ArrangementDirection.HORIZONTAL,
|
||||||
|
outputFormat: OutputFormat.PNG,
|
||||||
|
};
|
||||||
|
|
||||||
|
await expect(controller.composeImages(invalidDto)).rejects.toThrow(
|
||||||
|
'图片数量不能超过20张',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should validate arrangement direction', async () => {
|
||||||
|
const invalidDto: any = {
|
||||||
|
images: ['https://example.com/test.jpg'],
|
||||||
|
direction: 'invalid-direction', // 无效的排列方向
|
||||||
|
outputFormat: OutputFormat.PNG,
|
||||||
|
};
|
||||||
|
|
||||||
|
await expect(controller.composeImages(invalidDto)).rejects.toThrow(
|
||||||
|
'排列方向必须是以下值之一',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should validate image URL format', async () => {
|
||||||
|
const invalidDto: ImageCompositionDto = {
|
||||||
|
images: ['invalid-url'], // 无效的URL格式
|
||||||
|
direction: ArrangementDirection.HORIZONTAL,
|
||||||
|
outputFormat: OutputFormat.PNG,
|
||||||
|
};
|
||||||
|
|
||||||
|
await expect(controller.composeImages(invalidDto)).rejects.toThrow(
|
||||||
|
'图片必须是有效的HTTP(S) URL或base64格式',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
1
src/controllers/__tests__/image-composition.example.ts
Normal file
1
src/controllers/__tests__/image-composition.example.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
|
||||||
215
src/controllers/image-composition.controller.ts
Normal file
215
src/controllers/image-composition.controller.ts
Normal file
@@ -0,0 +1,215 @@
|
|||||||
|
import {
|
||||||
|
Controller,
|
||||||
|
Post,
|
||||||
|
Body,
|
||||||
|
HttpException,
|
||||||
|
HttpStatus,
|
||||||
|
UseGuards,
|
||||||
|
} from '@nestjs/common';
|
||||||
|
import {
|
||||||
|
ApiTags,
|
||||||
|
ApiOperation,
|
||||||
|
ApiResponse as SwaggerApiResponse,
|
||||||
|
ApiBody,
|
||||||
|
} from '@nestjs/swagger';
|
||||||
|
import { ImageCompositionService } from '../services/image-composition.service';
|
||||||
|
import {
|
||||||
|
ImageCompositionDto,
|
||||||
|
ImageCompositionResponseDto,
|
||||||
|
ArrangementDirection,
|
||||||
|
} from '../dto/image-composition.dto';
|
||||||
|
import { ApiCommonResponses } from '../decorators/api-common-responses.decorator';
|
||||||
|
import { PlatformAuthGuard } from '../platform/guards/platform-auth.guard';
|
||||||
|
import { ResponseUtil, ApiResponse } from '../utils/response.util';
|
||||||
|
|
||||||
|
@ApiTags('图片处理')
|
||||||
|
@Controller('image-composition')
|
||||||
|
@ApiCommonResponses()
|
||||||
|
export class ImageCompositionController {
|
||||||
|
constructor(
|
||||||
|
private readonly imageCompositionService: ImageCompositionService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
@Post('compose')
|
||||||
|
@UseGuards(PlatformAuthGuard)
|
||||||
|
@ApiOperation({
|
||||||
|
summary: '图片拼接',
|
||||||
|
description: '将多张图片按指定位置拼接到一个画布上,支持层级、透明度等设置',
|
||||||
|
})
|
||||||
|
@ApiBody({
|
||||||
|
type: ImageCompositionDto,
|
||||||
|
description: '图片拼接配置 - 简化版本',
|
||||||
|
examples: {
|
||||||
|
horizontal: {
|
||||||
|
summary: '水平排列示例',
|
||||||
|
value: {
|
||||||
|
images: [
|
||||||
|
'https://picsum.photos/300/200',
|
||||||
|
'https://picsum.photos/300/200',
|
||||||
|
'https://picsum.photos/300/200'
|
||||||
|
],
|
||||||
|
direction: 'horizontal',
|
||||||
|
outputFormat: 'jpeg',
|
||||||
|
maxWidth: 1920,
|
||||||
|
maxHeight: 1080,
|
||||||
|
quality: 80
|
||||||
|
},
|
||||||
|
},
|
||||||
|
vertical: {
|
||||||
|
summary: '垂直排列示例',
|
||||||
|
value: {
|
||||||
|
images: [
|
||||||
|
'https://picsum.photos/200/300',
|
||||||
|
'https://picsum.photos/200/300',
|
||||||
|
'https://picsum.photos/200/300'
|
||||||
|
],
|
||||||
|
direction: 'vertical',
|
||||||
|
outputFormat: 'webp',
|
||||||
|
maxWidth: 800,
|
||||||
|
maxHeight: 1200,
|
||||||
|
quality: 85
|
||||||
|
},
|
||||||
|
},
|
||||||
|
grid: {
|
||||||
|
summary: '网格排列示例',
|
||||||
|
value: {
|
||||||
|
images: [
|
||||||
|
'https://picsum.photos/200/200',
|
||||||
|
'https://picsum.photos/200/200',
|
||||||
|
'https://picsum.photos/200/200',
|
||||||
|
'https://picsum.photos/200/200',
|
||||||
|
'https://picsum.photos/200/200'
|
||||||
|
],
|
||||||
|
direction: 'grid',
|
||||||
|
outputFormat: 'jpeg',
|
||||||
|
maxWidth: 1024,
|
||||||
|
maxHeight: 1024,
|
||||||
|
quality: 75
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
@SwaggerApiResponse({
|
||||||
|
status: 200,
|
||||||
|
description: '图片拼接成功',
|
||||||
|
type: ImageCompositionResponseDto,
|
||||||
|
})
|
||||||
|
@SwaggerApiResponse({
|
||||||
|
status: 400,
|
||||||
|
description: '请求参数错误',
|
||||||
|
})
|
||||||
|
@SwaggerApiResponse({
|
||||||
|
status: 401,
|
||||||
|
description: '未授权访问',
|
||||||
|
})
|
||||||
|
@SwaggerApiResponse({
|
||||||
|
status: 500,
|
||||||
|
description: '服务器内部错误',
|
||||||
|
})
|
||||||
|
async composeImages(
|
||||||
|
@Body() compositionDto: ImageCompositionDto,
|
||||||
|
): Promise<ApiResponse<ImageCompositionResponseDto>> {
|
||||||
|
try {
|
||||||
|
// 基础参数验证
|
||||||
|
this.validateCompositionRequest(compositionDto);
|
||||||
|
|
||||||
|
const result = await this.imageCompositionService.composeImages(compositionDto);
|
||||||
|
|
||||||
|
return ResponseUtil.success(result, '图片拼接成功');
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof HttpException) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new HttpException(
|
||||||
|
`图片拼接失败: ${error.message}`,
|
||||||
|
HttpStatus.INTERNAL_SERVER_ERROR,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证拼接请求参数 - 简化版本
|
||||||
|
*/
|
||||||
|
private validateCompositionRequest(dto: ImageCompositionDto): void {
|
||||||
|
// 验证图片数量
|
||||||
|
if (!dto.images || dto.images.length === 0) {
|
||||||
|
throw new HttpException(
|
||||||
|
'至少需要提供一张图片',
|
||||||
|
HttpStatus.BAD_REQUEST,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dto.images.length > 20) {
|
||||||
|
throw new HttpException(
|
||||||
|
'图片数量不能超过20张',
|
||||||
|
HttpStatus.BAD_REQUEST,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证排列方向
|
||||||
|
const validDirections = Object.values(ArrangementDirection);
|
||||||
|
if (!validDirections.includes(dto.direction)) {
|
||||||
|
throw new HttpException(
|
||||||
|
`排列方向必须是以下值之一: ${validDirections.join(', ')}`,
|
||||||
|
HttpStatus.BAD_REQUEST,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证图片URL格式
|
||||||
|
for (const imageUrl of dto.images) {
|
||||||
|
if (!imageUrl || typeof imageUrl !== 'string') {
|
||||||
|
throw new HttpException(
|
||||||
|
'图片URL不能为空',
|
||||||
|
HttpStatus.BAD_REQUEST,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否为有效的URL或base64格式
|
||||||
|
const isValidUrl = this.isValidImageUrl(imageUrl);
|
||||||
|
const isValidBase64 = imageUrl.startsWith('data:image/');
|
||||||
|
|
||||||
|
if (!isValidUrl && !isValidBase64) {
|
||||||
|
throw new HttpException(
|
||||||
|
'图片必须是有效的HTTP(S) URL或base64格式',
|
||||||
|
HttpStatus.BAD_REQUEST,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证尺寸参数
|
||||||
|
if (dto.maxWidth !== undefined && (dto.maxWidth < 100 || dto.maxWidth > 4096)) {
|
||||||
|
throw new HttpException(
|
||||||
|
'最大宽度必须在100-4096像素之间',
|
||||||
|
HttpStatus.BAD_REQUEST,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dto.maxHeight !== undefined && (dto.maxHeight < 100 || dto.maxHeight > 4096)) {
|
||||||
|
throw new HttpException(
|
||||||
|
'最大高度必须在100-4096像素之间',
|
||||||
|
HttpStatus.BAD_REQUEST,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证质量参数
|
||||||
|
if (dto.quality !== undefined && (dto.quality < 1 || dto.quality > 100)) {
|
||||||
|
throw new HttpException(
|
||||||
|
'图片质量必须在1-100之间',
|
||||||
|
HttpStatus.BAD_REQUEST,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证图片URL格式
|
||||||
|
*/
|
||||||
|
private isValidImageUrl(url: string): boolean {
|
||||||
|
try {
|
||||||
|
const parsedUrl = new URL(url);
|
||||||
|
return parsedUrl.protocol === 'http:' || parsedUrl.protocol === 'https:';
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
117
src/dto/image-composition.dto.ts
Normal file
117
src/dto/image-composition.dto.ts
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||||
|
import { IsString, IsArray, IsEnum, IsOptional, IsNumber, Min, Max } from 'class-validator';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 排列方向枚举
|
||||||
|
*/
|
||||||
|
export enum ArrangementDirection {
|
||||||
|
HORIZONTAL = 'horizontal', // 水平排列
|
||||||
|
VERTICAL = 'vertical', // 垂直排列
|
||||||
|
GRID = 'grid', // 网格排列(自动计算行列)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 输出格式枚举
|
||||||
|
*/
|
||||||
|
export enum OutputFormat {
|
||||||
|
JPEG = 'jpeg',
|
||||||
|
PNG = 'png',
|
||||||
|
WEBP = 'webp',
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 图片拼接请求DTO - 简化版本
|
||||||
|
*/
|
||||||
|
export class ImageCompositionDto {
|
||||||
|
@ApiProperty({
|
||||||
|
description: '图片URL或base64数据列表',
|
||||||
|
example: [
|
||||||
|
'https://picsum.photos/300/200',
|
||||||
|
'https://picsum.photos/300/200',
|
||||||
|
'https://picsum.photos/300/200'
|
||||||
|
]
|
||||||
|
})
|
||||||
|
@IsArray()
|
||||||
|
@IsString({ each: true })
|
||||||
|
images: string[];
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
description: '排列方向',
|
||||||
|
enum: ArrangementDirection,
|
||||||
|
example: ArrangementDirection.HORIZONTAL
|
||||||
|
})
|
||||||
|
@IsEnum(ArrangementDirection)
|
||||||
|
direction: ArrangementDirection;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({
|
||||||
|
description: '输出格式',
|
||||||
|
enum: OutputFormat,
|
||||||
|
example: OutputFormat.PNG,
|
||||||
|
default: OutputFormat.PNG
|
||||||
|
})
|
||||||
|
@IsOptional()
|
||||||
|
@IsEnum(OutputFormat)
|
||||||
|
outputFormat?: OutputFormat = OutputFormat.PNG;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({
|
||||||
|
description: '最大输出宽度(像素),超过会等比压缩',
|
||||||
|
example: 1920,
|
||||||
|
default: 1920,
|
||||||
|
minimum: 100,
|
||||||
|
maximum: 4096
|
||||||
|
})
|
||||||
|
@IsOptional()
|
||||||
|
@IsNumber()
|
||||||
|
@Min(100)
|
||||||
|
@Max(4096)
|
||||||
|
maxWidth?: number = 1920;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({
|
||||||
|
description: '最大输出高度(像素),超过会等比压缩',
|
||||||
|
example: 1080,
|
||||||
|
default: 1080,
|
||||||
|
minimum: 100,
|
||||||
|
maximum: 4096
|
||||||
|
})
|
||||||
|
@IsOptional()
|
||||||
|
@IsNumber()
|
||||||
|
@Min(100)
|
||||||
|
@Max(4096)
|
||||||
|
maxHeight?: number = 1080;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({
|
||||||
|
description: '图片质量(1-100),仅对JPEG/WebP有效',
|
||||||
|
example: 80,
|
||||||
|
default: 80,
|
||||||
|
minimum: 1,
|
||||||
|
maximum: 100
|
||||||
|
})
|
||||||
|
@IsOptional()
|
||||||
|
@IsNumber()
|
||||||
|
@Min(1)
|
||||||
|
@Max(100)
|
||||||
|
quality?: number = 80;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 图片拼接响应DTO
|
||||||
|
*/
|
||||||
|
export class ImageCompositionResponseDto {
|
||||||
|
@ApiProperty({ description: '拼接后的图片URL', example: 'https://s3.amazonaws.com/bucket/composed-image-1234567890.png' })
|
||||||
|
imageUrl: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '图片格式', example: 'png' })
|
||||||
|
format: string;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '图片大小(字节)', example: 12345 })
|
||||||
|
size: number;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '画布宽度', example: 800 })
|
||||||
|
width: number;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '画布高度', example: 600 })
|
||||||
|
height: number;
|
||||||
|
|
||||||
|
@ApiProperty({ description: '处理耗时(毫秒)', example: 150 })
|
||||||
|
processingTime: number;
|
||||||
|
}
|
||||||
369
src/services/image-composition.service.ts
Normal file
369
src/services/image-composition.service.ts
Normal file
@@ -0,0 +1,369 @@
|
|||||||
|
import { Injectable, BadRequestException, Logger } from '@nestjs/common';
|
||||||
|
import sharp from 'sharp';
|
||||||
|
import { HttpService } from '@nestjs/axios';
|
||||||
|
import { firstValueFrom } from 'rxjs';
|
||||||
|
import FormData from 'form-data';
|
||||||
|
import {
|
||||||
|
ImageCompositionDto,
|
||||||
|
ImageCompositionResponseDto,
|
||||||
|
ArrangementDirection,
|
||||||
|
OutputFormat,
|
||||||
|
} from '../dto/image-composition.dto';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class ImageCompositionService {
|
||||||
|
private readonly logger = new Logger(ImageCompositionService.name);
|
||||||
|
|
||||||
|
constructor(private readonly httpService: HttpService) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 拼接图片 - 简化版本
|
||||||
|
*/
|
||||||
|
async composeImages(
|
||||||
|
compositionDto: ImageCompositionDto,
|
||||||
|
): Promise<ImageCompositionResponseDto> {
|
||||||
|
const startTime = Date.now();
|
||||||
|
|
||||||
|
try {
|
||||||
|
this.logger.log('开始图片拼接处理');
|
||||||
|
|
||||||
|
// 加载所有图片并获取尺寸信息,同时进行预处理
|
||||||
|
const imageInfos = await Promise.all(
|
||||||
|
compositionDto.images.map(async (imageUrl) => {
|
||||||
|
const buffer = await this.loadImage(imageUrl);
|
||||||
|
let processedBuffer = buffer;
|
||||||
|
|
||||||
|
// 压缩单张图片如果太大
|
||||||
|
const metadata = await sharp(buffer).metadata();
|
||||||
|
const originalWidth = metadata.width || 0;
|
||||||
|
const originalHeight = metadata.height || 0;
|
||||||
|
|
||||||
|
// 如果单张图片超过800x800,先压缩
|
||||||
|
if (originalWidth > 800 || originalHeight > 800) {
|
||||||
|
processedBuffer = await sharp(buffer)
|
||||||
|
.resize(800, 800, { fit: 'inside', withoutEnlargement: true })
|
||||||
|
.jpeg({ quality: 85 })
|
||||||
|
.toBuffer();
|
||||||
|
|
||||||
|
const newMetadata = await sharp(processedBuffer).metadata();
|
||||||
|
this.logger.log(`压缩图片: ${originalWidth}x${originalHeight} -> ${newMetadata.width}x${newMetadata.height}`);
|
||||||
|
|
||||||
|
return {
|
||||||
|
buffer: processedBuffer,
|
||||||
|
width: newMetadata.width || 0,
|
||||||
|
height: newMetadata.height || 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
buffer: processedBuffer,
|
||||||
|
width: originalWidth,
|
||||||
|
height: originalHeight,
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
// 根据排列方向计算画布尺寸和图片位置
|
||||||
|
const layout = this.calculateLayout(imageInfos, compositionDto.direction);
|
||||||
|
|
||||||
|
// 创建画布
|
||||||
|
const canvas = sharp({
|
||||||
|
create: {
|
||||||
|
width: layout.canvasWidth,
|
||||||
|
height: layout.canvasHeight,
|
||||||
|
channels: 4,
|
||||||
|
background: '#ffffff',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// 准备所有图片的composite参数
|
||||||
|
const compositeInputs = await Promise.all(
|
||||||
|
imageInfos.map(async (imageInfo, index) => {
|
||||||
|
const position = layout.positions[index];
|
||||||
|
let processedImage = sharp(imageInfo.buffer);
|
||||||
|
|
||||||
|
// 如果需要调整尺寸
|
||||||
|
if (
|
||||||
|
position.width !== imageInfo.width ||
|
||||||
|
position.height !== imageInfo.height
|
||||||
|
) {
|
||||||
|
processedImage = processedImage.resize(
|
||||||
|
position.width,
|
||||||
|
position.height,
|
||||||
|
{ fit: 'fill' },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
input: await processedImage.png().toBuffer(),
|
||||||
|
left: position.x,
|
||||||
|
top: position.y,
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
// 执行图片合成
|
||||||
|
let finalImage = canvas.composite(compositeInputs);
|
||||||
|
|
||||||
|
// 检查是否需要压缩最终图片
|
||||||
|
const maxWidth = compositionDto.maxWidth || 1920;
|
||||||
|
const maxHeight = compositionDto.maxHeight || 1080;
|
||||||
|
|
||||||
|
if (layout.canvasWidth > maxWidth || layout.canvasHeight > maxHeight) {
|
||||||
|
this.logger.log(`最终图片需要压缩: ${layout.canvasWidth}x${layout.canvasHeight} -> 最大${maxWidth}x${maxHeight}`);
|
||||||
|
finalImage = finalImage.resize(maxWidth, maxHeight, {
|
||||||
|
fit: 'inside',
|
||||||
|
withoutEnlargement: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 根据输出格式处理
|
||||||
|
const quality = compositionDto.quality || 80;
|
||||||
|
switch (compositionDto.outputFormat) {
|
||||||
|
case OutputFormat.JPEG:
|
||||||
|
finalImage = finalImage.jpeg({ quality, progressive: true });
|
||||||
|
break;
|
||||||
|
case OutputFormat.WEBP:
|
||||||
|
finalImage = finalImage.webp({ quality, effort: 6 });
|
||||||
|
break;
|
||||||
|
case OutputFormat.PNG:
|
||||||
|
default:
|
||||||
|
// PNG格式使用适度压缩
|
||||||
|
finalImage = finalImage.png({ compressionLevel: 8, adaptiveFiltering: true });
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
const resultBuffer = await finalImage.toBuffer();
|
||||||
|
const metadata = await sharp(resultBuffer).metadata();
|
||||||
|
|
||||||
|
const processingTime = Date.now() - startTime;
|
||||||
|
this.logger.log(`图片拼接完成,耗时: ${processingTime}ms`);
|
||||||
|
|
||||||
|
// 上传图片到S3并获取URL
|
||||||
|
const imageUrl = await this.uploadToS3(
|
||||||
|
resultBuffer,
|
||||||
|
compositionDto.outputFormat || 'png',
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
imageUrl,
|
||||||
|
format: compositionDto.outputFormat || 'png',
|
||||||
|
size: resultBuffer.length,
|
||||||
|
width: metadata.width || layout.canvasWidth,
|
||||||
|
height: metadata.height || layout.canvasHeight,
|
||||||
|
processingTime,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error('图片拼接失败', error);
|
||||||
|
throw new BadRequestException(`图片拼接失败: ${error.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据排列方向计算布局
|
||||||
|
*/
|
||||||
|
private calculateLayout(
|
||||||
|
imageInfos: Array<{ width: number; height: number }>,
|
||||||
|
direction: ArrangementDirection,
|
||||||
|
) {
|
||||||
|
const positions: Array<{
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
}> = [];
|
||||||
|
let canvasWidth = 0;
|
||||||
|
let canvasHeight = 0;
|
||||||
|
|
||||||
|
switch (direction) {
|
||||||
|
case ArrangementDirection.HORIZONTAL:
|
||||||
|
// 水平排列:所有图片保持原始高度,宽度累加
|
||||||
|
const maxHeight = Math.max(...imageInfos.map((img) => img.height));
|
||||||
|
canvasHeight = maxHeight;
|
||||||
|
|
||||||
|
let currentX = 0;
|
||||||
|
imageInfos.forEach((imageInfo) => {
|
||||||
|
positions.push({
|
||||||
|
x: currentX,
|
||||||
|
y: Math.floor((maxHeight - imageInfo.height) / 2), // 垂直居中
|
||||||
|
width: imageInfo.width,
|
||||||
|
height: imageInfo.height,
|
||||||
|
});
|
||||||
|
currentX += imageInfo.width;
|
||||||
|
});
|
||||||
|
canvasWidth = currentX;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case ArrangementDirection.VERTICAL:
|
||||||
|
// 垂直排列:所有图片保持原始宽度,高度累加
|
||||||
|
const maxWidth = Math.max(...imageInfos.map((img) => img.width));
|
||||||
|
canvasWidth = maxWidth;
|
||||||
|
|
||||||
|
let currentY = 0;
|
||||||
|
imageInfos.forEach((imageInfo) => {
|
||||||
|
positions.push({
|
||||||
|
x: Math.floor((maxWidth - imageInfo.width) / 2), // 水平居中
|
||||||
|
y: currentY,
|
||||||
|
width: imageInfo.width,
|
||||||
|
height: imageInfo.height,
|
||||||
|
});
|
||||||
|
currentY += imageInfo.height;
|
||||||
|
});
|
||||||
|
canvasHeight = currentY;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case ArrangementDirection.GRID:
|
||||||
|
// 网格排列:自动计算行列数
|
||||||
|
const imageCount = imageInfos.length;
|
||||||
|
const cols = Math.ceil(Math.sqrt(imageCount));
|
||||||
|
const rows = Math.ceil(imageCount / cols);
|
||||||
|
|
||||||
|
// 计算每个格子的尺寸(使用最大图片尺寸)
|
||||||
|
const cellWidth = Math.max(...imageInfos.map((img) => img.width));
|
||||||
|
const cellHeight = Math.max(...imageInfos.map((img) => img.height));
|
||||||
|
|
||||||
|
canvasWidth = cols * cellWidth;
|
||||||
|
canvasHeight = rows * cellHeight;
|
||||||
|
|
||||||
|
imageInfos.forEach((imageInfo, index) => {
|
||||||
|
const row = Math.floor(index / cols);
|
||||||
|
const col = index % cols;
|
||||||
|
|
||||||
|
positions.push({
|
||||||
|
x: col * cellWidth + Math.floor((cellWidth - imageInfo.width) / 2),
|
||||||
|
y:
|
||||||
|
row * cellHeight +
|
||||||
|
Math.floor((cellHeight - imageInfo.height) / 2),
|
||||||
|
width: imageInfo.width,
|
||||||
|
height: imageInfo.height,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
throw new BadRequestException(`不支持的排列方向: ${direction}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return { canvasWidth, canvasHeight, positions };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 加载图片
|
||||||
|
* 支持URL和base64格式
|
||||||
|
*/
|
||||||
|
private async loadImage(imageUrl: string): Promise<Buffer> {
|
||||||
|
try {
|
||||||
|
// 检查是否为base64格式
|
||||||
|
if (imageUrl.startsWith('data:image/')) {
|
||||||
|
const base64Data = imageUrl.split(',')[1];
|
||||||
|
return Buffer.from(base64Data, 'base64');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否为HTTP(S) URL
|
||||||
|
if (imageUrl.startsWith('http://') || imageUrl.startsWith('https://')) {
|
||||||
|
const response = await firstValueFrom(
|
||||||
|
this.httpService.get(imageUrl, {
|
||||||
|
responseType: 'arraybuffer',
|
||||||
|
timeout: 10000, // 10秒超时
|
||||||
|
maxContentLength: 10 * 1024 * 1024, // 最大10MB
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
return Buffer.from(response.data);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error('不支持的图片格式,请提供HTTP(S) URL或base64数据');
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error(`加载图片失败: ${imageUrl}`, error);
|
||||||
|
throw new BadRequestException(`加载图片失败: ${error.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 上传图片到S3
|
||||||
|
*/
|
||||||
|
private async uploadToS3(
|
||||||
|
imageBuffer: Buffer,
|
||||||
|
format: string,
|
||||||
|
): Promise<string> {
|
||||||
|
try {
|
||||||
|
this.logger.log('开始上传图片到S3');
|
||||||
|
|
||||||
|
// 创建FormData
|
||||||
|
const formData = new FormData();
|
||||||
|
const fileName = `composed-image-${Date.now()}.${format}`;
|
||||||
|
|
||||||
|
formData.append('file', imageBuffer, {
|
||||||
|
filename: fileName,
|
||||||
|
contentType: `image/${format}`,
|
||||||
|
});
|
||||||
|
|
||||||
|
// 上传到S3
|
||||||
|
const uploadResponse = await firstValueFrom(
|
||||||
|
this.httpService.post(
|
||||||
|
'https://bowongai-prod--text-video-agent-fastapi-app.modal.run/api/file/upload/s3',
|
||||||
|
formData,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
...formData.getHeaders(),
|
||||||
|
},
|
||||||
|
timeout: 30000, // 30秒超时
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
// 记录S3上传状态
|
||||||
|
this.logger.log('S3上传完成,状态:', uploadResponse.data.status);
|
||||||
|
|
||||||
|
// 检查响应并返回URL
|
||||||
|
if (
|
||||||
|
uploadResponse.data &&
|
||||||
|
uploadResponse.data.status &&
|
||||||
|
uploadResponse.data.data
|
||||||
|
) {
|
||||||
|
this.logger.log(`图片上传成功: ${uploadResponse.data.data}`);
|
||||||
|
return uploadResponse.data.data;
|
||||||
|
} else {
|
||||||
|
this.logger.error('S3上传响应格式异常,响应数据:', uploadResponse.data);
|
||||||
|
throw new Error('S3上传响应格式异常');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error('上传图片到S3失败,详细错误:', {
|
||||||
|
message: error.message,
|
||||||
|
status: error.response?.status,
|
||||||
|
statusText: error.response?.statusText,
|
||||||
|
data: error.response?.data,
|
||||||
|
config: {
|
||||||
|
url: error.config?.url,
|
||||||
|
method: error.config?.method,
|
||||||
|
headers: error.config?.headers,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// 如果S3上传失败,返回base64格式作为备选方案
|
||||||
|
this.logger.warn('S3上传失败,使用base64格式返回');
|
||||||
|
return `data:image/${format};base64,${imageBuffer.toString('base64')}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证图片格式
|
||||||
|
*/
|
||||||
|
private async validateImage(buffer: Buffer): Promise<void> {
|
||||||
|
try {
|
||||||
|
const metadata = await sharp(buffer).metadata();
|
||||||
|
if (!metadata.format) {
|
||||||
|
throw new Error('无法识别的图片格式');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查图片尺寸限制
|
||||||
|
if (metadata.width && metadata.width > 4096) {
|
||||||
|
throw new Error('图片宽度超过限制(最大4096px)');
|
||||||
|
}
|
||||||
|
if (metadata.height && metadata.height > 4096) {
|
||||||
|
throw new Error('图片高度超过限制(最大4096px)');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
throw new BadRequestException(`图片验证失败: ${error.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
253
swagger.json
253
swagger.json
@@ -95,7 +95,7 @@
|
|||||||
},
|
},
|
||||||
"/api/v1/templates/code/{code}/execute": {
|
"/api/v1/templates/code/{code}/execute": {
|
||||||
"post": {
|
"post": {
|
||||||
"description": "\n 根据模板代码执行AI生成任务,支持同步和异步两种审核模式:\n - 同步模式:立即返回审核结果,若通过则开始执行,返回 executionId 和 taskId\n - 异步模式:返回 executionId 和 auditTaskId,等待审核回调完成后开始执行\n \n ID说明:\n - executionId: 数据库执行记录主键,用于查询执行状态\n - taskId: 外部服务(N8N)任务ID,同步模式时返回\n - auditTaskId: 审核服务任务ID,异步模式时返回\n ",
|
"description": "\n 根据模板代码执行AI生成任务,直接执行模板无需后端审核。\n \n 参数说明:\n - imageUrl: 输入图片URL(必需)\n - auditTaskId: 前端审核任务ID(可选),用于追踪审核记录\n \n 返回ID说明:\n - executionId: 数据库执行记录主键,用于查询执行状态\n - taskId: 外部服务(N8N)任务ID,用于追踪模板执行状态\n - auditTaskId: 前端传入的审核任务ID\n ",
|
||||||
"operationId": "TemplateController_executeTemplateByCode",
|
"operationId": "TemplateController_executeTemplateByCode",
|
||||||
"parameters": [
|
"parameters": [
|
||||||
{
|
{
|
||||||
@@ -131,7 +131,9 @@
|
|||||||
"data": {
|
"data": {
|
||||||
"executionId": 123,
|
"executionId": 123,
|
||||||
"taskId": "n8n_task_456",
|
"taskId": "n8n_task_456",
|
||||||
"status": "processing"
|
"auditTaskId": "audit_task_789",
|
||||||
|
"status": "processing",
|
||||||
|
"message": "模板执行已启动"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -158,20 +160,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"403": {
|
|
||||||
"description": "图片审核未通过",
|
|
||||||
"content": {
|
|
||||||
"application/json": {
|
|
||||||
"schema": {
|
|
||||||
"example": {
|
|
||||||
"code": 403,
|
|
||||||
"message": "图片审核未通过: 包含不当内容",
|
|
||||||
"data": null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"429": {
|
"429": {
|
||||||
"description": "任务数量限制",
|
"description": "任务数量限制",
|
||||||
"content": {
|
"content": {
|
||||||
@@ -1094,52 +1082,95 @@
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"/api/v1/enhanced/templates/code/{code}/execute": {
|
"/api/v1/image-composition/compose": {
|
||||||
"post": {
|
"post": {
|
||||||
"description": "统一异步模式:先提交审核,然后通过回调继续执行模板。支持实时状态查询。",
|
"description": "将多张图片按指定位置拼接到一个画布上,支持层级、透明度等设置",
|
||||||
"operationId": "EnhancedTemplateController_executeTemplateByCode",
|
"operationId": "ImageCompositionController_composeImages",
|
||||||
"parameters": [
|
"parameters": [],
|
||||||
{
|
"requestBody": {
|
||||||
"name": "code",
|
"required": true,
|
||||||
"required": true,
|
"description": "图片拼接配置 - 简化版本",
|
||||||
"in": "path",
|
"content": {
|
||||||
"schema": {
|
"application/json": {
|
||||||
"type": "string"
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/ImageCompositionDto"
|
||||||
|
},
|
||||||
|
"examples": {
|
||||||
|
"horizontal": {
|
||||||
|
"summary": "水平排列示例",
|
||||||
|
"value": {
|
||||||
|
"images": [
|
||||||
|
"https://picsum.photos/300/200",
|
||||||
|
"https://picsum.photos/300/200",
|
||||||
|
"https://picsum.photos/300/200"
|
||||||
|
],
|
||||||
|
"direction": "horizontal",
|
||||||
|
"outputFormat": "jpeg",
|
||||||
|
"maxWidth": 1920,
|
||||||
|
"maxHeight": 1080,
|
||||||
|
"quality": 80
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"vertical": {
|
||||||
|
"summary": "垂直排列示例",
|
||||||
|
"value": {
|
||||||
|
"images": [
|
||||||
|
"https://picsum.photos/200/300",
|
||||||
|
"https://picsum.photos/200/300",
|
||||||
|
"https://picsum.photos/200/300"
|
||||||
|
],
|
||||||
|
"direction": "vertical",
|
||||||
|
"outputFormat": "webp",
|
||||||
|
"maxWidth": 800,
|
||||||
|
"maxHeight": 1200,
|
||||||
|
"quality": 85
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"grid": {
|
||||||
|
"summary": "网格排列示例",
|
||||||
|
"value": {
|
||||||
|
"images": [
|
||||||
|
"https://picsum.photos/200/200",
|
||||||
|
"https://picsum.photos/200/200",
|
||||||
|
"https://picsum.photos/200/200",
|
||||||
|
"https://picsum.photos/200/200",
|
||||||
|
"https://picsum.photos/200/200"
|
||||||
|
],
|
||||||
|
"direction": "grid",
|
||||||
|
"outputFormat": "jpeg",
|
||||||
|
"maxWidth": 1024,
|
||||||
|
"maxHeight": 1024,
|
||||||
|
"quality": 75
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
|
||||||
"responses": {
|
|
||||||
"201": {
|
|
||||||
"description": ""
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"summary": "异步执行模板 (增强版)",
|
|
||||||
"tags": [
|
|
||||||
"AI模板系统 (增强版)"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"/api/v1/enhanced/templates/{executionId}/status": {
|
|
||||||
"get": {
|
|
||||||
"operationId": "EnhancedTemplateController_getExecutionStatus",
|
|
||||||
"parameters": [
|
|
||||||
{
|
|
||||||
"name": "executionId",
|
|
||||||
"required": true,
|
|
||||||
"in": "path",
|
|
||||||
"schema": {
|
|
||||||
"type": "number"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"responses": {
|
"responses": {
|
||||||
"200": {
|
"200": {
|
||||||
"description": ""
|
"description": "图片拼接成功",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/ImageCompositionResponseDto"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"description": "请求参数错误"
|
||||||
|
},
|
||||||
|
"401": {
|
||||||
|
"description": "未授权访问"
|
||||||
|
},
|
||||||
|
"500": {
|
||||||
|
"description": "服务器内部错误"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"summary": "查询执行状态 (增强版)",
|
"summary": "图片拼接",
|
||||||
"tags": [
|
"tags": [
|
||||||
"AI模板系统 (增强版)"
|
"图片处理"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -1778,7 +1809,7 @@
|
|||||||
],
|
],
|
||||||
"servers": [
|
"servers": [
|
||||||
{
|
{
|
||||||
"url": "http://localhost:3004",
|
"url": "http://localhost:3003",
|
||||||
"description": "开发环境"
|
"description": "开发环境"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -1850,6 +1881,11 @@
|
|||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "输入图片URL",
|
"description": "输入图片URL",
|
||||||
"example": "https://cdn.example.com/upload/image.jpg"
|
"example": "https://cdn.example.com/upload/image.jpg"
|
||||||
|
},
|
||||||
|
"auditTaskId": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "审核任务ID(可选)",
|
||||||
|
"example": "audit_task_12345"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": [
|
"required": [
|
||||||
@@ -2227,6 +2263,115 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"ImageCompositionDto": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"images": {
|
||||||
|
"description": "图片URL或base64数据列表",
|
||||||
|
"example": [
|
||||||
|
"https://picsum.photos/300/200",
|
||||||
|
"https://picsum.photos/300/200",
|
||||||
|
"https://picsum.photos/300/200"
|
||||||
|
],
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"direction": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "排列方向",
|
||||||
|
"enum": [
|
||||||
|
"horizontal",
|
||||||
|
"vertical",
|
||||||
|
"grid"
|
||||||
|
],
|
||||||
|
"example": "horizontal"
|
||||||
|
},
|
||||||
|
"outputFormat": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "输出格式",
|
||||||
|
"enum": [
|
||||||
|
"jpeg",
|
||||||
|
"png",
|
||||||
|
"webp"
|
||||||
|
],
|
||||||
|
"example": "png",
|
||||||
|
"default": "png"
|
||||||
|
},
|
||||||
|
"maxWidth": {
|
||||||
|
"type": "number",
|
||||||
|
"description": "最大输出宽度(像素),超过会等比压缩",
|
||||||
|
"example": 1920,
|
||||||
|
"default": 1920,
|
||||||
|
"minimum": 100,
|
||||||
|
"maximum": 4096
|
||||||
|
},
|
||||||
|
"maxHeight": {
|
||||||
|
"type": "number",
|
||||||
|
"description": "最大输出高度(像素),超过会等比压缩",
|
||||||
|
"example": 1080,
|
||||||
|
"default": 1080,
|
||||||
|
"minimum": 100,
|
||||||
|
"maximum": 4096
|
||||||
|
},
|
||||||
|
"quality": {
|
||||||
|
"type": "number",
|
||||||
|
"description": "图片质量(1-100),仅对JPEG/WebP有效",
|
||||||
|
"example": 80,
|
||||||
|
"default": 80,
|
||||||
|
"minimum": 1,
|
||||||
|
"maximum": 100
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"images",
|
||||||
|
"direction"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"ImageCompositionResponseDto": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"imageUrl": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "拼接后的图片URL",
|
||||||
|
"example": "https://s3.amazonaws.com/bucket/composed-image-1234567890.png"
|
||||||
|
},
|
||||||
|
"format": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "图片格式",
|
||||||
|
"example": "png"
|
||||||
|
},
|
||||||
|
"size": {
|
||||||
|
"type": "number",
|
||||||
|
"description": "图片大小(字节)",
|
||||||
|
"example": 12345
|
||||||
|
},
|
||||||
|
"width": {
|
||||||
|
"type": "number",
|
||||||
|
"description": "画布宽度",
|
||||||
|
"example": 800
|
||||||
|
},
|
||||||
|
"height": {
|
||||||
|
"type": "number",
|
||||||
|
"description": "画布高度",
|
||||||
|
"example": 600
|
||||||
|
},
|
||||||
|
"processingTime": {
|
||||||
|
"type": "number",
|
||||||
|
"description": "处理耗时(毫秒)",
|
||||||
|
"example": 150
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"imageUrl",
|
||||||
|
"format",
|
||||||
|
"size",
|
||||||
|
"width",
|
||||||
|
"height",
|
||||||
|
"processingTime"
|
||||||
|
]
|
||||||
|
},
|
||||||
"PlatformType": {
|
"PlatformType": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"enum": [
|
"enum": [
|
||||||
|
|||||||
Reference in New Issue
Block a user