first-update
This commit is contained in:
@@ -0,0 +1,313 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { getGaPairsByFileId, toggleGaPairActive, saveGaPairs, createGaPairs } from '@/lib/db/ga-pairs';
|
||||
import { getUploadFileInfoById } from '@/lib/db/upload-files';
|
||||
import { generateGaPairs } from '@/lib/services/ga/ga-generation';
|
||||
import logger from '@/lib/util/logger';
|
||||
import { db } from '@/lib/db/index';
|
||||
|
||||
/**
|
||||
* 生成文件的 GA 对
|
||||
*/
|
||||
export async function POST(request, { params }) {
|
||||
try {
|
||||
const { projectId, fileId } = params;
|
||||
const { regenerate = false, appendMode = false, language = '中文' } = await request.json();
|
||||
|
||||
// 验证参数
|
||||
if (!projectId || !fileId) {
|
||||
return NextResponse.json({ error: 'Project ID and File ID are required' }, { status: 400 });
|
||||
}
|
||||
|
||||
logger.info(`Starting GA pairs generation for project: ${projectId}, file: ${fileId}, appendMode: ${appendMode}`);
|
||||
|
||||
// 检查文件是否存在
|
||||
const file = await getUploadFileInfoById(fileId);
|
||||
if (!file || file.projectId !== projectId) {
|
||||
return NextResponse.json({ error: 'File not found or does not belong to the project' }, { status: 404 });
|
||||
}
|
||||
|
||||
// 获取现有的GA对
|
||||
const existingGaPairs = await getGaPairsByFileId(fileId);
|
||||
|
||||
// 如果是追加模式且已有GA对,或者不是重新生成且已存在GA对
|
||||
if (!regenerate && !appendMode && existingGaPairs.length > 0) {
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
message: 'GA pairs already exist for this file',
|
||||
data: existingGaPairs
|
||||
});
|
||||
}
|
||||
|
||||
// 读取文件内容
|
||||
const fileContent = await getFileContent(projectId, file.fileName);
|
||||
if (!fileContent) {
|
||||
return NextResponse.json({ error: 'Failed to read file content' }, { status: 500 });
|
||||
}
|
||||
|
||||
logger.info(`File content loaded successfully, length: ${fileContent.length}`);
|
||||
|
||||
// 检查模型配置
|
||||
try {
|
||||
const { getActiveModel } = await import('@/lib/services/models');
|
||||
const activeModel = await getActiveModel(projectId);
|
||||
|
||||
if (!activeModel) {
|
||||
logger.error('No active model configuration found');
|
||||
return NextResponse.json(
|
||||
{ error: 'No active AI model configured. Please configure a model in settings first.' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
logger.info(`Using active model: ${activeModel.provider} - ${activeModel.model}`);
|
||||
} catch (modelError) {
|
||||
logger.error('Error checking model configuration:', modelError);
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to load model configuration. Please check your AI model settings.' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
|
||||
// 调用 LLM 生成 GA 对
|
||||
logger.info(`Generating GA pairs for file: ${file.fileName}`);
|
||||
let generatedGaPairs;
|
||||
|
||||
try {
|
||||
generatedGaPairs = await generateGaPairs(fileContent, projectId, language);
|
||||
|
||||
if (!generatedGaPairs || generatedGaPairs.length === 0) {
|
||||
logger.warn('No GA pairs generated from LLM');
|
||||
return NextResponse.json(
|
||||
{
|
||||
error:
|
||||
'No GA pairs could be generated from the file content. The content might be too short or not suitable for GA pair generation.'
|
||||
},
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
logger.info(`Successfully generated ${generatedGaPairs.length} GA pairs from LLM`);
|
||||
} catch (generationError) {
|
||||
logger.error('GA pairs generation failed:', generationError);
|
||||
|
||||
// 现有的错误处理逻辑...
|
||||
let errorMessage = 'Failed to generate GA pairs';
|
||||
if (generationError.message.includes('No active model')) {
|
||||
errorMessage = 'No active AI model available. Please configure and activate a model in settings.';
|
||||
} else if (generationError.message.includes('API key')) {
|
||||
errorMessage = 'Invalid API key or model configuration. Please check your AI model settings.';
|
||||
} else if (generationError.message.includes('rate limit')) {
|
||||
errorMessage = 'API rate limit exceeded. Please try again later.';
|
||||
} else {
|
||||
errorMessage = `AI model error: ${generationError.message}`;
|
||||
}
|
||||
|
||||
return NextResponse.json({ error: errorMessage }, { status: 500 });
|
||||
}
|
||||
|
||||
// 保存到数据库
|
||||
try {
|
||||
if (appendMode && existingGaPairs.length > 0) {
|
||||
// 追加模式:只保存新生成的GA对,不删除现有的
|
||||
logger.info(`Appending ${generatedGaPairs.length} new GA pairs to existing ${existingGaPairs.length} pairs`);
|
||||
|
||||
// 为新GA对设置正确的pairNumber
|
||||
const startPairNumber = existingGaPairs.length + 1;
|
||||
const newGaPairData = generatedGaPairs.map((pair, index) => ({
|
||||
projectId,
|
||||
fileId,
|
||||
pairNumber: startPairNumber + index,
|
||||
genreTitle: pair.genre?.title || pair.genreTitle || '',
|
||||
genreDesc: pair.genre?.description || pair.genreDesc || '',
|
||||
audienceTitle: pair.audience?.title || pair.audienceTitle || '',
|
||||
audienceDesc: pair.audience?.description || pair.audienceDesc || '',
|
||||
isActive: true
|
||||
}));
|
||||
|
||||
// 只创建新的GA对,不删除现有的
|
||||
await createGaPairs(newGaPairData);
|
||||
logger.info('New GA pairs appended to database successfully');
|
||||
} else {
|
||||
// 覆盖模式:删除现有的,保存新的
|
||||
await saveGaPairs(projectId, fileId, generatedGaPairs);
|
||||
logger.info('GA pairs saved to database successfully');
|
||||
}
|
||||
} catch (saveError) {
|
||||
logger.error('Failed to save GA pairs to database:', saveError);
|
||||
return NextResponse.json(
|
||||
{ error: 'Generated GA pairs successfully but failed to save to database' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
|
||||
// 获取保存后的所有GA对
|
||||
const allGaPairs = await getGaPairsByFileId(fileId);
|
||||
|
||||
if (appendMode && existingGaPairs.length > 0) {
|
||||
// 追加模式:只返回新生成的GA对
|
||||
const newGaPairs = allGaPairs.slice(existingGaPairs.length);
|
||||
logger.info(`Successfully appended ${newGaPairs.length} GA pairs. Total pairs: ${allGaPairs.length}`);
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
message: `${newGaPairs.length} new GA pairs appended successfully`,
|
||||
data: newGaPairs,
|
||||
total: allGaPairs.length
|
||||
});
|
||||
} else {
|
||||
// 覆盖模式:返回所有GA对
|
||||
logger.info(`Successfully generated and saved ${allGaPairs.length} GA pairs for file: ${file.fileName}`);
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
message: 'GA pairs generated successfully',
|
||||
data: allGaPairs
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('Unexpected error in GA pairs generation:', error);
|
||||
return NextResponse.json(
|
||||
{ error: error.message || 'Unexpected error occurred during GA pairs generation' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取文件的 GA 对
|
||||
*/
|
||||
export async function GET(request, { params }) {
|
||||
try {
|
||||
const { projectId, fileId } = params;
|
||||
|
||||
if (!projectId || !fileId) {
|
||||
return NextResponse.json({ error: 'Project ID and File ID are required' }, { status: 400 });
|
||||
}
|
||||
|
||||
const gaPairs = await getGaPairsByFileId(fileId);
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
data: gaPairs
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error getting GA pairs:', String(error));
|
||||
return NextResponse.json({ error: 'Failed to get GA pairs' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新/替换文件的所有 GA 对
|
||||
*/
|
||||
export async function PUT(request, { params }) {
|
||||
try {
|
||||
const { projectId, fileId } = params;
|
||||
const body = await request.json();
|
||||
|
||||
if (!projectId || !fileId) {
|
||||
return NextResponse.json({ error: 'Project ID and File ID are required' }, { status: 400 });
|
||||
}
|
||||
|
||||
const { updates } = body;
|
||||
|
||||
if (!updates || !Array.isArray(updates)) {
|
||||
return NextResponse.json({ error: 'Updates array is required' }, { status: 400 });
|
||||
}
|
||||
|
||||
logger.info(`Replacing all GA pairs for file ${fileId} with ${updates.length} pairs`);
|
||||
|
||||
// 使用数据库事务确保原子性操作
|
||||
const results = await db.$transaction(async tx => {
|
||||
// 1. 先删除所有现有的GA对
|
||||
await tx.gaPairs.deleteMany({
|
||||
where: { fileId }
|
||||
});
|
||||
|
||||
// 2. 然后创建新的GA对
|
||||
if (updates.length > 0) {
|
||||
const gaPairData = updates.map((pair, index) => ({
|
||||
projectId,
|
||||
fileId,
|
||||
pairNumber: index + 1,
|
||||
genreTitle: pair.genreTitle || pair.genre?.title || pair.genre || '',
|
||||
genreDesc: pair.genreDesc || pair.genre?.description || '',
|
||||
audienceTitle: pair.audienceTitle || pair.audience?.title || pair.audience || '',
|
||||
audienceDesc: pair.audienceDesc || pair.audience?.description || '',
|
||||
isActive: pair.isActive !== undefined ? pair.isActive : true
|
||||
}));
|
||||
|
||||
// 验证数据
|
||||
for (const data of gaPairData) {
|
||||
if (!data.genreTitle || !data.audienceTitle) {
|
||||
throw new Error(`Invalid GA pair data: missing genre or audience title`);
|
||||
}
|
||||
}
|
||||
|
||||
await tx.gaPairs.createMany({ data: gaPairData });
|
||||
}
|
||||
|
||||
// 3. 返回新创建的GA对
|
||||
return await tx.gaPairs.findMany({
|
||||
where: { fileId },
|
||||
orderBy: { pairNumber: 'asc' }
|
||||
});
|
||||
});
|
||||
|
||||
logger.info(`Successfully replaced GA pairs, new count: ${results.length}`);
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
data: results
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Error updating GA pairs:', error);
|
||||
return NextResponse.json({ error: error.message || 'Failed to update GA pairs' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 切换 GA 对激活状态
|
||||
*/
|
||||
export async function PATCH(request, { params }) {
|
||||
try {
|
||||
const { projectId, fileId } = params;
|
||||
const body = await request.json();
|
||||
|
||||
if (!projectId || !fileId) {
|
||||
return NextResponse.json({ error: 'Project ID and File ID are required' }, { status: 400 });
|
||||
}
|
||||
|
||||
const { gaPairId, isActive } = body;
|
||||
|
||||
if (!gaPairId || typeof isActive !== 'boolean') {
|
||||
return NextResponse.json({ error: 'GA pair ID and active status are required' }, { status: 400 });
|
||||
}
|
||||
|
||||
const updatedPair = await toggleGaPairActive(gaPairId, isActive);
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
data: updatedPair
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error toggling GA pair active status:', String(error));
|
||||
return NextResponse.json({ error: 'Failed to toggle GA pair active status' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to read file content
|
||||
async function getFileContent(projectId, fileName) {
|
||||
try {
|
||||
const { getProjectRoot } = await import('@/lib/db/base');
|
||||
const path = await import('path');
|
||||
const fs = await import('fs');
|
||||
|
||||
const projectRoot = await getProjectRoot();
|
||||
const filePath = path.join(projectRoot, projectId, 'files', fileName.replace('.pdf', '.md'));
|
||||
|
||||
return await fs.promises.readFile(filePath, 'utf8');
|
||||
} catch (error) {
|
||||
logger.error('Failed to read file content:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
243
easy-dataset-main/app/api/projects/[projectId]/files/route.js
Normal file
243
easy-dataset-main/app/api/projects/[projectId]/files/route.js
Normal file
@@ -0,0 +1,243 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { getProject } from '@/lib/db/projects';
|
||||
import path from 'path';
|
||||
import { getProjectRoot, ensureDir } from '@/lib/db/base';
|
||||
import { promises as fs } from 'fs';
|
||||
import {
|
||||
checkUploadFileInfoByMD5,
|
||||
createUploadFileInfo,
|
||||
delUploadFileInfoById,
|
||||
getUploadFilesPagination
|
||||
} from '@/lib/db/upload-files';
|
||||
import { getFileMD5 } from '@/lib/util/file';
|
||||
import { batchSaveTags } from '@/lib/db/tags';
|
||||
import { getProjectChunks, getProjectTocByName } from '@/lib/file/text-splitter';
|
||||
import { handleDomainTree } from '@/lib/util/domain-tree';
|
||||
|
||||
// Replace the deprecated config export with the new export syntax
|
||||
export const dynamic = 'force-dynamic';
|
||||
// This tells Next.js not to parse the request body automatically
|
||||
export const bodyParser = false;
|
||||
|
||||
// 获取项目文件列表
|
||||
export async function GET(request, { params }) {
|
||||
try {
|
||||
const { projectId } = params;
|
||||
|
||||
// 验证项目ID
|
||||
if (!projectId) {
|
||||
return NextResponse.json({ error: 'The project ID cannot be empty' }, { status: 400 });
|
||||
}
|
||||
const { searchParams } = new URL(request.url);
|
||||
const page = parseInt(searchParams.get('page')) || 1;
|
||||
const pageSize = parseInt(searchParams.get('pageSize')) || 10; // 每页10个文件,支持分页
|
||||
const fileName = searchParams.get('fileName') || '';
|
||||
const getAllIds = searchParams.get('getAllIds') === 'true'; // 新增:获取所有文件ID的标志
|
||||
|
||||
// 如果请求所有文件ID,直接返回ID列表
|
||||
if (getAllIds) {
|
||||
const allFiles = await getUploadFilesPagination(projectId, 1, 9999, fileName); // 获取所有文件
|
||||
const allFileIds = allFiles.data?.map(file => String(file.id)) || [];
|
||||
return NextResponse.json({ allFileIds });
|
||||
}
|
||||
// 获取文件列表
|
||||
const files = await getUploadFilesPagination(projectId, page, pageSize, fileName);
|
||||
|
||||
return NextResponse.json(files);
|
||||
} catch (error) {
|
||||
console.error('Error obtaining file list:', String(error));
|
||||
return NextResponse.json({ error: error.message || 'Error obtaining file list' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
// 删除文件
|
||||
export async function DELETE(request, { params }) {
|
||||
try {
|
||||
const { projectId } = params;
|
||||
const { searchParams } = new URL(request.url);
|
||||
const fileId = searchParams.get('fileId');
|
||||
const domainTreeAction = searchParams.get('domainTreeAction') || 'keep';
|
||||
|
||||
// 从请求体中获取模型信息和语言环境
|
||||
const requestData = await request.json();
|
||||
const model = requestData.model;
|
||||
const language = requestData.language || 'en';
|
||||
|
||||
// 验证项目ID和文件名
|
||||
if (!projectId) {
|
||||
return NextResponse.json({ error: 'The project ID cannot be empty' }, { status: 400 });
|
||||
}
|
||||
|
||||
if (!fileId) {
|
||||
return NextResponse.json({ error: 'The file name cannot be empty' }, { status: 400 });
|
||||
}
|
||||
|
||||
// 获取项目信息
|
||||
const project = await getProject(projectId);
|
||||
if (!project) {
|
||||
return NextResponse.json({ error: 'The project does not exist' }, { status: 404 });
|
||||
}
|
||||
|
||||
// 删除文件及其相关的文本块、问题和数据集
|
||||
const { stats, fileName, fileInfo } = await delUploadFileInfoById(fileId);
|
||||
const deleteToc = await getProjectTocByName(projectId, fileName);
|
||||
try {
|
||||
const projectRoot = await getProjectRoot();
|
||||
const projectPath = path.join(projectRoot, projectId);
|
||||
const tocDir = path.join(projectPath, 'toc');
|
||||
const baseName = path.basename(fileInfo.fileName, path.extname(fileInfo.fileName));
|
||||
const tocPath = path.join(tocDir, `${baseName}-toc.json`);
|
||||
|
||||
// 检查文件是否存在再删除
|
||||
await fs.unlink(tocPath);
|
||||
console.log(`成功删除 TOC 文件: ${tocPath}`);
|
||||
} catch (error) {
|
||||
console.error(`删除 TOC 文件失败:`, String(error));
|
||||
// 即使 TOC 文件删除失败,不影响整体结果
|
||||
}
|
||||
|
||||
// 如果选择了保持领域树不变,直接返回删除结果
|
||||
if (domainTreeAction === 'keep') {
|
||||
return NextResponse.json({
|
||||
message: '文件删除成功',
|
||||
stats: stats,
|
||||
domainTreeAction: 'keep',
|
||||
cascadeDelete: true
|
||||
});
|
||||
}
|
||||
|
||||
// 处理领域树更新
|
||||
try {
|
||||
// 获取项目的所有文件
|
||||
const { chunks, toc } = await getProjectChunks(projectId);
|
||||
|
||||
// 如果不存在文本块,说明项目已经没有文件了
|
||||
if (!chunks || chunks.length === 0) {
|
||||
// 清空领域树
|
||||
await batchSaveTags(projectId, []);
|
||||
return NextResponse.json({
|
||||
message: '文件删除成功,领域树已清空',
|
||||
stats: stats,
|
||||
domainTreeAction,
|
||||
cascadeDelete: true
|
||||
});
|
||||
}
|
||||
|
||||
// 调用领域树处理模块
|
||||
await handleDomainTree({
|
||||
projectId,
|
||||
action: domainTreeAction,
|
||||
allToc: toc,
|
||||
model,
|
||||
language,
|
||||
deleteToc,
|
||||
project
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error updating domain tree after file deletion:', String(error));
|
||||
// 即使领域树更新失败,也不影响文件删除的结果
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
message: '文件删除成功',
|
||||
stats: stats,
|
||||
domainTreeAction,
|
||||
cascadeDelete: true
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error deleting file:', String(error));
|
||||
return NextResponse.json({ error: error.message || 'Error deleting file' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
// 上传文件
|
||||
export async function POST(request, { params }) {
|
||||
console.log('File upload request processing, parameters:', params);
|
||||
const { projectId } = params;
|
||||
|
||||
// 验证项目ID
|
||||
if (!projectId) {
|
||||
console.log('The project ID cannot be empty, returning 400 error');
|
||||
return NextResponse.json({ error: 'The project ID cannot be empty' }, { status: 400 });
|
||||
}
|
||||
|
||||
// 获取项目信息
|
||||
const project = await getProject(projectId);
|
||||
if (!project) {
|
||||
console.log('The project does not exist, returning 404 error');
|
||||
return NextResponse.json({ error: 'The project does not exist' }, { status: 404 });
|
||||
}
|
||||
console.log('Project information retrieved successfully:', project.name || project.id);
|
||||
|
||||
try {
|
||||
console.log('Try using alternate methods for file upload...');
|
||||
|
||||
// 检查请求头中是否包含文件名
|
||||
const encodedFileName = request.headers.get('x-file-name');
|
||||
const fileName = encodedFileName ? decodeURIComponent(encodedFileName) : null;
|
||||
console.log('Get file name from request header:', fileName);
|
||||
|
||||
if (!fileName) {
|
||||
console.log('The request header does not contain a file name');
|
||||
return NextResponse.json(
|
||||
{ error: 'The request header does not contain a file name (x-file-name)' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// 检查文件类型
|
||||
if (!fileName.endsWith('.md') && !fileName.endsWith('.pdf')) {
|
||||
return NextResponse.json({ error: 'Only Markdown files are supported' }, { status: 400 });
|
||||
}
|
||||
|
||||
// 直接从请求体中读取二进制数据
|
||||
const fileBuffer = Buffer.from(await request.arrayBuffer());
|
||||
|
||||
// 保存文件
|
||||
const projectRoot = await getProjectRoot();
|
||||
const projectPath = path.join(projectRoot, projectId);
|
||||
const filesDir = path.join(projectPath, 'files');
|
||||
|
||||
await ensureDir(filesDir);
|
||||
|
||||
const filePath = path.join(filesDir, fileName);
|
||||
await fs.writeFile(filePath, fileBuffer);
|
||||
//获取文件大小
|
||||
const stats = await fs.stat(filePath);
|
||||
//获取文件md5
|
||||
const md5 = await getFileMD5(filePath);
|
||||
//获取文件扩展名
|
||||
const ext = path.extname(filePath);
|
||||
|
||||
// let res = await checkUploadFileInfoByMD5(projectId, md5);
|
||||
// if (res) {
|
||||
// return NextResponse.json({ error: `【${fileName}】该文件已在此项目中存在` }, { status: 400 });
|
||||
// }
|
||||
|
||||
let fileInfo = await createUploadFileInfo({
|
||||
projectId,
|
||||
fileName,
|
||||
size: stats.size,
|
||||
md5,
|
||||
fileExt: ext,
|
||||
path: filesDir
|
||||
});
|
||||
|
||||
console.log('The file upload process is complete, and a successful response is returned');
|
||||
return NextResponse.json({
|
||||
message: 'File uploaded successfully',
|
||||
fileName,
|
||||
filePath,
|
||||
fileId: fileInfo.id
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error processing file upload:', String(error));
|
||||
console.error('Error stack:', error.stack);
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: 'File upload failed: ' + (error.message || 'Unknown error')
|
||||
},
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user