content-moderation 内容审核
图片和文本内容审核,确保用户生成内容符合平台规范
概述
@mono/content-moderation 是项目的内容审核包,提供图片和文本内容审核功能。它为平台提供了一层内容安全保护,确保用户上传的内容符合社区规范和法律法规要求。
为什么需要内容审核?
在任何允许用户生成内容(UGC)的平台上,内容审核都是必不可少的:
法律合规
- 法律要求:中国《网络安全法》要求平���对用户发布的信息进行审核
- 平台责任:平台需要对违法违规内容承担连带责任
- 用户保护:保护未成年人和其他用户免受不良内容影响
社区健康
- 用户体验:过滤违规内容,维护良好的社区氛围
- 品牌形象:避免违规内容损害平台品牌
- 运营效率:自动化审核减少人工审核成本
业务风险
- 法律风险:违规内容可能导致平台被处罚或关停
- 用户流失:不良内容会导致用户流失
- 商业损失:广告主不愿在违规内容旁投放广告
核心功能
- 图片审核 - 检测图片中的违规内容
- 多种模式 - 支持头像和内容两种审核模式
- 灵活集成 - 可轻松集成第三方审核服务
- 类型安全 - 完整的 TypeScript 支持
快速开始
基础用法
import { requestImageModeration } from "@mono/content-moderation";
// 审核图片
const result = await requestImageModeration(
"https://example.com/image.jpg",
"content"
);
if (result.ok) {
console.log("审核通过");
} else {
console.log("审核失败:", result.reason);
}审核模式
包支持两种审核模式:
// 头像模式 - 更严格
const avatarResult = await requestImageModeration(
"https://example.com/avatar.jpg",
"avatar"
);
// 内容模式 - 相对宽松
const contentResult = await requestImageModeration(
"https://example.com/photo.jpg",
"content"
);实战案例
案例 1: 用户头像上传
完整的头像上传流程,包括审核和错误处理。
// app/api/avatar/upload/route.ts
import { requestImageModeration } from "@mono/content-moderation";
import { uploadFileToS3, deleteFileFromS3 } from "@mono/storage";
export async function POST(request: Request) {
const formData = await request.formData();
const file = formData.get("avatar") as File;
// 验证文件
if (!file.type.startsWith("image/")) {
return Response.json(
{ error: "只支持图���格式" },
{ status: 400 }
);
}
// 上传到临时位置
const tempPath = `temp/avatars/${Date.now()}-${file.name}`;
const buffer = Buffer.from(await file.arrayBuffer());
await uploadFileToS3(tempPath, {
bucket: process.env.S3_BUCKET!,
body: buffer,
contentType: file.type,
});
const tempUrl = `${process.env.S3_PUBLIC_ENDPOINT}/${tempPath}`;
// 审核图片
try {
const moderation = await requestImageModeration(tempUrl, "avatar");
if (!moderation.ok) {
// 删除违规图片
await deleteFileFromS3(tempPath, {
bucket: process.env.S3_BUCKET!
});
return Response.json(
{ error: `头像审核失败: ${moderation.reason}` },
{ status: 400 }
);
}
// 移动到正式位置
const userId = "user-123"; // 从 session 获取
const finalPath = `avatars/${userId}.jpg`;
// ... 移动文件逻辑
return Response.json({ url: finalUrl });
} catch (error) {
// 清理临时文件
await deleteFileFromS3(tempPath, {
bucket: process.env.S3_BUCKET!
});
throw error;
}
}案例 2: 社区内容发布
在用户发布内容时审核所有图片。
// app/api/posts/create/route.ts
import { requestImageModeration } from "@mono/content-moderation";
import { prisma } from "@/lib/prisma";
export async function POST(request: Request) {
const { content, imageUrls } = await request.json();
// 验证输入
if (!content || !Array.isArray(imageUrls)) {
return Response.json(
{ error: "无效的请求参数" },
{ status: 400 }
);
}
// 并发审核所有图片
const moderationResults = await Promise.all(
imageUrls.map(url => requestImageModeration(url, "content"))
);
// 检查是否有图片未通过审核
const failedIndex = moderationResults.findIndex(r => !r.ok);
if (failedIndex !== -1) {
return Response.json(
{
error: "图片审核失败",
failedImage: imageUrls[failedIndex],
reason: moderationResults[failedIndex].reason,
},
{ status: 400 }
);
}
// 所��图片通过审核,创建帖子
const post = await prisma.post.create({
data: {
content,
images: imageUrls,
userId: "user-123", // 从 session 获取
},
});
return Response.json(post);
}案例 3: 批量图片审核
批量审核多张图片,并返回详细结果。
// lib/moderation.ts
import { requestImageModeration } from "@mono/content-moderation";
interface BatchModerationResult {
url: string;
passed: boolean;
reason?: string;
}
export async function batchModerateImages(
imageUrls: string[],
mode: "avatar" | "content" = "content"
): Promise<BatchModerationResult[]> {
// 使用 Promise.allSettled 确保所有请求都完成
const results = await Promise.allSettled(
imageUrls.map(url => requestImageModeration(url, mode))
);
return results.map((result, index) => {
if (result.status === "rejected") {
return {
url: imageUrls[index],
passed: false,
reason: result.reason?.message || "审核请求失败",
};
}
return {
url: imageUrls[index],
passed: result.value.ok,
reason: result.value.reason,
};
});
}
// 使用示例
const imageUrls = [
"https://example.com/image1.jpg",
"https://example.com/image2.jpg",
"https://example.com/image3.jpg",
];
const results = await batchModerateImages(imageUrls);
// 检查结果
const allPassed = results.every(r => r.passed);
const failedImages = results.filter(r => !r.passed);
if (!allPassed) {
console.log("以下图片未通过审核:");
failedImages.forEach(img => {
console.log(`- ${img.url}: ${img.reason}`);
});
}案例 4: 审核结果缓存
缓存审核结果,避免重复审核相同图片,节省成本。
// lib/cached-moderation.ts
import { requestImageModeration, type ImageModerationResult } from "@mono/content-moderation";
import { createMemoryCache } from "@mono/cache";
// 创建缓存实例
const moderationCache = createMemoryCache();
export async function cachedImageModeration(
imageUrl: string,
mode: "avatar" | "content" = "content"
): Promise<ImageModerationResult> {
const cacheKey = `moderation:${mode}:${imageUrl}`;
// 检查缓存
const cached = moderationCache.get<ImageModerationResult>(cacheKey);
if (cached) {
console.log("使用缓存的审核结果");
return cached;
}
// 执行审核
const result = await requestImageModeration(imageUrl, mode);
// 缓存结果(24 小时)
// 注意:只缓存通过的结果,失败的结果可能需要重新审核
if (result.ok) {
moderationCache.set(cacheKey, result, 24 * 60 * 60);
}
return result;
}
// 清除特定图片的缓存
export function clearModerationCache(imageUrl: string) {
moderationCache.delete(`moderation:avatar:${imageUrl}`);
moderationCache.delete(`moderation:content:${imageUrl}`);
}案例 5: Server Action 集成
在 Next.js Server Action 中使用内容审核。
// app/actions/upload-image.ts
"use server";
import { requestImageModeration } from "@mono/content-moderation";
import { uploadFileToS3, deleteFileFromS3 } from "@mono/storage";
import { revalidatePath } from "next/cache";
export async function uploadAndModerateImage(formData: FormData) {
const file = formData.get("image") as File;
if (!file) {
return { error: "未选择文件" };
}
// 验证文件类型
const allowedTypes = ["image/jpeg", "image/png", "image/webp"];
if (!allowedTypes.includes(file.type)) {
return { error: "只支持 JPG、PNG、WebP 格式" };
}
// 验证文件大小(最大 10MB)
if (file.size > 10 * 1024 * 1024) {
return { error: "文件大小不能超过 10MB" };
}
try {
// 上传文件
const path = `uploads/${Date.now()}-${file.name}`;
const buffer = Buffer.from(await file.arrayBuffer());
await uploadFileToS3(path, {
bucket: process.env.S3_BUCKET!,
body: buffer,
contentType: file.type,
});
const url = `${process.env.S3_PUBLIC_ENDPOINT}/${path}`;
// 审核图片
const moderation = await requestImageModeration(url, "content");
if (!moderation.ok) {
// 删除违规图片
await deleteFileFromS3(path, {
bucket: process.env.S3_BUCKET!
});
return {
error: `图片审核失败: ${moderation.reason || "内容不符合规范"}`
};
}
// 重新验证相关页面
revalidatePath("/gallery");
return { url };
} catch (error) {
console.error("Upload failed:", error);
return { error: "上传失败,请稍后重试" };
}
}客户端组件:
// components/ImageUpload.tsx
"use client";
import { uploadAndModerateImage } from "@/app/actions/upload-image";
import { useState } from "react";
export function ImageUpload() {
const [uploading, setUploading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [imageUrl, setImageUrl] = useState<string | null>(null);
async function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
e.preventDefault();
setUploading(true);
setError(null);
const formData = new FormData(e.currentTarget);
const result = await uploadAndModerateImage(formData);
if (result.error) {
setError(result.error);
} else {
setImageUrl(result.url!);
alert("上传成功!");
}
setUploading(false);
}
return (
<div className="space-y-4">
<form onSubmit={handleSubmit} className="space-y-4">
<div>
<label htmlFor="image" className="block text-sm font-medium">
选择图片
</label>
<input
type="file"
id="image"
name="image"
accept="image/jpeg,image/png,image/webp"
required
className="mt-1 block w-full"
/>
</div>
<button
type="submit"
disabled={uploading}
className="px-4 py-2 bg-blue-600 text-white rounded disabled:opacity-50"
>
{uploading ? "上传中..." : "上传图片"}
</button>
{error && (
<div className="p-3 bg-red-50 text-red-700 rounded">
{error}
</div>
)}
</form>
{imageUrl && (
<div>
<p className="text-sm text-gray-600 mb-2">上传成功:</p>
<img
src={imageUrl}
alt="Uploaded"
className="max-w-md rounded shadow"
/>
</div>
)}
</div>
);
}案例 6: 异步审核
对于非关键内容,可以先允许上传,异步审核后处理。
// lib/async-moderation.ts
import { requestImageModeration } from "@mono/content-moderation";
import { prisma } from "@/lib/prisma";
export async function asyncModeratePost(postId: string) {
const post = await prisma.post.findUnique({
where: { id: postId },
select: { images: true },
});
if (!post || !post.images.length) {
return;
}
// 审核所有图片
const results = await Promise.all(
post.images.map(url => requestImageModeration(url, "content"))
);
// 检查是否有违规图片
const hasViolation = results.some(r => !r.ok);
if (hasViolation) {
// 标记帖子为违规
await prisma.post.update({
where: { id: postId },
data: {
status: "hidden",
moderationReason: "图片包含违规内容",
moderatedAt: new Date(),
},
});
// 通知用户
// await notifyUser(post.userId, "您的帖子因包含违规内容已被隐藏");
} else {
// 标记为已审核
await prisma.post.update({
where: { id: postId },
data: {
status: "published",
moderatedAt: new Date(),
},
});
}
}
// 在创建帖子后调用
export async function createPostWithAsyncModeration(data: PostData) {
const post = await prisma.post.create({ data });
// 异步审核(不阻塞响应)
asyncModeratePost(post.id).catch(error => {
console.error("Async moderation failed:", error);
});
return post;
}集成第三方审核服务
当前实现是一个占位符,返回 { ok: true }。要集成真实的审核服务,需要修改 packages/content-moderation/src/client.ts。
集成腾讯云内容审核
// packages/content-moderation/src/client.ts
import { moderateImage } from "@mono/tencent-cloud";
export async function requestImageModeration(
imageUrl: string,
mode: ImageModerationMode = "content",
): Promise<ImageModerationResult> {
if (!imageUrl || !/^https?:\/\//i.test(imageUrl)) {
throw new Error("Invalid image url");
}
try {
// 调用腾讯云图片审核
const result = await moderateImage(imageUrl);
if (result.Suggestion === "Pass") {
return { ok: true };
}
// 提取违规原因
const reasons = result.Label || "内容不符合规范";
return { ok: false, reason: reasons };
} catch (error) {
console.error("Image moderation failed:", error);
// 降级策略:审核服务失败时允许通过
// 生产环境可能需要更保守的策略
return { ok: true };
}
}集成阿里云内容安全
import AlibabaCloud from "@alicloud/pop-core";
const client = new AlibabaCloud({
accessKeyId: process.env.ALIYUN_ACCESS_KEY_ID!,
accessKeySecret: process.env.ALIYUN_ACCESS_KEY_SECRET!,
endpoint: "https://green.cn-shanghai.aliyuncs.com",
apiVersion: "2018-05-09",
});
export async function requestImageModeration(
imageUrl: string,
mode: ImageModerationMode = "content",
): Promise<ImageModerationResult> {
if (!imageUrl || !/^https?:\/\//i.test(imageUrl)) {
throw new Error("Invalid image url");
}
const scenes = mode === "avatar"
? ["porn", "terrorism"]
: ["porn", "terrorism", "ad", "live"];
try {
const result = await client.request(
"ImageSyncScan",
{
RegionId: "cn-shanghai",
ImageUrl: imageUrl,
Scenes: scenes,
},
{ method: "POST" }
);
const suggestion = result.Data[0].Results[0].Suggestion;
if (suggestion === "pass") {
return { ok: true };
}
return {
ok: false,
reason: result.Data[0].Results[0].Label
};
} catch (error) {
console.error("Image moderation failed:", error);
return { ok: true }; // 降级策略
}
}审核策略
头像审核
头像审核通常更严格,建议检测:
- ✅ 色情内容
- ✅ 暴力血腥
- ✅ 违法违规
- ✅ 政治敏感
- ✅ 恶心内容
const result = await requestImageModeration(avatarUrl, "avatar");内容审核
内容审核可以相对宽松:
- ✅ 色情内容
- ✅ 暴力血腥
- ✅ 违法违规
- ⚠️ 广告营销(可选)
- ⚠️ 二维码(可选)
const result = await requestImageModeration(imageUrl, "content");降级策略
当审核服务不可用时,需要选择合适的降级策略:
乐观策略(默认通过)
适用于:社区内容、用户评论等非关键场景
try {
return await requestImageModeration(url, mode);
} catch (error) {
console.error("Moderation service unavailable:", error);
// 允许上传,后续人工审核
return { ok: true };
}保守策略(默认拒绝)
适用于:用户头像、公开展示的内容等关键场景
try {
return await requestImageModeration(url, mode);
} catch (error) {
console.error("Moderation service unavailable:", error);
// 拒绝上传,提示用户稍后重试
return { ok: false, reason: "审核服务暂时不可用,请稍后重试" };
}API 参考
requestImageModeration(imageUrl, mode?)
请求图片内容审核。
参数:
imageUrl(string): 图片 URL,必须是完整的 http/https URLmode(ImageModerationMode, 可选): 审核模式,默认 "content""avatar"- 头像审核模式(更严格)"content"- 内容审核模式
返回: Promise<ImageModerationResult>
interface ImageModerationResult {
ok: boolean; // 是否通过审核
reason?: string; // 未通过时的原因
}抛出异常:
- 如果
imageUrl为空或格式无效,抛出Error
示例:
// 基础用法
const result = await requestImageModeration("https://example.com/image.jpg");
// 指定审核模式
const result = await requestImageModeration(
"https://example.com/avatar.jpg",
"avatar"
);
// 错误处理
try {
const result = await requestImageModeration(imageUrl);
if (!result.ok) {
console.log("审核失败:", result.reason);
}
} catch (error) {
console.error("请求失败:", error.message);
}最佳实践
1. 先上传后审核
// ✅ 推荐:先上传到临时位置
const tempUrl = await uploadToTemp(file);
const result = await requestImageModeration(tempUrl);
if (result.ok) {
await moveToFinal(tempUrl);
} else {
await deleteTemp(tempUrl);
}
// ❌ 避免:审核通过后再上传(用户体验差)
const result = await requestImageModeration(localFileUrl);
if (result.ok) {
await upload(file);
}2. 批量审核优化
// ✅ 并发审核
const results = await Promise.all(
urls.map(url => requestImageModeration(url))
);
// ❌ 串行审核(慢)
for (const url of urls) {
await requestImageModeration(url);
}3. 缓存审核结果
// 避免重复审核相同图片
const cacheKey = `moderation:${imageUrl}`;
const cached = cache.get(cacheKey);
if (cached) {
return cached;
}
const result = await requestImageModeration(imageUrl);
cache.set(cacheKey, result, 24 * 60 * 60); // 缓存 24 小时4. 监控和日志
const startTime = Date.now();
try {
const result = await requestImageModeration(imageUrl, mode);
const duration = Date.now() - startTime;
// 记录审核结果
await db.moderationLog.create({
data: { imageUrl, mode, passed: result.ok, duration },
});
return result;
} catch (error) {
console.error("Moderation failed:", { imageUrl, mode, error });
throw error;
}5. 用户友好的错误提示
const result = await requestImageModeration(imageUrl);
if (!result.ok) {
// ❌ 不友好
return { error: result.reason };
// ✅ 友好
return {
error: "图片包含不适当内容,请更换后重试",
details: result.reason, // 可选,用于调试
};
}故障排查
URL 格式错误
// ❌ 错误
await requestImageModeration("/uploads/image.jpg");
await requestImageModeration("image.jpg");
// ✅ 正确
await requestImageModeration("https://example.com/uploads/image.jpg");审核服务超时
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 5000);
try {
const result = await requestImageModeration(url);
clearTimeout(timeoutId);
return result;
} catch (error) {
if (error.name === "AbortError") {
return { ok: false, reason: "审核超时" };
}
throw error;
}审核结果不准确
如果审核结果不符合预期:
- 检查审核模式是否正确(avatar vs content)
- 确认第三方服务配置正确
- 查看审核服务的详细日志
- 考虑调整审核策略或阈值
环境变量
如果集成第三方审核服务,需要配置相应的环境变量:
# 腾讯云内容审核
TENCENT_SECRET_ID=your-secret-id
TENCENT_SECRET_KEY=your-secret-key
# 阿里云内容安全
ALIYUN_ACCESS_KEY_ID=your-access-key-id
ALIYUN_ACCESS_KEY_SECRET=your-access-key-secret