first-update

This commit is contained in:
2026-03-17 14:36:31 +08:00
parent 72f08aee7c
commit 4eddf05e79
516 changed files with 115270 additions and 1 deletions

View File

@@ -0,0 +1,31 @@
import { NextResponse } from 'next/server';
import { getImageDetailWithQuestions } from '@/lib/services/images';
// 根据图片ID获取图片详情包含问题列表和已标注数据
export async function GET(request, { params }) {
try {
const { projectId, imageId } = params;
// 调用服务层获取图片详情
const imageData = await getImageDetailWithQuestions(projectId, imageId);
return NextResponse.json({
success: true,
data: imageData
});
} catch (error) {
console.error('Failed to get image details:', error);
// 根据错误类型返回不同的状态码
let statusCode = 500;
if (error.message === '缺少图片ID') {
statusCode = 400;
} else if (error.message === '图片不存在') {
statusCode = 404;
} else if (error.message === '图片不属于指定项目') {
statusCode = 403;
}
return NextResponse.json({ error: error.message || 'Failed to get image details' }, { status: statusCode });
}
}

View File

@@ -0,0 +1,89 @@
import { NextResponse } from 'next/server';
import { PrismaClient } from '@prisma/client';
import { getImageById, getImageChunk } from '@/lib/db/images';
import { createImageDataset } from '@/lib/db/imageDatasets';
const prisma = new PrismaClient();
// 创建标注
export async function POST(request, { params }) {
try {
const { projectId } = params;
const { imageId, questionId, question, answerType, answer, note } = await request.json();
// 验证必填字段
if (!imageId || !question || !answerType || answer === undefined || answer === null) {
return NextResponse.json({ error: '缺少必要参数imageId, question, answerType, answer' }, { status: 400 });
}
// 验证图片存在
const image = await getImageById(imageId);
if (!image || image.projectId !== projectId) {
return NextResponse.json({ error: '图片不存在' }, { status: 404 });
}
// 验证答案类型
if (!['text', 'label', 'custom_format'].includes(answerType)) {
return NextResponse.json({ error: '无效的答案类型' }, { status: 400 });
}
// 验证答案内容
if (answerType === 'text' && typeof answer !== 'string') {
return NextResponse.json({ error: '文本类型答案必须是字符串' }, { status: 400 });
}
if (answerType === 'label' && !Array.isArray(answer)) {
return NextResponse.json({ error: '标签类型答案必须是数组' }, { status: 400 });
}
// 序列化答案
let answerString = answer;
if (answerType !== 'text' && typeof answerString !== 'string') {
answerString = JSON.stringify(answer, null, 2);
}
// 1. 获取问题记录(前端传递的 questionId 指向已有的问题)
if (!questionId) {
return NextResponse.json({ error: '缺少必要参数questionId' }, { status: 400 });
}
const questionRecord = await prisma.questions.findUnique({
where: { id: questionId }
});
if (!questionRecord) {
return NextResponse.json({ error: '问题不存在' }, { status: 404 });
}
// 验证问题属于该图片
if (questionRecord.imageId !== imageId) {
return NextResponse.json({ error: '问题不属于该图片' }, { status: 400 });
}
// 2. 更新问题为已回答
await prisma.questions.update({
where: { id: questionRecord.id },
data: { answered: true }
});
// 3. 创建 ImageDataset 记录
const dataset = await createImageDataset(projectId, {
imageId: image.id,
imageName: image.imageName,
questionId: questionRecord.id,
question,
answer: answerString,
answerType,
model: 'manual',
note: note || ''
});
return NextResponse.json({
success: true,
dataset,
questionId: questionRecord.id
});
} catch (error) {
console.error('Failed to create annotation:', error);
return NextResponse.json({ error: error.message || 'Failed to create annotation' }, { status: 500 });
}
}

View File

@@ -0,0 +1,41 @@
import { NextResponse } from 'next/server';
import { getImageByName } from '@/lib/db/images';
import imageService from '@/lib/services/images';
// 生成图像数据集
export async function POST(request, { params }) {
try {
const { projectId } = params;
const { imageName, question, model, language = 'zh', previewOnly = false } = await request.json();
if (!imageName || !question) {
return NextResponse.json({ error: '缺少必要参数' }, { status: 400 });
}
if (!model) {
return NextResponse.json({ error: '请选择一个视觉模型' }, { status: 400 });
}
// 获取图片信息
const image = await getImageByName(projectId, imageName);
if (!image) {
return NextResponse.json({ error: '图片不存在' }, { status: 404 });
}
// 调用图片数据集生成服务
const result = await imageService.generateDatasetForImage(projectId, image.id, question, {
model,
language,
previewOnly
});
return NextResponse.json({
success: true,
answer: result.answer,
dataset: result.dataset
});
} catch (error) {
console.error('Failed to generate image dataset:', error);
return NextResponse.json({ error: error.message || 'Failed to generate dataset' }, { status: 500 });
}
}

View File

@@ -0,0 +1,41 @@
import { NextResponse } from 'next/server';
import { PrismaClient } from '@prisma/client';
import { getImageDetailWithQuestions } from '@/lib/services/images';
const prisma = new PrismaClient();
// 获取下一个有未标注问题的图片
export async function GET(request, { params }) {
try {
const { projectId } = params;
// 查找第一个有未标注问题的图片
const unansweredQuestion = await prisma.questions.findFirst({
where: {
projectId,
imageId: {
not: null
},
answered: false
}
});
if (!unansweredQuestion) {
return NextResponse.json({
success: true,
data: null
});
}
// 调用服务层获取图片详情
const imageData = await getImageDetailWithQuestions(projectId, unansweredQuestion.imageId);
return NextResponse.json({
success: true,
data: imageData
});
} catch (error) {
console.error('Failed to get next unanswered image:', error);
return NextResponse.json({ error: error.message || 'Failed to get next unanswered image' }, { status: 500 });
}
}

View File

@@ -0,0 +1,98 @@
import { NextResponse } from 'next/server';
import { getProjectPath } from '@/lib/db/base';
import { importImagesFromDirectories } from '@/lib/services/images';
import fs from 'fs/promises';
import path from 'path';
import { savePdfAsImages } from '@/lib/util/file';
// PDF 转图片并导入
export async function POST(request, { params }) {
let tempPdfPath = null;
let tempImagesDir = null;
try {
const { projectId } = params;
const formData = await request.formData();
const pdfFile = formData.get('file');
if (!pdfFile) {
return NextResponse.json({ error: '请选择 PDF 文件' }, { status: 400 });
}
if (!pdfFile.name.toLowerCase().endsWith('.pdf')) {
return NextResponse.json({ error: '只支持 PDF 文件' }, { status: 400 });
}
const projectPath = await getProjectPath(projectId);
const tempDir = path.join(projectPath, 'temp');
await fs.mkdir(tempDir, { recursive: true });
// 1. 保存 PDF 到临时目录
tempPdfPath = path.join(tempDir, `temp_${Date.now()}_${pdfFile.name}`);
const pdfBuffer = Buffer.from(await pdfFile.arrayBuffer());
await fs.writeFile(tempPdfPath, pdfBuffer);
// 2. 创建临时图片目录
tempImagesDir = path.join(tempDir, `pdf_images_${Date.now()}`);
await fs.mkdir(tempImagesDir, { recursive: true });
// 3. 调用 pdf2md-js 转换 PDF 为图片
console.log('开始转换 PDF 为图片...');
const imagePaths = await savePdfAsImages(tempPdfPath, tempImagesDir, 3);
console.log('PDF 转换完成,生成图片数量:', imagePaths.length);
if (!imagePaths || imagePaths.length === 0) {
throw new Error('PDF 转换失败,未生成图片');
}
// 4. 直接调用服务层导入图片
const importResult = await importImagesFromDirectories(projectId, [tempImagesDir]);
// 5. 清理临时文件
try {
if (tempPdfPath) {
await fs.unlink(tempPdfPath);
}
if (tempImagesDir) {
const tempImages = await fs.readdir(tempImagesDir);
for (const img of tempImages) {
await fs.unlink(path.join(tempImagesDir, img));
}
await fs.rmdir(tempImagesDir);
}
const tempDirContents = await fs.readdir(tempDir);
if (tempDirContents.length === 0) {
await fs.rmdir(tempDir);
}
} catch (cleanupErr) {
console.warn('清理临时文件失败:', cleanupErr);
}
return NextResponse.json({
success: true,
count: importResult.count,
images: importResult.images,
pdfName: pdfFile.name
});
} catch (error) {
console.error('Failed to convert PDF:', error);
// 清理临时文件
try {
if (tempPdfPath) {
await fs.unlink(tempPdfPath).catch(() => {});
}
if (tempImagesDir) {
const tempImages = await fs.readdir(tempImagesDir).catch(() => []);
for (const img of tempImages) {
await fs.unlink(path.join(tempImagesDir, img)).catch(() => {});
}
await fs.rmdir(tempImagesDir).catch(() => {});
}
} catch (cleanupErr) {
console.warn('清理临时文件失败:', cleanupErr);
}
return NextResponse.json({ error: error.message || 'Failed to convert PDF' }, { status: 500 });
}
}

View File

@@ -0,0 +1,40 @@
import { NextResponse } from 'next/server';
import { getImageByName } from '@/lib/db/images';
import imageService from '@/lib/services/images';
// 生成图片问题
export async function POST(request, { params }) {
try {
const { projectId } = params;
const { imageName, count = 3, model, language = 'zh' } = await request.json();
if (!imageName) {
return NextResponse.json({ error: '缺少图片名称' }, { status: 400 });
}
if (!model) {
return NextResponse.json({ error: '请选择一个视觉模型' }, { status: 400 });
}
// 获取图片信息
const image = await getImageByName(projectId, imageName);
if (!image) {
return NextResponse.json({ error: '图片不存在' }, { status: 404 });
}
// 调用图片问题生成服务
const result = await imageService.generateQuestionsForImage(projectId, image.id, {
model,
language,
count
});
return NextResponse.json({
success: true,
questions: result.questions
});
} catch (error) {
console.error('Failed to generate image questions:', error);
return NextResponse.json({ error: error.message || 'Failed to generate questions' }, { status: 500 });
}
}

View File

@@ -0,0 +1,92 @@
import { NextResponse } from 'next/server';
import { getImages, deleteImage, getImageDetail } from '@/lib/db/images';
import { getProjectPath } from '@/lib/db/base';
import { db } from '@/lib/db/index';
import { importImagesFromDirectories } from '@/lib/services/images';
import fs from 'fs/promises';
import path from 'path';
// 获取图片列表
export async function GET(request, { params }) {
try {
const { projectId } = params;
const { searchParams } = new URL(request.url);
const page = parseInt(searchParams.get('page')) || 1;
const pageSize = parseInt(searchParams.get('pageSize')) || 20;
const imageName = searchParams.get('imageName') || '';
const hasQuestions = searchParams.get('hasQuestions');
const hasDatasets = searchParams.get('hasDatasets');
const simple = searchParams.get('simple');
const result = await getImages(projectId, page, pageSize, imageName, hasQuestions, hasDatasets, simple);
return NextResponse.json(result);
} catch (error) {
console.error('Failed to get images:', error);
return NextResponse.json({ error: error.message || 'Failed to get images' }, { status: 500 });
}
}
// 导入图片
export async function POST(request, { params }) {
try {
const { projectId } = params;
const { directories } = await request.json();
// 调用服务层处理图片导入
const result = await importImagesFromDirectories(projectId, directories);
return NextResponse.json(result);
} catch (error) {
console.error('Failed to import images:', error);
return NextResponse.json({ error: error.message || 'Failed to import images' }, { status: 500 });
}
}
// 删除图片
export async function DELETE(request, { params }) {
try {
const { projectId } = params;
const { searchParams } = new URL(request.url);
const imageId = searchParams.get('imageId');
if (!imageId) {
return NextResponse.json({ error: '缺少图片ID' }, { status: 400 });
}
// 获取图片信息
const image = await getImageDetail(imageId);
if (!image) {
return NextResponse.json({ error: '图片不存在' }, { status: 404 });
}
// 删除关联的数据集
await db.imageDatasets.deleteMany({
where: { imageId }
});
// 删除关联的问题
await db.questions.deleteMany({
where: { imageId }
});
// 删除文件
const projectPath = await getProjectPath(projectId);
const filePath = path.join(projectPath, 'images', image.imageName);
try {
await fs.unlink(filePath);
} catch (err) {
console.warn('删除文件失败:', err);
}
// 删除数据库记录
await deleteImage(imageId);
return NextResponse.json({ success: true });
} catch (error) {
console.error('Failed to delete image:', error);
return NextResponse.json({ error: error.message || 'Failed to delete image' }, { status: 500 });
}
}

View File

@@ -0,0 +1,127 @@
import { NextResponse } from 'next/server';
import { getProjectPath } from '@/lib/db/base';
import { importImagesFromDirectories } from '@/lib/services/images';
import fs from 'fs/promises';
import path from 'path';
import AdmZip from 'adm-zip';
// 压缩包解压并导入图片
export async function POST(request, { params }) {
let tempZipPath = null;
let tempExtractDir = null;
try {
const { projectId } = params;
const formData = await request.formData();
const zipFile = formData.get('file');
if (!zipFile) {
return NextResponse.json({ error: '请选择压缩包文件' }, { status: 400 });
}
if (!zipFile.name.toLowerCase().endsWith('.zip')) {
return NextResponse.json({ error: '只支持 ZIP 格式的压缩包' }, { status: 400 });
}
const projectPath = await getProjectPath(projectId);
const tempDir = path.join(projectPath, 'temp');
await fs.mkdir(tempDir, { recursive: true });
// 1. 保存压缩包到临时目录
tempZipPath = path.join(tempDir, `temp_${Date.now()}_${zipFile.name}`);
const zipBuffer = Buffer.from(await zipFile.arrayBuffer());
await fs.writeFile(tempZipPath, zipBuffer);
// 2. 创建临时解压目录
tempExtractDir = path.join(tempDir, `zip_extract_${Date.now()}`);
await fs.mkdir(tempExtractDir, { recursive: true });
// 3. 使用 adm-zip 解压文件
console.log('开始解压压缩包...');
const zip = new AdmZip(tempZipPath);
const zipEntries = zip.getEntries();
// 支持的图片扩展名
const imageExtensions = ['.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp', '.svg'];
let extractedCount = 0;
// 遍历压缩包中的所有文件
for (const entry of zipEntries) {
// 跳过目录和隐藏文件
if (
entry.isDirectory ||
entry.entryName.startsWith('__MACOSX') ||
path.basename(entry.entryName).startsWith('.')
) {
continue;
}
const ext = path.extname(entry.entryName).toLowerCase();
if (imageExtensions.includes(ext)) {
// 提取文件名(不包含路径)
const fileName = path.basename(entry.entryName);
const targetPath = path.join(tempExtractDir, fileName);
// 解压文件
zip.extractEntryTo(entry, tempExtractDir, false, true, false, fileName);
extractedCount++;
}
}
console.log(`压缩包解压完成,提取图片数量: ${extractedCount}`);
if (extractedCount === 0) {
throw new Error('压缩包中没有找到支持的图片文件');
}
// 4. 调用服务层导入图片
const importResult = await importImagesFromDirectories(projectId, [tempExtractDir]);
// 5. 清理临时文件
try {
if (tempZipPath) {
await fs.unlink(tempZipPath);
}
if (tempExtractDir) {
const tempImages = await fs.readdir(tempExtractDir);
for (const img of tempImages) {
await fs.unlink(path.join(tempExtractDir, img));
}
await fs.rmdir(tempExtractDir);
}
const tempDirContents = await fs.readdir(tempDir);
if (tempDirContents.length === 0) {
await fs.rmdir(tempDir);
}
} catch (cleanupErr) {
console.warn('清理临时文件失败:', cleanupErr);
}
return NextResponse.json({
success: true,
count: importResult.count,
images: importResult.images,
zipName: zipFile.name
});
} catch (error) {
console.error('Failed to import ZIP:', error);
// 清理临时文件
try {
if (tempZipPath) {
await fs.unlink(tempZipPath).catch(() => {});
}
if (tempExtractDir) {
const tempImages = await fs.readdir(tempExtractDir).catch(() => []);
for (const img of tempImages) {
await fs.unlink(path.join(tempExtractDir, img)).catch(() => {});
}
await fs.rmdir(tempExtractDir).catch(() => {});
}
} catch (cleanupErr) {
console.warn('清理临时文件失败:', cleanupErr);
}
return NextResponse.json({ error: error.message || 'Failed to import ZIP' }, { status: 500 });
}
}