优化图片压缩技术:避免双重压缩,保持JPG/PNG格式

- 客户端压缩:保持原格式(JPG保持JPG,PNG保持PNG),不再强制转换为WebP
- 提高压缩质量:最低质量从0.6提高到0.90,避免画质损失
- 启用高质量图像平滑:减少锯齿,提升画质
- 服务器端智能优化:使用MozJPEG和PNG优化算法
- 避免双重压缩:小文件(<3MB)跳过服务器端优化,只有优化效果明显(>5%)才使用
- 优化策略:优先缩小尺寸而不是降低质量,留余量给服务器端优化

技术改进:
- JPEG: MozJPEG编码器,质量92%,渐进式,优化扫描
- PNG: 压缩级别6,自适应过滤,调色板优化
- 智能判断:避免不必要的双重压缩,保护画质
This commit is contained in:
Your Name 2026-01-02 20:46:01 +08:00
parent 4c0bc92bbf
commit fea5e0fac5
9 changed files with 1230 additions and 18 deletions

View File

@ -4,6 +4,83 @@ import { z } from "zod";
import { auth } from "@/app/(auth)/auth"; import { auth } from "@/app/(auth)/auth";
import { uploadFile } from "@/lib/storage/service"; import { uploadFile } from "@/lib/storage/service";
/**
* 使用Sharp智能优化图片(保持原格式)
* 策略:避免双重压缩,只在需要时优化
* - 如果文件已经很小(<3MB),跳过优化(客户端已处理)
* - 如果文件较大,使用MozJPEG/PNG优化算法进一步优化
*/
async function optimizeImage(buffer: Buffer, mimeType: string): Promise<Buffer> {
try {
const fileSizeMB = buffer.length / 1024 / 1024;
const OPTIMIZE_THRESHOLD_MB = 3; // 3MB阈值:小于此值不优化(避免双重压缩)
// ✅ 智能判断:如果文件已经很小,跳过优化(避免双重压缩)
if (fileSizeMB < OPTIMIZE_THRESHOLD_MB) {
console.log(`[Upload] 文件已较小(${fileSizeMB.toFixed(2)}MB < ${OPTIMIZE_THRESHOLD_MB}MB),跳过优化,避免双重压缩`);
return buffer;
}
const sharp = await import("sharp");
const image = sharp.default(buffer);
if (mimeType === 'image/jpeg' || mimeType === 'image/jpg') {
// ✅ 使用MozJPEG编码器,高质量压缩
// 注意:这里使用MozJPEG重新编码,使用更好的算法,而不是降低质量
const optimized = await image
.jpeg({
quality: 92, // 高质量(92%质量,画质优秀)
mozjpeg: true, // 使用MozJPEG编码器(比标准JPEG压缩率高15-25%,但质量更好)
progressive: true, // 渐进式JPEG,加载体验更好
optimizeScans: true, // 优化扫描
trellisQuantisation: true, // 网格量化,更好的压缩
overshootDeringing: true, // 过冲去振铃,减少伪影
optimizeCoding: true, // 优化编码
})
.toBuffer();
// 只有优化后文件明显更小才使用(避免不必要的处理)
const reductionPercent = ((1 - optimized.length / buffer.length) * 100);
if (reductionPercent > 5) { // 只有减少超过5%才使用
console.log(`[Upload] JPEG优化: ${buffer.length} → ${optimized.length} bytes (减少${reductionPercent.toFixed(1)}%)`);
return optimized;
} else {
console.log(`[Upload] JPEG优化效果不明显(${reductionPercent.toFixed(1)}%),使用原图`);
return buffer;
}
} else if (mimeType === 'image/png') {
// ✅ PNG优化压缩(无损优化)
const metadata = await image.metadata();
const optimized = await image
.png({
compressionLevel: 6, // 压缩级别(0-9,6是平衡点)
adaptiveFiltering: true, // 自适应过滤,减少文件大小
palette: !metadata.hasAlpha, // 无透明通道时使用调色板模式
quality: 100, // PNG质量(100=无损)
effort: 6, // 压缩努力程度(0-10,6是平衡点)
})
.toBuffer();
// 只有优化后文件明显更小才使用
const reductionPercent = ((1 - optimized.length / buffer.length) * 100);
if (reductionPercent > 5) { // 只有减少超过5%才使用
console.log(`[Upload] PNG优化: ${buffer.length} → ${optimized.length} bytes (减少${reductionPercent.toFixed(1)}%)`);
return optimized;
} else {
console.log(`[Upload] PNG优化效果不明显(${reductionPercent.toFixed(1)}%),使用原图`);
return buffer;
}
}
// 其他格式返回原图
return buffer;
} catch (error) {
console.error("[Upload] 图片优化失败,使用原图:", error);
// 优化失败时返回原图,不影响上传流程
return buffer;
}
}
// Use Blob instead of File since File is not available in Node.js environment // Use Blob instead of File since File is not available in Node.js environment
const FileSchema = z.object({ const FileSchema = z.object({
file: z file: z
@ -68,8 +145,27 @@ export async function POST(request: Request) {
console.log("[Upload API] File buffer size:", fileBuffer.byteLength); console.log("[Upload API] File buffer size:", fileBuffer.byteLength);
try { try {
// ✅ 使用Sharp优化图片(保持原格式)
let optimizedBuffer: Buffer = Buffer.from(fileBuffer);
// ✅ 优化原图(保持JPG/PNG格式)
// WebP格式不优化(用于预览图,保持原样)
if (file.type === 'image/jpeg' || file.type === 'image/jpg' || file.type === 'image/png') {
optimizedBuffer = await optimizeImage(optimizedBuffer, file.type);
} else if (file.type === 'image/webp') {
// WebP格式不优化(用于预览图),直接使用
console.log(`[Upload] WebP格式(预览图),跳过优化,直接上传`);
}
// 将Buffer转换为ArrayBuffer用于上传
const optimizedArrayBuffer = optimizedBuffer.buffer.slice(
optimizedBuffer.byteOffset,
optimizedBuffer.byteOffset + optimizedBuffer.byteLength
) as ArrayBuffer;
// 使用统一的存储服务(支持数据库配置的 S3、环境变量 S3 或 Vercel Blob) // 使用统一的存储服务(支持数据库配置的 S3、环境变量 S3 或 Vercel Blob)
const data = await uploadFile(filename, fileBuffer); // 使用优化后的buffer上传
const data = await uploadFile(filename, optimizedArrayBuffer);
console.log("[Upload API] Upload successful:", data.url); console.log("[Upload API] Upload successful:", data.url);
return NextResponse.json(data); return NextResponse.json(data);

View File

@ -238,16 +238,25 @@ function PureMultimodalInput({
resetHeight, resetHeight,
]); ]);
// 压缩图片函数 // 压缩图片函数(保持原格式:JPG保持JPG,PNG保持PNG)
const compressImage = useCallback(async (file: File, maxSizeMB = 8): Promise<File> => { const compressImage = useCallback(async (file: File, maxSizeMB = 8): Promise<File> => {
// 如果不是图片,直接返回 // 如果不是图片,直接返回
if (!file.type.startsWith('image/')) { if (!file.type.startsWith('image/')) {
return file; return file;
} }
// 如果已经是 WebP 且小于限制,直接返回 // ✅ 保持原格式:只处理JPG和PNG,其他格式(如WebP)直接返回
if (file.type === 'image/webp' && file.size <= maxSizeMB * 1024 * 1024) { const isJPEG = file.type === 'image/jpeg' || file.type === 'image/jpg';
const isPNG = file.type === 'image/png';
// 如果不是JPG/PNG,直接返回(WebP等格式不压缩)
if (!isJPEG && !isPNG) {
if (file.size <= maxSizeMB * 1024 * 1024) {
return file; return file;
}
// 如果WebP等格式太大,也返回(让服务器端处理或提示用户)
console.log(`[压缩] 非JPG/PNG格式,跳过压缩: ${file.type}`);
return file;
} }
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
@ -286,16 +295,33 @@ function PureMultimodalInput({
canvas.width = newWidth; canvas.width = newWidth;
canvas.height = newHeight; canvas.height = newHeight;
const ctx = canvas.getContext('2d'); const ctx = canvas.getContext('2d', {
alpha: isPNG, // PNG需要透明通道
desynchronized: false, // 同步渲染,保证质量
willReadFrequently: false,
});
if (!ctx) { if (!ctx) {
reject(new Error('无法创建 Canvas 上下文')); reject(new Error('无法创建 Canvas 上下文'));
return; return;
} }
// ✅ 启用高质量图像平滑,减少锯齿
ctx.imageSmoothingEnabled = true;
ctx.imageSmoothingQuality = 'high';
ctx.drawImage(img, 0, 0, newWidth, newHeight); ctx.drawImage(img, 0, 0, newWidth, newHeight);
// 尝试不同的质量等级,直到文件大小小于限制 // ✅ 保持原格式压缩
let quality = 0.9; // 策略:优先缩小尺寸,尽量保持高质量,让服务器端做最终优化
const outputFormat = isPNG ? 'image/png' : 'image/jpeg';
const TARGET_SIZE_MB = maxSizeMB * 0.9; // 目标大小(留10%余量给服务器端优化)
// 如果尺寸已经调整过,使用高质量直接输出(让服务器端优化)
const wasResized = newWidth !== width || newHeight !== height;
let quality = wasResized ? 0.95 : 0.92; // 如果已缩小尺寸,使用更高质量
const minQuality = 0.90; // 最低质量(提高,避免画质损失)
const tryCompress = () => { const tryCompress = () => {
canvas.toBlob( canvas.toBlob(
(blob) => { (blob) => {
@ -304,26 +330,46 @@ function PureMultimodalInput({
return; return;
} }
console.log(`[压缩] 格式: image/webp, 质量: ${quality}, 大小: ${(blob.size / 1024 / 1024).toFixed(2)}MB`); console.log(`[压缩] 格式: ${outputFormat}, 质量: ${quality}, 大小: ${(blob.size / 1024 / 1024).toFixed(2)}MB`);
// 目标 maxSizeMB (默认8MB) // 目标 TARGET_SIZE_MB(留余量给服务器端优化)
// 如果还是太大且质量可以降低,继续压缩 // 如果还是太大且质量可以降低,继续压缩
if (blob.size > maxSizeMB * 1024 * 1024 && quality > 0.6) { // 最低质量 0.6 if (blob.size > TARGET_SIZE_MB * 1024 * 1024 && quality > minQuality) {
quality -= 0.1; quality -= 0.02; // 每次减少0.02(更精细的调整)
tryCompress(); tryCompress();
} else if (blob.size > TARGET_SIZE_MB * 1024 * 1024 && quality <= minQuality) {
// 如果质量已到最低还是太大,缩小尺寸而不是继续降低质量
if (newWidth > 1000 || newHeight > 1000) {
newWidth = Math.floor(newWidth * 0.95);
newHeight = Math.floor(newHeight * 0.95);
canvas.width = newWidth;
canvas.height = newHeight;
ctx.imageSmoothingEnabled = true;
ctx.imageSmoothingQuality = 'high';
ctx.drawImage(img, 0, 0, newWidth, newHeight);
quality = 0.95; // 重置质量(缩小尺寸后使用高质量)
tryCompress();
} else {
// 已经很小了,使用当前结果
const compressedFile = new File([blob], file.name, {
type: outputFormat, // 保持原格式
lastModified: Date.now(),
});
console.log(`[压缩] 原始: ${(file.size / 1024 / 1024).toFixed(2)}MB → 压缩后: ${(compressedFile.size / 1024 / 1024).toFixed(2)}MB`);
resolve(compressedFile);
}
} else { } else {
// 创建新文件,强制使用 .webp 扩展名 // 大小合适,使用当前结果(让服务器端做最终优化)
const newName = file.name.replace(/\.[^/.]+$/, "") + ".webp"; const compressedFile = new File([blob], file.name, {
const compressedFile = new File([blob], newName, { type: outputFormat, // 保持原格式
type: "image/webp",
lastModified: Date.now(), lastModified: Date.now(),
}); });
console.log(`[压缩] 原始: ${(file.size / 1024 / 1024).toFixed(2)}MB → 压缩后: ${(compressedFile.size / 1024 / 1024).toFixed(2)}MB`); console.log(`[压缩] 原始: ${(file.size / 1024 / 1024).toFixed(2)}MB → 压缩后: ${(compressedFile.size / 1024 / 1024).toFixed(2)}MB (将交由服务器端进一步优化)`);
resolve(compressedFile); resolve(compressedFile);
} }
}, },
"image/webp", outputFormat, // 保持原格式
quality isPNG ? undefined : quality // PNG不需要quality参数
); );
}; };

View File

@ -82,3 +82,5 @@ echo ""

View File

@ -0,0 +1,132 @@
# 图片压缩优化方案说明
## 问题分析
### 双重压缩问题
- **客户端压缩**:使用Canvas API,质量0.88-0.92
- **服务器端优化**:使用Sharp MozJPEG,质量92%
- **风险**:如果客户端已经压缩,服务器端再压缩,可能造成双重压缩,导致画质损失
---
## 优化方案
### 策略:避免双重压缩,智能优化
#### 1. 客户端压缩策略(`components/multimodal-input.tsx`)
**优化点**:
- ✅ **优先缩小尺寸**:如果图片太大,先缩小尺寸而不是降低质量
- ✅ **提高质量下限**:最低质量从0.6提高到0.90,避免画质损失
- ✅ **留余量给服务器**:目标大小设为`maxSizeMB * 0.9`,留10%余量给服务器端优化
- ✅ **尺寸调整后使用高质量**:如果已缩小尺寸,使用0.95质量,让服务器端做最终优化
**逻辑**:
```
如果图片 > 5MB 或 > 4K分辨率
↓
优先缩小尺寸(保持高质量)
↓
如果还是太大,才降低质量(最低0.90)
↓
输出文件(留余量给服务器端优化)
```
#### 2. 服务器端优化策略(`app/(chat)/api/files/upload/route.ts`)
**优化点**:
- ✅ **智能判断**:如果文件 < 3MB,跳过优化(避免双重压缩)
- ✅ **效果检查**:只有优化后文件减少 > 5% 才使用,否则返回原图
- ✅ **使用更好的编码器**:MozJPEG(更好的算法,而不是降低质量)
**逻辑**:
```
接收文件
↓
检查文件大小
├─ < 3MB → 跳过优化(客户端已处理)
└─ ≥ 3MB → 使用MozJPEG/PNG优化
↓
检查优化效果
├─ 减少 > 5% → 使用优化结果
└─ 减少 ≤ 5% → 使用原图
```
---
## 工作流程
### 场景1:小文件(< 5MB,< 4K)
```
客户端:直接返回原图(免检通道)
服务器:< 3MB,跳过优化
结果:原图直接存储,无压缩
```
### 场景2:中等文件(5-8MB,< 4K)
```
客户端:可能轻微压缩(质量0.90-0.95)
服务器:< 3MB,跳过优化
结果:客户端压缩后存储,无双重压缩
```
### 场景3:大文件(> 8MB 或 > 4K)
```
客户端:缩小尺寸 + 高质量压缩(质量0.90-0.95)
↓
服务器:≥ 3MB,使用MozJPEG优化
↓
检查优化效果
├─ 减少 > 5% → 使用优化结果
└─ 减少 ≤ 5% → 使用原图(避免双重压缩)
```
---
## 优化效果
### 避免双重压缩
- ✅ 小文件(< 3MB):服务器端跳过优化
- ✅ 优化效果检查:只有明显优化才使用
- ✅ 客户端提高质量下限:0.6 → 0.90
### 画质保护
- ✅ 客户端优先缩小尺寸,而不是降低质量
- ✅ 服务器端使用更好的编码器(MozJPEG),而不是降低质量
- ✅ 智能判断,避免不必要的处理
### 文件大小
- ✅ 客户端留10%余量给服务器端优化
- ✅ 服务器端使用MozJPEG(压缩率高15-25%)
- ✅ 双重优化效果更好(但避免双重压缩)
---
## 技术细节
### 客户端压缩参数
- **起始质量**:0.92(尺寸未调整)或 0.95(尺寸已调整)
- **最低质量**:0.90(避免画质损失)
- **目标大小**:`maxSizeMB * 0.9`(留余量)
- **图像平滑**:`high`(减少锯齿)
### 服务器端优化参数
- **优化阈值**:3MB(小于此值跳过优化)
- **效果阈值**:5%(只有减少超过5%才使用)
- **JPEG质量**:92%(高质量)
- **PNG质量**:100%(无损)
---
## 预期效果
| 文件大小 | 客户端处理 | 服务器端处理 | 最终结果 |
|---------|-----------|-------------|---------|
| < 3MB | 原图或轻微压缩 | 跳过优化 | 高质量,无双重压缩 |
| 3-8MB | 高质量压缩 | 智能优化 | 高质量,单次优化 |
| > 8MB | 缩小尺寸+高质量 | MozJPEG优化 | 高质量,双重优化(但避免双重压缩) |
---
**总结**:通过智能判断和效果检查,避免不必要的双重压缩,同时保证画质和文件大小优化。

155
图片压缩冲突分析.md Normal file
View File

@ -0,0 +1,155 @@
# 图片压缩功能冲突分析
## 当前压缩流程
### 1. 客户端压缩 (`components/multimodal-input.tsx`)
- **函数**: `compressImage`
- **作用**: 在浏览器端压缩图片
- **输出格式**: **强制转换为WebP** (第316-320行)
- **质量范围**: 0.6 - 0.9 (最低质量0.6,可能导致画质损失)
- **处理逻辑**:
- 如果图片 < 5MB 且 < 4K分辨率,直接返回原图
- 否则压缩并转换为WebP格式
### 2. 服务器端优化 (`app/(chat)/api/files/upload/route.ts`)
- **函数**: `optimizeImage`
- **作用**: 在服务器端优化图片
- **支持格式**: **仅JPEG和PNG** (第126行)
- **处理逻辑**:
- JPEG: 使用MozJPEG编码器,质量92%
- PNG: 自适应过滤,压缩级别6
- **WebP: 不处理,直接返回原图**
---
## ⚠️ 发现的冲突问题
### 问题1: 格式不匹配
- **客户端**: JPG/PNG → **WebP** (强制转换)
- **服务器端**: 只优化 **JPEG/PNG**,不优化WebP
- **结果**: 客户端转换的WebP图片在服务器端不会被优化
### 问题2: 双重处理但效果不佳
- 客户端已经压缩(可能损失画质)
- 服务器端无法进一步优化WebP
- 最终结果:画质可能已经受损,且文件大小未进一步优化
### 问题3: 格式要求冲突
- 用户要求:**保持JPG和PNG格式不变**
- 当前实现:客户端强制转换为WebP
- **冲突**: 不符合用户需求
---
## ✅ 解决方案
### 方案A: 修改客户端压缩逻辑(推荐)
**修改客户端代码,保持原格式**:
```typescript
// 修改 compressImage 函数
const compressImage = useCallback(async (file: File, maxSizeMB = 8): Promise<File> => {
// ... 现有代码 ...
// ✅ 保持原格式,不转换为WebP
const outputFormat = file.type === 'image/png' ? 'image/png' : 'image/jpeg';
const outputQuality = file.type === 'image/png' ? undefined : 0.92; // PNG不需要quality
canvas.toBlob(
(blob) => { /* ... */ },
outputFormat, // 保持原格式
outputQuality
);
});
```
**优点**:
- 符合用户需求(保持JPG/PNG格式)
- 服务器端可以进一步优化
- 双重优化效果更好
### 方案B: 服务器端也支持WebP优化
**扩展服务器端优化函数**:
```typescript
async function optimizeImage(buffer: Buffer, mimeType: string): Promise<Buffer> {
// ... 现有JPEG/PNG代码 ...
// ✅ 新增WebP优化
else if (mimeType === 'image/webp') {
const optimized = await image
.webp({
quality: 90,
effort: 6,
smartSubsample: true,
nearLossless: true,
})
.toBuffer();
return optimized;
}
}
```
**优点**:
- 可以优化客户端转换的WebP
- 不需要修改客户端代码
**缺点**:
- 不符合用户"保持JPG/PNG格式"的要求
### 方案C: 跳过客户端压缩,仅使用服务器端优化
**修改客户端代码**:
```typescript
const compressImage = useCallback(async (file: File, maxSizeMB = 8): Promise<File> => {
// 如果图片较小,直接返回原图
if (file.size <= maxSizeMB * 1024 * 1024) {
return file; // 不压缩,让服务器端优化
}
// 只处理超大图片(缩小尺寸,保持格式)
// ... 只缩小尺寸,不改变格式和质量
});
```
**优点**:
- 画质最好(服务器端Sharp优化更专业)
- 保持原格式
- 避免双重压缩
---
## 📊 推荐方案
**推荐使用方案A + 方案C的组合**:
1. **修改客户端压缩逻辑**:
- 保持原格式(JPG保持JPG,PNG保持PNG)
- 提高最低质量(0.6 → 0.88)
- 只处理超大图片(>8MB或>4K)
2. **服务器端优化**:
- 继续优化JPEG和PNG
- 使用MozJPEG和PNG优化算法
3. **工作流程**:
```
用户上传 → 客户端检查大小 →
如果太大:客户端压缩(保持格式) →
服务器端进一步优化(MozJPEG/PNG优化) →
存储
```
---
## 🔍 当前状态总结
| 项目 | 客户端 | 服务器端 | 冲突 |
|------|--------|----------|------|
| 输出格式 | WebP | JPEG/PNG | ✅ 是 |
| 质量范围 | 0.6-0.9 | 92% (JPEG) | ⚠️ 客户端质量低 |
| 格式保持 | ❌ 强制转换 | ✅ 保持原格式 | ✅ 是 |
| 优化效果 | 基础压缩 | 专业优化 | ⚠️ WebP无法优化 |
**结论**: 存在冲突,需要修改客户端代码以保持原格式。

View File

@ -0,0 +1,157 @@
注意事项
接口返回的链接有效期为 1 小时,请及时下载存储。
图片格式要求
格式 分辨率 大小
jpg, jpeg, bmp, png, webp, tiff, tif, bitmap, raw, rgb, jfif, lzw 最大 4096x4096 最大20MB
快速开始
异步
不同调用方式的区别&优缺点?
Image URL
Image File
cURL
PHP
Java
NodeJs
Python
C#
Swift
Go
#Create a task.
curl -k 'https://techsz.aoscdn.com/api/tasks/visual/scale' \
-H 'X-API-KEY: YOUR_API_KEY' \
-F 'sync=0' \
-F 'image_url=YOU_IMG_URL'
#Get the cutout result
#Polling requests using the following methods 1. The polling interval is set to 1 second, 2. The polling time does not exceed 30 seconds
curl -k 'https://techsz.aoscdn.com/api/tasks/visual/scale/{task_id}' \
-H 'X-API-KEY: YOUR_API_KEY' \
在线调试
POST
/api/tasks/visual/scale
创建图片变清晰任务
创建图片变清晰任务
图片大小: 不超过 20 MB
图片来源:文件上传(二进制)或从 URL 下载
消耗:每次成功调用,最长边≤2048px则消耗2算粒,2048px<最长边≤4096px则消耗3算粒。
Parameters
Name Description
X-API-KEY *
string
(header)
API Key
Request body
image_file
string($binary)
源图像文件(二进制)。(如果此参数存在,则其他图像源参数必须为空)
Send empty value
image_url
string
源图像URL。(如果存在此参数,则其他图像源参数必须为空)
Send empty value
sync
integer
是否等待结果就绪并立即返回(默认值:0)。如果设为1,API将会等待结果准备好后立即返回。如果设为0,API将返回 task_id,稍后可通过 /{task_id} 端点获取结果。请注意,请求提交后,结果最多只会保留1个小时。
Send empty value
type
string
修复类型:
"clean" = 通用放大清晰化。
"face" = 人像放大清晰化。(1张图片中,最多仅能同时变清晰10个人)
Send empty value
scale_factor
integer
放大倍数:
空 = 自动缩放。(10,512],放大4倍;(512,1024],放大2倍;(1024,4096],保持原大小。
1 = 不放大。(长边范围需要在区间[10,4096]内)
2 = 2倍放大。(长边范围需要在区间[10,1024]内)
4 = 4倍放大。(长边范围需要在区间[10,512]内)
Send empty value
return_type
integer
1 = 返回图片的下载URL。
2 = 以base64字符串形式返回图片。
3 = 以二进制流的形式返回图片(当前只支持同步接口)。
Send empty value
Execute
Responses
Code Description
200
任务创建成功。
Media type
Controls Accept header.
Example Value
Schema
{
"status": 200,
"message": "success",
"data": {
"task_id": "xxxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
}
}
400
客户端参数传递错误。 请检查参数是否缺失或值是否正确
401
认证失败。 请检查X-API-KEY是否正确或者服务是否开通
429
请求频率超出QPS限制(默认QPS为2)。请放缓请求速率,或者联系商务提升QPS限制
更多详见状态码说明
GET
/api/tasks/visual/scale/{task_id}
获取图片变清晰结果
获取图片变清晰结果。
Url中的参数 task_id 由创建图片变清晰任务后返回。
Parameters
Name Description
X-API-KEY *
string
(header)
API Key
task_id *
string
(path)
图片变清晰任务的ID
Execute
Responses
Code Description
200
请求成功
Media type
Controls Accept header.
Example Value
Schema
{
"status": 200,
"message": "success",
"data": {
"created_at": "1634884056",
"processed_at": "1634884056",
"completed_at": "1634884056",
"image": "https://wxtechsz.oss-cn-shenzhen.aliyuncs.com/tasks/output/segmentation/93a5e47b-a9c1-47ac-9b5e-22cb89f7d358-image.jpg",
"task_id": "xxxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
"progress": 100,
"state": 1
}
}

View File

@ -0,0 +1,382 @@
# 恶意程序来源深入调查报告
## 调查时间
2025年1月1日
## 调查目的
深入调查 `kinsing`、`kdevtmpfsi` 等恶意挖矿程序的启动来源和传播途径。
---
## 执行摘要
### ✅ 好消息:恶意程序已被成功清除
**当前系统状态**:
- ✅ **真正的恶意进程数**: 0
- ✅ **恶意文件数**: 0
- ✅ **CPU空闲率**: 98.5%(挖矿程序会导致接近100%使用率)
- ✅ **系统负载**: 正常(0.35)
- ✅ **网络连接**: 无异常
### ⚠️ 重要发现:日志存在误报
**关键问题**:
- 日志中显示的"恶意进程"检测实际上大部分是**误报**
- 清理脚本使用 `find / -name .system3d` 命令时,产生的 `find` 进程被防重感染脚本误判为恶意进程
- 防重感染脚本使用 `pgrep -f` 匹配命令行,导致误匹配清理脚本的子进程
**实际情况**:
- 真正的恶意程序已被清理脚本成功清除
- 当前系统无真正的恶意进程在运行
- 所有启动源(定时任务、systemd服务、启动脚本等)均已检查,均为正常
- 日志的高频率检测是误报,不是真正的威胁
### 📋 建议
1. **立即行动**:修复脚本误报问题(使用更精确的检测方法)
2. **持续监控**:关注CPU使用率等真实指标,而不是误报日志
3. **系统加固**:继续保持现有安全措施,加强SSH和网络安全
---
## 1. 恶意程序行为分析
### 1.1 恶意程序特征
根据日志分析,检测到以下恶意程序:
- **kinsing** - 加密货币挖矿恶意软件
- **kdevtmpfsi** - 恶意挖矿程序
- **.system3d** - 隐藏的恶意程序
### 1.2 活动频率
- **检测频率**:每1-2分钟检测到新的恶意进程启动
- **终止次数**:最近1小时内约100次
- **模式**:恶意程序持续尝试重新启动,但很快被清理脚本终止
---
## 2. 启动源检查结果
### 2.1 定时任务 (Cron)
检查位置:
- `/var/spool/cron/root` - ✅ 仅包含正常的清理脚本
- `/etc/crontab` - ✅ 正常系统定时任务
- `/etc/cron.d/*` - ✅ 仅包含 `0hourly`
- `/etc/cron.hourly`, `/etc/cron.daily` 等 - ✅ 正常
**结果**:✅ **未发现恶意定时任务**
### 2.2 Systemd 服务
检查位置:
- `/etc/systemd/system/` - ✅ 仅发现防护服务:
- `prevent-malware-regeneration.service` - 防护服务(正常)
- `malware-monitor.service` - 监控服务(正常)
- `/usr/lib/systemd/system/` - ✅ 系统服务正常
- `~/.config/systemd/user/` - ✅ 目录不存在
**结果**:✅ **未发现恶意systemd服务**
### 2.3 系统启动脚本
检查位置:
- `/etc/rc.local` - ✅ 仅包含标准内容(touch /var/lock/subsys/local)
- `/etc/rc.d/rc.local` - ✅ 仅包含标准内容
- `~/.bashrc` - ✅ 正常
- `~/.bash_profile` - ✅ 正常(无恶意命令)
- `~/.profile` - ✅ 文件不存在
**结果**:✅ **启动脚本均为正常内容**
### 2.4 文件系统位置
检查位置:
- `/tmp/` - ✅ 未发现恶意文件(可疑目录已删除)
- `/var/tmp/` - ✅ 未发现恶意文件(可疑目录已删除)
- `/dev/shm/` - ✅ 未发现恶意文件
- `/root/` - ✅ 仅包含正常的脚本文件
- `/etc/data/` - ✅ 目录已被锁定
**结果**:✅ **文件系统中无恶意文件**
---
## 3. 网络活动分析
### 3.1 可疑网络连接
检查是否有:
- 可疑的外部IP连接 - ✅ 未发现
- 下载恶意脚本的网络活动 - ✅ 未发现
- 与挖矿池的连接 - ✅ 未发现
- 反向shell或后门连接 - ✅ 未发现
**结果**:✅ **网络连接正常,无可疑活动**
### 3.2 下载记录
检查日志中的:
- wget/curl 下载记录 - ✅ 系统日志中未发现
- 可疑URL访问 - ✅ 未发现
- 文件下载活动 - ✅ 监控脚本日志正常
**结果**:✅ **未发现恶意下载活动**
---
## 4. 进程分析
### 4.1 进程树分析
**关键发现**:捕获到的"恶意进程"实际上是清理脚本的子进程!
示例:
- **进程PID**: 1003037
- **命令**: `find / -name .system3d -type f -exec rm -f {} ;`
- **父进程**: `aggressive-malware-cleanup.sh` (PID 1003009)
- **工作目录**: `/root`
- **可执行文件**: `/usr/bin/find`
**结论**:这是清理脚本在删除恶意文件时启动的正常 `find` 进程,不是恶意程序本身。
### 4.2 真正的恶意进程检查
使用精确方法检查(只检查进程名,不检查命令行):
- **方法1**: 检查进程名(`ps aux | awk '{print $11}'`) - ✅ 未发现
- **方法2**: 检查可执行文件路径 - ✅ 未发现
- **当前运行**: 0个真正的恶意进程
- **恶意文件**: 0个
**结果**:✅ **当前无真正的恶意进程在运行**
---
## 5. 日志分析
### 5.1 系统日志
检查 `/var/log/` 中的:
- secure - SSH登录和命令执行 - ✅ 未发现恶意命令记录
- messages - 系统消息 - ✅ 正常
- audit - 审计日志 - ⚠️ 服务未启用
**结果**:✅ **系统日志中未发现恶意活动记录**
### 5.2 应用日志
检查应用相关的日志:
- 清理脚本日志 - ⚠️ 包含大量"恶意进程"记录(实际是误报)
- 防重生成日志 - ⚠️ 包含大量"发现恶意进程"记录(实际是误报)
- 监控日志 - ✅ 正常
**关键问题**:日志中的"恶意进程"记录大部分是误报,因为:
- 清理脚本使用 `find / -name .system3d` 命令
- 这些 `find` 进程的命令行包含 `.system3d` 关键词
- 防重感染脚本的 `pgrep -f` 匹配到了这些进程
**结果**:⚠️ **日志存在误报,但真正的恶意活动已被清除**
---
## 6. 安全脚本分析
### 6.1 清理脚本逻辑
**aggressive-malware-cleanup.sh** 脚本分析:
- **检测方法**: 使用 `pkill -9 -f` 匹配进程命令行
- **终止方式**: 强制终止(`-9` 信号)
- **清理策略**:
1. 终止恶意进程(kinsing, kdevtmpfsi, .system3d等)
2. 删除恶意文件和目录
3. 使用 `find` 搜索并删除隐藏文件
4. 锁定目录(`chattr +i`)
5. 清理定时任务和启动脚本
**问题**:脚本使用 `find / -name '.system3d' -type f -exec rm -f {} \;` 时,这个 `find` 进程会被防重感染脚本误判为恶意进程。
### 6.2 防重生成脚本
**prevent-reinfection.sh** 脚本分析:
- **检测逻辑**: 使用 `pgrep -f 'kinsing|kdevtmpfsi|system3d|b4nd1d0'` 匹配进程
- **问题**: `-f` 参数会匹配整个命令行,包括参数,导致误报
- **防护机制**:
1. 检查crontab是否被修改
2. 检查.bash_profile是否被修改
3. 检查恶意目录是否重新创建
4. 检测并终止恶意进程
**改进建议**:
- 使用 `pgrep -x` 只匹配进程名,不匹配命令行
- 或排除清理脚本的进程(通过检查父进程)
- 记录进程的完整信息以便分析
---
## 7. 调查结论
### 7.1 关键发现
⚠️ **重要发现:日志中的"恶意进程"大部分是误报!**
通过深入分析发现:
1. **误报问题**:
- 防重感染脚本使用 `pgrep -f 'kinsing|kdevtmpfsi|system3d'` 检测恶意进程
- 但清理脚本执行的命令如 `find / -name .system3d -type f -exec rm -f {} ;` 也包含关键词 `.system3d`
- 这导致清理脚本的 `find` 进程被误判为"恶意进程"
- 实际上这些进程是清理脚本在清理恶意文件时产生的正常进程
2. **真正的恶意进程**:
- 当前检查未发现真正的 `kinsing` 或 `kdevtmpfsi` 可执行文件
- 当前系统无真正的恶意进程在运行(排除误报后)
- 之前的恶意程序已被清理脚本成功清除
3. **启动源检查结果**:
- ✅ **定时任务**:未发现恶意定时任务,只有正常的清理脚本
- ✅ **Systemd服务**:只有防护服务,无恶意服务
- ✅ **启动脚本**:`.bash_profile`、`.bashrc` 等均为正常内容
- ✅ **文件系统**:未发现恶意可执行文件
- ✅ **网络连接**:无可疑外部连接
- ✅ **容器**:未使用Docker/Podman容器
- ✅ **Web脚本**:未发现通过Web服务器执行的恶意脚本
### 7.2 实际情况分析
**日志显示的高频率检测实际上是:**
1. 清理脚本每分钟执行,使用 `find` 命令搜索并删除恶意文件
2. `find` 进程的命令行包含 `.system3d` 等关键词
3. 防重感染脚本每2分钟执行,使用 `pgrep -f` 匹配包含这些关键词的进程
4. 误将清理脚本的 `find` 进程识别为"恶意进程"
5. 脚本记录日志并尝试终止这些进程(但由于进程很快结束,实际无影响)
**真正的状态:**
- ✅ 恶意程序已被成功清除
- ✅ 当前无真正的恶意进程在运行
- ✅ 清理脚本工作正常,持续清理可能的残留
- ⚠️ 日志记录存在误报,导致看起来恶意程序很活跃
### 7.3 恶意程序的原始来源推测
虽然当前未发现活跃的恶意程序,但从历史记录和常见攻击模式推测,可能的来源:
1. **SSH暴力破解**:通过弱密码或密钥泄露获得SSH访问权限
2. **Web应用漏洞**:通过Web应用(如Redis未授权访问、Web Shell等)获得执行权限
3. **供应链攻击**:通过被感染的依赖包或第三方脚本
4. **已修复的漏洞**:通过已修复的已知漏洞入侵(系统已更新)
由于恶意程序已被清除,原始入侵痕迹可能已被清理或覆盖。
---
## 8. 建议措施
### 8.1 立即行动
1. **修复脚本误报问题** ⭐ **重要**
修改 `/root/scripts/prevent-reinfection.sh` 脚本:
```bash
# 当前代码(有问题):
if pgrep -f 'kinsing|kdevtmpfsi|system3d|b4nd1d0' > /dev/null; then
# 这会误匹配清理脚本的find进程
fi
# 建议修改为:
# 方法1: 只检查进程名(更精确)
if pgrep -x 'kinsing' > /dev/null || pgrep -x 'kdevtmpfsi' > /dev/null; then
# 只匹配进程名,不匹配命令行
fi
# 方法2: 排除清理脚本的进程
pgrep -f 'kinsing|kdevtmpfsi|system3d' | while read pid; do
# 检查父进程,排除清理脚本
ppid=$(ps -o ppid= -p $pid | tr -d ' ')
if ! ps -p $ppid -o cmd= | grep -qE 'cleanup|prevent'; then
# 这是真正的恶意进程
kill -9 $pid
fi
done
```
2. **持续监控**
- ✅ 继续保持清理脚本运行(已有效清除恶意程序)
- ⚠️ 注意日志中的误报,关注真正的威胁指标
- ✅ **最重要的指标**:CPU使用率(当前98.5%空闲,正常)
- ✅ 系统负载(当前正常)
- ✅ 恶意文件数量(当前为0)
### 8.2 系统加固
1. **SSH安全**
- 使用密钥认证,禁用密码登录
- 限制SSH访问IP(如可能)
- 使用fail2ban防止暴力破解
2. **网络安全**
- 确保Redis等服务仅监听本地或使用密码
- 定期检查开放的端口和服务
- 使用防火墙限制不必要的网络访问
3. **应用安全**
- 保持系统和应用更新
- 使用最小权限原则
- 定期进行安全审计
### 8.3 监控优化
1. **改进日志记录** ⭐ **推荐**
建议修改脚本,添加更详细的进程信息:
```bash
# 记录进程详细信息
pgrep -f 'kinsing|kdevtmpfsi' | while read pid; do
cmd=$(ps -p $pid -o cmd=)
ppid=$(ps -p $pid -o ppid=)
pcmd=$(ps -p $ppid -o cmd=)
log "发现进程 PID=$pid, CMD=$cmd, PPID=$ppid, PCMD=$pcmd"
# 只终止真正的恶意进程(排除清理脚本的子进程)
if ! echo "$pcmd" | grep -qE 'cleanup|prevent'; then
kill -9 $pid
log "已终止真正的恶意进程: $pid"
else
log "跳过清理脚本的子进程: $pid (父进程: $pcmd)"
fi
done
```
2. **资源监控** ✅ **当前正常**
- ✅ CPU使用率:98.5% 空闲(挖矿程序会导致接近100%使用)
- ✅ 系统负载:0.35(正常)
- ✅ 内存使用:正常
- ⚠️ 建议:设置CPU使用率告警(如>80%持续5分钟)
- ⚠️ 建议:监控网络流量(挖矿程序需要连接挖矿池)
### 8.4 长期建议
1. **备份和恢复**
- 建立定期备份机制
- 测试恢复流程
- 保存系统配置的干净副本
2. **安全审计**
- 定期进行安全审计
- 检查系统日志
- 关注安全公告和更新
3. **事件响应**
- 建立事件响应流程
- 记录所有安全事件
- 分析攻击模式和来源
---
**报告生成时间**: 2025年1月1日
**调查人员**: AI Assistant
**服务器**: 45.192.99.48

View File

@ -0,0 +1,240 @@
# 服务器挖矿程序检查报告
## 检查时间
2025年1月1日 12:43
## 检查摘要
### ✅ 总体状态:未发现活跃的挖矿程序
---
## 1. 进程检查
### 1.1 挖矿相关进程
- ✅ **未发现明显的挖矿进程**
- 检查的关键词:`miner`, `xmrig`, `monero`, `coinhive`, `cpuminer`, `minerd`
- **结果**:未发现任何匹配的进程
### 1.2 已知恶意进程检查
- ✅ **未发现已知恶意进程**
- 检查的进程:`kinsing`, `kdevtmpfsi`, `.system3d`, `uhavenobotsxd`, `est1`, `ladyg0g0`
- **结果**:所有已知恶意进程均未在运行
### 1.3 CPU使用率检查
- **当前CPU使用率**:0.7% user, 1.1% system, 96.8% idle
- **最高CPU使用的进程**:
1. `next-server` (6.0%) - 正常的Next.js应用服务器
2. `sshd-session` (3.0%) - SSH会话,正常
3. `crowdsec` (2.3%) - 安全监控服务,正常
- **结论**:CPU使用率正常,无异常高CPU占用进程
---
## 2. 系统资源使用
### 2.1 系统负载
- **当前负载**:0.83 (1分钟), 3.56 (5分钟), 2.79 (15分钟)
- **CPU状态**:空闲率 96.8%
- **内存使用**:5332.1 MB 空闲 / 15729.9 MB 总计 (约66% 空闲)
- **结论**:系统负载正常,无资源耗尽迹象
### 2.2 网络连接
- 检查了所有已建立的连接
- 未发现可疑的外部连接
- 所有连接均为正常的服务端口(80, 443, 3000, 5432, 6379, 22, 8002)
---
## 3. 安全脚本状态
### 3.1 正在运行的清理脚本
以下脚本正在通过crontab定期执行:
1. **aggressive-malware-cleanup.sh**
- 执行频率:每分钟
- 日志文件:`/var/log/aggressive-cleanup.log` (156KB)
- 功能:终止恶意进程、删除恶意文件、锁定目录
2. **prevent-reinfection.sh**
- 执行频率:每2分钟
- 日志文件:`/var/log/prevent-malware-regeneration.log` (371KB)
- 功能:防止恶意程序重新生成
3. **enhanced-malware-cleanup.sh**
- 执行频率:每30分钟
- 日志文件:`/var/log/enhanced-cleanup.log` (48KB)
- 功能:增强清理功能
4. **monitor-downloads.sh**
- 执行频率:每5分钟
- 日志文件:`/var/log/malware-monitor.log` (7.9KB)
- 功能:监控下载活动
### 3.2 脚本清理的目标
根据 `aggressive-malware-cleanup.sh` 脚本内容,它针对以下恶意程序:
- **kinsing** - 加密货币挖矿恶意软件
- **kdevtmpfsi** - 恶意挖矿程序
- **.system3d** - 隐藏的恶意程序
- **uhavenobotsxd** - 恶意脚本
- **est1, ladyg0g0, esd101** - 其他恶意变种
脚本执行的清理操作:
- 终止恶意进程(`pkill -9`)
- 删除恶意文件和目录
- 锁定 `/etc/data` 目录
- 清理恶意定时任务
- 清理 `.bash_profile` 中的恶意命令
---
## 4. 日志分析
### 4.1 日志文件位置
- `/var/log/aggressive-cleanup.log` - 强力清理日志 (156KB)
- `/var/log/prevent-malware-regeneration.log` - 防重生成日志 (371KB)
- `/var/log/enhanced-cleanup.log` - 增强清理日志 (48KB)
- `/var/log/malware-monitor.log` - 恶意软件监控日志 (7.9KB)
- `/var/log/malware-cleanup.log` - 恶意软件清理日志 (3.6KB)
- `/var/log/eradicate-malware.log` - 根除恶意软件日志 (1.1KB)
### 4.2 日志大小分析
- 日志文件持续增长,说明脚本在持续执行
- `prevent-malware-regeneration.log` 最大 (371KB),表明防重生成脚本工作频繁
- `aggressive-cleanup.log` 也较大 (156KB),说明定期清理在执行
---
## 5. 文件系统检查
### 5.1 可疑文件检查
检查了脚本清理的目标文件/目录:
- ✅ `/etc/data/kinsing` - 不存在
- ✅ `/tmp/kdevtmpfsi` - 不存在
- ⚠️ `/tmp/.est1` - 已发现(目录,创建于12:30:05,已删除)
- ⚠️ `/var/tmp/.ladyg0g0` - 已发现(目录,创建于12:30:05,已删除)
- ⚠️ `/tmp/.esd101` - 已发现(目录,创建于12:30:05,已删除)
- ✅ `/.system3d` - 不存在
**结论**:发现3个可疑目录,已删除。这些目录可能是恶意程序的工作目录或启动脚本位置。
### 5.2 定时任务检查
- 检查了用户crontab和系统cron目录
- 只发现正常的清理脚本任务
- 未发现恶意定时任务
---
## 6. 结论
### 6.1 当前状态
⚠️ **恶意程序持续尝试运行,但清理脚本有效阻止**
**依据**:
1. ✅ 进程检查:当前无活跃挖矿进程(已被清理脚本终止)
2. ⚠️ 日志显示:恶意进程(kinsing, kdevtmpfsi, .system3d)频繁尝试启动
3. ✅ CPU使用率:正常(96.8% 空闲) - 说明恶意程序未成功运行
4. ✅ 系统负载:正常
5. ✅ 网络连接:无可疑连接
6. ⚠️ 文件系统:发现3个可疑文件(`.est1`, `.ladyg0g0`, `.esd101`)
7. ✅ 定时任务:无恶意定时任务
**日志分析**:
- 从 `prevent-malware-regeneration.log` 可以看到,每2分钟检测到新的恶意进程
- 最近1小时内检测并终止了多次恶意进程
- 清理脚本每分钟执行,及时清理恶意程序
- 最新记录:12:42:20 发现并终止了恶意进程 1001429
### 6.2 安全脚本效果
✅ **安全清理脚本正在有效工作,成功阻止恶意程序运行**
**证据**:
1. ✅ 脚本定期执行(通过crontab)
2. ✅ 日志文件持续更新,显示频繁的清理操作
3. ✅ 恶意进程被及时检测和终止
4. ✅ 系统资源使用正常(CPU空闲率96.8%)
5. ✅ 恶意程序无法长期运行(每次启动后很快被终止)
**日志统计**:
- **强力清理脚本**:每分钟执行,定期终止 `kinsing`、`kdevtmpfsi`、`.system3d` 进程
- **防重生成脚本**:每2分钟执行,检测并终止新启动的恶意进程
- **监控脚本**:持续检测到恶意程序尝试运行
**示例日志记录**(最近1小时):
- 12:37:02 - 已终止kinsing进程
- 12:38:02 - 已终止kdevtmpfsi进程
- 12:42:20 - 发现并终止恶意进程 1001429
- 多次检测到恶意进程尝试启动,但都被及时终止
### 6.3 重要发现
⚠️ **恶意程序在不断尝试重新启动**
从日志分析可以看出:
1. **kinsing** 和 **kdevtmpfsi** 恶意挖矿程序持续尝试运行
2. 清理脚本每1-2分钟检测并终止这些进程
3. 恶意程序可能通过某种机制(定时任务、文件系统触发器等)自动重新启动
4. 发现了3个可疑文件,可能是恶意程序的启动脚本或二进制文件
### 6.4 建议
1. **立即处理可疑文件**
- 已发现的可疑文件:`/tmp/.est1`, `/var/tmp/.ladyg0g0`, `/tmp/.esd101`
- 建议:删除这些文件并锁定目录,防止重新创建
2. **深入调查恶意程序来源**
- 检查是否有隐藏的定时任务或服务
- 检查系统启动脚本(`/etc/rc.local`, `/etc/systemd/system/` 等)
- 检查是否有rootkit或后门
3. **增强清理脚本**
- 在清理脚本中添加对这些可疑文件的强制删除
- 考虑使用 `chattr +i` 锁定关键目录
4. **继续保持监控**
- 脚本已设置定期执行,建议继续运行
- 定期检查日志文件,关注恶意程序尝试频率
- 如果频率增加,说明攻击者仍在尝试
5. **系统加固**
- 确保所有安全补丁已更新
- 继续使用fail2ban和crowdsec等安全工具
- 考虑限制文件系统写入权限
6. **备份策略**
- 确保重要数据有定期备份
- 考虑完全清理系统并重新部署(如果攻击持续)
---
## 7. 脚本配置详情
### 7.1 当前运行的Crontab任务
```cron
* * * * * /root/scripts/aggressive-malware-cleanup.sh > /dev/null 2>&1
*/2 * * * * /root/scripts/prevent-reinfection.sh > /dev/null 2>&1
*/30 * * * * /root/scripts/enhanced-malware-cleanup.sh > /dev/null 2>&1
*/5 * * * * /root/scripts/monitor-downloads.sh > /dev/null 2>&1
*/1 * * * * /usr/local/bin/clear-http-bans.sh >/dev/null 2>&1
```
### 7.2 脚本功能总结
- **aggressive-malware-cleanup.sh**: 每分钟强力清理恶意程序
- **prevent-reinfection.sh**: 每2分钟防止恶意程序重新生成
- **enhanced-malware-cleanup.sh**: 每30分钟增强清理
- **monitor-downloads.sh**: 每5分钟监控下载活动
- **clear-http-bans.sh**: 每分钟清理HTTP禁令(fail2ban相关)
---
**报告生成时间**: 2025年1月1日 12:43
**检查人员**: AI Assistant
**服务器**: 45.192.99.48

View File

@ -135,3 +135,5 @@ pmset -g