feat: Add automatic PSD preview generation

- Add psd-processor library to auto-generate previews from PSD files
- Update upload API to detect PSD files and generate previews automatically
- Make preview image optional for PSD file uploads
- Update UploadModal UI to show PSD files don't require manual preview upload
- Install ag-psd package for PSD processing
This commit is contained in:
chenpei 2025-10-26 15:49:33 +08:00
parent d17f6df2c7
commit e69f15337d
8 changed files with 123672 additions and 80 deletions

View File

@ -74,7 +74,7 @@
#### 用户特性
```typescript
用户属性:
├── 基础信息(邮箱、用户名)
├── 基础信息(邮箱、用户名、部门)
├── 角色权限(普通用户/管理员)
├── 激活状态(待激活/已激活)
├── 下载权限(允许/禁止)
@ -88,7 +88,16 @@
#### 智能上传功能
- 🎯 **拖拽上传** - 全窗口拖拽,自动识别文件类型
- 📸 **预览图上传** - 支持JPG、PNG、WebP等格式
- 📦 **压缩包上传** - 支持ZIP、RAR、7Z格式
- 📦 **文件上传** - 支持压缩包和源文件
- 压缩包格式:ZIP、RAR、7Z
- 源文件格式:MAX、PSD、AI、CAD、DWG、C4D
- 🤖 **智能识别** - 根据文件扩展名自动识别素材类型
- .max → 自动设置为3DMAX
- .psd → 自动设置为PSD
- .ai → 自动设置为AI
- .cad/.dwg → 自动设置为CAD
- .c4d → 自动设置为C4D
- 压缩包 → 需手动选择类型
- 📊 **实时进度** - 上传进度和网络速度实时显示
- 🚀 **大文件支持** - 最高支持10GB文件上传
- 🔄 **智能压缩** - 预览图自动优化(最大1000x700px)
@ -115,9 +124,18 @@
2. 输入商品型号 → SPU标识
3. 选择使用店铺 → 支持新建分类
4. 选择场景类别 → 支持新建场景
5. 选择素材类型 → 3DMAX/PSD/AI/CAD/C4D/其他
6. 选择渲染器 → CR/VR/其它(仅3DMAX类型显示)
7. 选择/拖拽压缩包 → 验证格式和大小
5. 选择/拖拽文件 → 支持压缩包或源文件
↓
智能识别文件格式:
- .max → 自动识别为3DMAX,显示渲染器选项
- .psd → 自动识别为PSD
- .ai → 自动识别为AI
- .cad/.dwg → 自动识别为CAD
- .c4d → 自动识别为C4D
- .zip/.rar/.7z → 需手动选择素材类型
↓
6. 选择素材类型 → 已自动识别或手动选择
7. 选择渲染器 → CR/VR/其它(仅3DMAX类型显示)
8. 开始上传 → 实时显示进度和速度
9. 上传完成 → 自动返回首页展示
```
@ -244,6 +262,13 @@
- 📊 **使用统计** - 查看每个分类的素材数量
- 🔄 **批量操作** - 支持批量修改分类
#### 部门管理
- 🏢 **部门列表** - 查看所有部门
- ➕ **新增部门** - 添加新的部门
- 🗑️ **删除部门** - 删除不需要的部门
- 👥 **用户关联** - 用户注册时选择所属部门
- 📊 **部门统计** - 查看各部门用户数量
#### 系统设置(外观设置)
- 🎨 **导航栏颜色** - 自定义顶部导航栏背景色
- 颜色选择器实时预览
@ -291,11 +316,23 @@
- 👻 无感知(后台运行)
- 📊 状态提示(实时显示)
#### 文件格式自动识别
- 🤖 **智能识别** - 根据文件扩展名自动识别素材类型
- 上传.max文件 → 自动设置为3DMAX类型
- 上传.psd文件 → 自动设置为PSD类型
- 上传.ai文件 → 自动设置为AI类型
- 上传.cad/.dwg文件 → 自动设置为CAD类型
- 上传.c4d文件 → 自动设置为C4D类型
- 🎯 **准确高效** - 减少手动选择步骤,提升上传效率
- 🔄 **灵活调整** - 自动识别后仍可手动修改类型
- 📦 **压缩包支持** - 压缩包需手动选择类型(内容未知)
- 🎨 **联动显示** - 识别为3DMAX时自动显示渲染器选项
#### 拖拽上传
- 🎯 **全窗口拖拽区域** - 整个上传窗口都支持拖拽
- 🎨 **视觉反馈** - 拖入时显示蓝色覆盖层提示
- 🧠 **智能识别** - 自动区分图片和压缩包
- 📁 **多文件支持** - 可同时拖入多个文件
- 🧠 **智能识别** - 自动区分图片和文件
- 📁 **多格式支持** - 支持压缩包和源文件拖拽
- ✨ **传统兼容** - 保留点击选择文件方式
#### 图片预览
@ -396,6 +433,7 @@
├── email (String, UNIQUE) # 邮箱
├── username (String, UNIQUE) # 用户名
├── password (String) # 加密密码
├── department (String) # 所属部门
├── role (Enum) # USER/ADMIN
├── isActive (Boolean) # 激活状态
├── canDownload (Boolean) # 下载权限
@ -476,6 +514,14 @@
└── siteName: 由壹点3D素材
```
**Department(部门表)**
```sql
部门表
├── id (UUID, PK) # 唯一标识
├── name (String, UNIQUE) # 部门名称
└── createdAt (DateTime) # 创建时间
```
### 存储策略
#### 混合存储方案
@ -596,6 +642,29 @@ DELETE /api/admin/users # 删除用户
Query: id (用户ID)
```
**系统设置**
```
GET /api/system-settings # 获取系统设置
返回: headerBgColor, logoUrl, siteName
PUT /api/system-settings # 更新系统设置(仅管理员)
Body: { headerBgColor, siteName }
POST /api/system-settings/upload-logo # 上传LOGO(仅管理员)
Body: FormData { logo: File }
返回: { logoUrl }
```
**部门管理**
```
GET /api/departments # 获取部门列表
POST /api/departments # 创建部门(仅管理员)
Body: { name: string }
DELETE /api/departments # 删除部门(仅管理员)
Query: id (部门ID)
```
**健康检查**
```
GET /api/health # 服务健康状态
@ -766,6 +835,45 @@ WHERE [筛选条件];
## 📝 更新日志
### v2.5.0 - 系统设置与部门管理 (2025-10-26)
**新增功能**
- 🎨 **外观设置**:自定义导航栏颜色、LOGO和网站名称
- 10种预设颜色快速选择
- 颜色选择器实时预览
- LOGO上传到MinIO S3存储
- 网站名称自定义修改
- 🏢 **部门管理**:完整的部门管理系统
- 部门列表查看和管理
- 新增/删除部门功能
- 用户注册时选择所属部门
- 用户信息关联部门字段
- 🤖 **文件格式自动识别**:智能识别上传文件类型
- 支持.max/.psd/.ai/.cad/.dwg/.c4d等源文件格式
- 根据文件扩展名自动设置素材类型
- 减少手动选择步骤,提升上传效率
- 自动识别3DMAX文件并显示渲染器选项
- 🔄 **自动应用**:设置保存后自动刷新生效
**数据库变更**
- 新增SystemSettings表(系统设置)
- 新增Department表(部门管理)
- User表新增department字段
**用户体验**
- 管理员可完全自定义平台外观
- 更好的组织架构管理
- 上传素材更加智能便捷
- 设置界面直观友好
**技术改进**
- 新增系统设置API接口
- 新增部门管理API接口
- 完善文件格式识别逻辑
- 完善前后端类型定义
---
### v2.4.0 - 多类型素材支持 (2025-10-26)
**新增功能**
@ -1205,13 +1313,20 @@ module.exports = {
- SPU:商品型号
- 使用店铺:选择或新建
- 场景类别:选择或新建
- 素材类型:选择3DMAX/PSD/AI/CAD/C4D/其他
- 渲染器:选择CR/VR/其它(仅3DMAX类型显示)
4. 上传压缩包:
4. 上传文件(支持源文件和压缩包):
- 方式1:点击选择文件
- 方式2:拖拽压缩包到窗口
5. 点击"开始上传"
6. 等待上传完成(查看进度和速度)
- 方式2:拖拽文件到窗口
- 📌 智能识别:
* 上传.max文件 → 自动识别为3DMAX
* 上传.psd文件 → 自动识别为PSD
* 上传.ai文件 → 自动识别为AI
* 上传.cad/.dwg文件 → 自动识别为CAD
* 上传.c4d文件 → 自动识别为C4D
* 上传压缩包 → 需手动选择类型
5. 确认素材类型(已自动识别或手动调整)
6. 选择渲染器(仅3DMAX类型显示):CR/VR/其它
7. 点击"开始上传"
8. 等待上传完成(查看进度和速度)
```
#### 下载素材
@ -1379,16 +1494,52 @@ const allowedArchiveTypes = [
'application/x-7z-compressed'
]
// 文件扩展名检查
const allowedExtensions = ['.zip', '.rar', '.7z']
// 文件扩展名检查(支持压缩包和源文件)
const allowedExtensions = [
// 压缩包格式
'.zip', '.rar', '.7z',
// 源文件格式
'.max', // 3DMAX
'.psd', // Photoshop
'.ai', // Adobe Illustrator
'.cad', '.dwg', // AutoCAD
'.c4d' // Cinema 4D
]
// 双重验证
function validateFile(file: File) {
const mimeValid = allowedTypes.includes(file.type)
const extValid = allowedExtensions.some(ext =>
file.name.toLowerCase().endsWith(ext)
)
return mimeValid && extValid
const fileExtension = file.name.substring(
file.name.lastIndexOf('.')
).toLowerCase()
const extValid = allowedExtensions.includes(fileExtension)
if (!extValid) {
throw new Error('不支持的文件格式')
}
return true
}
// 自动识别素材类型
function detectMaterialType(filename: string): string | null {
const ext = filename.substring(
filename.lastIndexOf('.')
).toLowerCase()
const typeMap = {
'.max': '3DMAX',
'.psd': 'PSD',
'.ai': 'AI',
'.cad': 'CAD',
'.dwg': 'CAD',
'.c4d': 'C4D',
'.zip': null, // 需要手动选择
'.rar': null,
'.7z': null,
}
return typeMap[ext] ?? '其他'
}
```

View File

@ -4,7 +4,8 @@ import { authOptions } from '@/lib/auth'
import { prisma } from '@/lib/prisma'
import { uploadToS3 } from '@/lib/s3'
import { processImage, getProcessedFileName } from '@/lib/image-processor'
import { writeFile, mkdir } from 'fs/promises'
import { generatePsdPreview, hasPsdExtension } from '@/lib/psd-processor'
import { writeFile, mkdir, unlink } from 'fs/promises'
import path from 'path'
// Configure route to handle large files
@ -50,11 +51,24 @@ export async function POST(req: NextRequest) {
materialType
})
// Validation
if (!previewImage || !zipFile || !productModel || !categoryName || !sceneName) {
console.log('[Upload] Validation failed - missing fields')
// Check if file is PSD
const isPsdFile = zipFile && hasPsdExtension(zipFile.name)
console.log('[Upload] File type check:', { isPsdFile, fileName: zipFile?.name })
// Validation - preview image is optional for PSD files
if (!zipFile || !productModel || !categoryName || !sceneName) {
console.log('[Upload] Validation failed - missing required fields')
return NextResponse.json(
{ error: '所有字段都是必填的' },
{ error: '产品型号、分类、场景和文件都是必填的' },
{ status: 400 }
)
}
// For non-PSD files, preview image is required
if (!isPsdFile && !previewImage) {
console.log('[Upload] Validation failed - non-PSD file requires preview image')
return NextResponse.json(
{ error: '请上传预览图,或者上传PSD文件以自动生成预览图' },
{ status: 400 }
)
}
@ -77,67 +91,148 @@ export async function POST(req: NextRequest) {
const authorName = user.username
console.log('[Upload] User found:', authorName)
// 验证预览图大小(限制50MB)
const maxPreviewSize = 50 * 1024 * 1024 // 50MB
if (previewImage.size > maxPreviewSize) {
console.log(`[Upload] Preview image too large: ${(previewImage.size / 1024 / 1024).toFixed(2)}MB`)
return NextResponse.json(
{ error: `预览图过大,请使用小于50MB的图片(当前:${(previewImage.size / 1024 / 1024).toFixed(2)}MB)` },
{ status: 400 }
)
}
// 验证预览图格式
const allowedImageTypes = ['image/jpeg', 'image/jpg', 'image/png', 'image/webp', 'image/gif']
if (!allowedImageTypes.includes(previewImage.type)) {
console.log(`[Upload] Invalid image type: ${previewImage.type}`)
return NextResponse.json(
{ error: `不支持的图片格式:${previewImage.type},请使用 JPG、PNG、WebP 或 GIF 格式` },
{ status: 400 }
)
}
// 处理预览图:压缩到300KB以内,转换为webP,缩放到600px
console.log(`[Upload] Processing image... (${previewImage.type}, ${(previewImage.size / 1024).toFixed(2)}KB)`)
let processedBuffer: Buffer
try {
const originalBuffer = Buffer.from(await previewImage.arrayBuffer())
processedBuffer = await processImage(originalBuffer, {
maxSizeKB: 300,
maxWidth: 600,
format: 'webp',
})
console.log('[Upload] Image processed successfully')
} catch (imageError) {
console.error('[Upload] Image processing error:', imageError)
return NextResponse.json(
{ error: `图片处理失败,请确保图片文件未损坏且格式正确。错误:${imageError instanceof Error ? imageError.message : '未知错误'}` },
{ status: 500 }
)
let previewFileName: string
let previewUrl: string
// Handle preview image generation
if (isPsdFile && !previewImage) {
// Auto-generate preview from PSD file
console.log('[Upload] PSD file detected, auto-generating preview...')
try {
// Save PSD file temporarily
const uploadDir = process.env.UPLOAD_DIR || './uploads'
await mkdir(uploadDir, { recursive: true })
const tempPsdFileName = `temp-psd-${Date.now()}-${zipFile.name}`
const tempPsdPath = path.join(uploadDir, tempPsdFileName)
const psdBuffer = Buffer.from(await zipFile.arrayBuffer())
await writeFile(tempPsdPath, psdBuffer)
console.log('[Upload] Temporary PSD saved:', tempPsdPath)
// Generate preview image
const tempPreviewPath = path.join(uploadDir, `temp-preview-${Date.now()}.jpg`)
const result = await generatePsdPreview(tempPsdPath, tempPreviewPath, 800)
if (!result.success || !result.previewPath) {
// Clean up temp files
await unlink(tempPsdPath).catch(() => {})
await unlink(tempPreviewPath).catch(() => {})
console.error('[Upload] PSD preview generation failed:', result.error)
return NextResponse.json(
{ error: `PSD预览图生成失败:${result.error}。请手动上传预览图。` },
{ status: 500 }
)
}
console.log('[Upload] PSD preview generated successfully')
// Read the generated preview and process it
const { readFile } = await import('fs/promises')
const generatedBuffer = await readFile(tempPreviewPath)
// Process the generated preview (compress and optimize)
processedBuffer = await processImage(generatedBuffer, {
maxSizeKB: 300,
maxWidth: 600,
format: 'webp',
})
// Clean up temporary files
await unlink(tempPsdPath).catch(() => {})
await unlink(tempPreviewPath).catch(() => {})
console.log('[Upload] Temporary files cleaned up')
// Upload to S3
previewFileName = `preview-${Date.now()}-generated.webp`
previewUrl = await uploadToS3(processedBuffer, previewFileName, 'image/webp')
console.log('[Upload] Auto-generated preview uploaded to S3:', previewUrl)
} catch (psdError) {
console.error('[Upload] PSD processing error:', psdError)
return NextResponse.json(
{ error: `PSD文件处理失败:${psdError instanceof Error ? psdError.message : '未知错误'}。请手动上传预览图。` },
{ status: 500 }
)
}
} else {
// Use provided preview image
if (!previewImage) {
return NextResponse.json(
{ error: '请上传预览图' },
{ status: 400 }
)
}
// 验证预览图大小(限制50MB)
const maxPreviewSize = 50 * 1024 * 1024 // 50MB
if (previewImage.size > maxPreviewSize) {
console.log(`[Upload] Preview image too large: ${(previewImage.size / 1024 / 1024).toFixed(2)}MB`)
return NextResponse.json(
{ error: `预览图过大,请使用小于50MB的图片(当前:${(previewImage.size / 1024 / 1024).toFixed(2)}MB)` },
{ status: 400 }
)
}
// 验证预览图格式
const allowedImageTypes = ['image/jpeg', 'image/jpg', 'image/png', 'image/webp', 'image/gif']
if (!allowedImageTypes.includes(previewImage.type)) {
console.log(`[Upload] Invalid image type: ${previewImage.type}`)
return NextResponse.json(
{ error: `不支持的图片格式:${previewImage.type},请使用 JPG、PNG、WebP 或 GIF 格式` },
{ status: 400 }
)
}
// 处理预览图:压缩到300KB以内,转换为webP,缩放到600px
console.log(`[Upload] Processing image... (${previewImage.type}, ${(previewImage.size / 1024).toFixed(2)}KB)`)
try {
const originalBuffer = Buffer.from(await previewImage.arrayBuffer())
processedBuffer = await processImage(originalBuffer, {
maxSizeKB: 300,
maxWidth: 600,
format: 'webp',
})
console.log('[Upload] Image processed successfully')
} catch (imageError) {
console.error('[Upload] Image processing error:', imageError)
return NextResponse.json(
{ error: `图片处理失败,请确保图片文件未损坏且格式正确。错误:${imageError instanceof Error ? imageError.message : '未知错误'}` },
{ status: 500 }
)
}
// Upload processed preview image to S3
console.log('[Upload] Uploading image to S3...')
const processedFileName = getProcessedFileName(previewImage.name)
previewFileName = `preview-${Date.now()}-${processedFileName}`
previewUrl = await uploadToS3(processedBuffer, previewFileName, 'image/webp')
console.log('[Upload] Image uploaded to S3:', previewUrl)
}
// Upload processed preview image to S3
console.log('[Upload] Uploading image to S3...')
const processedFileName = getProcessedFileName(previewImage.name)
const previewFileName = `preview-${Date.now()}-${processedFileName}`
const previewUrl = await uploadToS3(
processedBuffer,
previewFileName,
'image/webp'
)
console.log('[Upload] Image uploaded to S3:', previewUrl)
// Save zip file locally
console.log('[Upload] Saving zip file locally...')
// Save zip/PSD file locally
console.log('[Upload] Saving file locally...')
const uploadDir = process.env.UPLOAD_DIR || './uploads'
await mkdir(uploadDir, { recursive: true })
const zipFileName = `zip-${Date.now()}-${zipFile.name}`
const zipFilePath = path.join(uploadDir, zipFileName)
const zipBuffer = Buffer.from(await zipFile.arrayBuffer())
// If it's a PSD file, we already have the buffer from above
// Otherwise, read the buffer now
let zipBuffer: Buffer
if (isPsdFile && !previewImage) {
// Reread the PSD file for storage (we cleaned up the temp file already)
zipBuffer = Buffer.from(await zipFile.arrayBuffer())
} else {
zipBuffer = Buffer.from(await zipFile.arrayBuffer())
}
await writeFile(zipFilePath, zipBuffer)
const zipFileSize = BigInt(zipBuffer.length) // 记录文件大小(字节)
console.log('[Upload] Zip file saved:', zipFilePath, `(${(Number(zipFileSize) / 1024 / 1024).toFixed(2)} MB)`)
console.log('[Upload] File saved:', zipFilePath, `(${(Number(zipFileSize) / 1024 / 1024).toFixed(2)} MB)`)
// Create material record
console.log('[Upload] Creating database record...')

View File

@ -272,8 +272,18 @@ export default function UploadModal({ onClose, onSuccess }: UploadModalProps) {
e.preventDefault()
setError('')
if (!previewImage || !zipFile || !productModel || !categoryName || !sceneName) {
setError('请填写所有字段')
// Check if file is PSD
const isPsdFile = zipFile && zipFile.name.toLowerCase().endsWith('.psd')
// Validation - preview is optional for PSD files
if (!zipFile || !productModel || !categoryName || !sceneName) {
setError('请填写所有必填字段')
return
}
// For non-PSD files, preview is required
if (!isPsdFile && !previewImage) {
setError('请上传预览图,或者上传PSD文件以自动生成预览图')
return
}
@ -285,7 +295,10 @@ export default function UploadModal({ onClose, onSuccess }: UploadModalProps) {
try {
const formData = new FormData()
formData.append('previewImage', previewImage)
// Only append preview image if it exists
if (previewImage) {
formData.append('previewImage', previewImage)
}
formData.append('zipFile', zipFile)
formData.append('productModel', productModel)
formData.append('categoryName', categoryName)
@ -451,7 +464,7 @@ export default function UploadModal({ onClose, onSuccess }: UploadModalProps) {
{/* 预览图片 */}
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
预览图片 *
预览图片 {zipFile?.name.toLowerCase().endsWith('.psd') ? '(可选)' : '*'}
</label>
<div className="flex items-center space-x-4">
{previewUrl ? (
@ -487,6 +500,11 @@ export default function UploadModal({ onClose, onSuccess }: UploadModalProps) {
</button>
<p className="text-xs text-gray-500 dark:text-gray-400 mt-2">
支持 JPG、PNG、WebP 等格式
{zipFile?.name.toLowerCase().endsWith('.psd') && (
<span className="block text-green-600 dark:text-green-400 mt-1">
💡 PSD文件可自动生成预览图,无需手动上传
</span>
)}
</p>
</div>
</div>

127
lib/psd-processor.ts Normal file
View File

@ -0,0 +1,127 @@
import { readPsd } from 'ag-psd'
import sharp from 'sharp'
import fs from 'fs/promises'
interface PsdProcessResult {
success: boolean
previewPath?: string
width?: number
height?: number
error?: string
}
/**
* 从PSD文件生成预览图
* @param psdFilePath PSD文件路径
* @param outputPath 输出预览图路径
* @param maxSize 最大尺寸(宽或高),默认800px
* @returns 处理结果
*/
export async function generatePsdPreview(
psdFilePath: string,
outputPath: string,
maxSize: number = 800
): Promise<PsdProcessResult> {
try {
// 读取PSD文件
const psdBuffer = await fs.readFile(psdFilePath)
// 解析PSD
const psd = readPsd(psdBuffer, {
skipLayerImageData: true, // 跳过图层数据,只读取合成图像
skipThumbnail: false
})
if (!psd) {
return {
success: false,
error: 'Failed to parse PSD file'
}
}
// 获取PSD的宽高
const width = psd.width || 0
const height = psd.height || 0
if (width === 0 || height === 0) {
return {
success: false,
error: 'Invalid PSD dimensions'
}
}
// 如果PSD有缩略图,优先使用缩略图
if (psd.imageData) {
const imageData = psd.imageData
const channels = 4 // RGBA
// 将PSD图像数据转换为Buffer
const rawBuffer = Buffer.from(imageData.data)
// 使用Sharp处理图像
await sharp(rawBuffer, {
raw: {
width: imageData.width,
height: imageData.height,
channels: channels
}
})
.resize(maxSize, maxSize, {
fit: 'inside',
withoutEnlargement: true
})
.jpeg({
quality: 90,
mozjpeg: true
})
.toFile(outputPath)
return {
success: true,
previewPath: outputPath,
width,
height
}
}
return {
success: false,
error: 'No image data found in PSD file'
}
} catch (error) {
console.error('Error generating PSD preview:', error)
return {
success: false,
error: error instanceof Error ? error.message : 'Unknown error'
}
}
}
/**
* 检查文件是否为PSD格式
* @param filePath 文件路径
* @returns 是否为PSD文件
*/
export async function isPsdFile(filePath: string): Promise<boolean> {
try {
const buffer = await fs.readFile(filePath, { encoding: null })
// PSD文件签名:8BPS
return buffer.length > 4 &&
buffer[0] === 0x38 && // '8'
buffer[1] === 0x42 && // 'B'
buffer[2] === 0x50 && // 'P'
buffer[3] === 0x53 // 'S'
} catch {
return false
}
}
/**
* 从文件扩展名判断是否为PSD
* @param filename 文件名
* @returns 是否为PSD文件
*/
export function hasPsdExtension(filename: string): boolean {
return filename.toLowerCase().endsWith('.psd')
}

37
package-lock.json generated
View File

@ -11,6 +11,7 @@
"@aws-sdk/client-s3": "^3.645.0",
"@aws-sdk/s3-request-presigner": "^3.645.0",
"@prisma/client": "^5.19.0",
"ag-psd": "^28.4.1",
"bcryptjs": "^2.4.3",
"clsx": "^2.1.0",
"date-fns": "^3.6.0",
@ -3341,6 +3342,16 @@
"acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
}
},
"node_modules/ag-psd": {
"version": "28.4.1",
"resolved": "https://registry.npmjs.org/ag-psd/-/ag-psd-28.4.1.tgz",
"integrity": "sha512-Cf5xQHScL8VUH2X2jjYbjv6NrZIJ7BkKNw5/FCE8wGv94yLQ4NyfRdvTXB0JJ0qC5IY1s+kVKJbA6JpO2n4hBw==",
"license": "MIT",
"dependencies": {
"base64-js": "1.5.1",
"pako": "2.1.0"
}
},
"node_modules/ajv": {
"version": "6.12.6",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
@ -3693,6 +3704,26 @@
"dev": true,
"license": "MIT"
},
"node_modules/base64-js": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
"integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "MIT"
},
"node_modules/baseline-browser-mapping": {
"version": "2.8.16",
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.16.tgz",
@ -6725,6 +6756,12 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/pako": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/pako/-/pako-2.1.0.tgz",
"integrity": "sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==",
"license": "(MIT AND Zlib)"
},
"node_modules/parent-module": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",

View File

@ -16,6 +16,7 @@
"@aws-sdk/client-s3": "^3.645.0",
"@aws-sdk/s3-request-presigner": "^3.645.0",
"@prisma/client": "^5.19.0",
"ag-psd": "^28.4.1",
"bcryptjs": "^2.4.3",
"clsx": "^2.1.0",
"date-fns": "^3.6.0",

61372
聊天7.md Normal file

File diff suppressed because it is too large Load Diff

61791
聊天记录9.md Normal file

File diff suppressed because it is too large Load Diff