feat(storage): integrate Alibaba Cloud OSS for file persistence - Add OSSAdapter and LocalAdapter with StorageFactory pattern - Integrate PKB module with OSS upload - Rename difyDocumentId to storageKey - Create 4 OSS buckets and development specification
This commit is contained in:
@@ -1,5 +1,8 @@
|
||||
import type { FastifyRequest, FastifyReply } from 'fastify';
|
||||
import * as documentService from '../services/documentService.js';
|
||||
import { storage } from '../../../common/storage/index.js';
|
||||
import { randomUUID } from 'crypto';
|
||||
import path from 'path';
|
||||
|
||||
/**
|
||||
* 获取用户ID(从JWT Token中获取)
|
||||
@@ -12,6 +15,30 @@ function getUserId(request: FastifyRequest): string {
|
||||
return userId;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取租户ID(从JWT Token中获取)
|
||||
*/
|
||||
function getTenantId(request: FastifyRequest): string {
|
||||
const tenantId = (request as any).user?.tenantId;
|
||||
// 如果没有租户ID,使用默认值
|
||||
return tenantId || 'default';
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成 PKB 文档存储 Key
|
||||
* 格式:tenants/{tenantId}/users/{userId}/pkb/{kbId}/{uuid}.{ext}
|
||||
*/
|
||||
function generatePkbStorageKey(
|
||||
tenantId: string,
|
||||
userId: string,
|
||||
kbId: string,
|
||||
filename: string
|
||||
): string {
|
||||
const uuid = randomUUID().replace(/-/g, '').substring(0, 16);
|
||||
const ext = path.extname(filename).toLowerCase();
|
||||
return `tenants/${tenantId}/users/${userId}/pkb/${kbId}/${uuid}${ext}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 上传文档
|
||||
*/
|
||||
@@ -45,15 +72,15 @@ export async function uploadDocument(
|
||||
const fileType = data.mimetype;
|
||||
const fileSizeBytes = file.length;
|
||||
|
||||
// 文件大小限制(10MB)
|
||||
const maxSize = 10 * 1024 * 1024;
|
||||
console.log(`📊 文件大小: ${(fileSizeBytes / 1024 / 1024).toFixed(2)}MB (限制: 10MB)`);
|
||||
// 文件大小限制(30MB - 按 OSS 规范)
|
||||
const maxSize = 30 * 1024 * 1024;
|
||||
console.log(`📊 文件大小: ${(fileSizeBytes / 1024 / 1024).toFixed(2)}MB (限制: 30MB)`);
|
||||
|
||||
if (fileSizeBytes > maxSize) {
|
||||
console.error(`❌ 文件太大: ${(fileSizeBytes / 1024 / 1024).toFixed(2)}MB`);
|
||||
return reply.status(400).send({
|
||||
success: false,
|
||||
message: 'File size exceeds 10MB limit',
|
||||
message: 'File size exceeds 30MB limit',
|
||||
});
|
||||
}
|
||||
|
||||
@@ -75,9 +102,30 @@ export async function uploadDocument(
|
||||
});
|
||||
}
|
||||
|
||||
// 上传文档(这里fileUrl暂时为空,实际应该上传到对象存储)
|
||||
console.log(`⚙️ 调用文档服务上传文件...`);
|
||||
// 获取用户信息
|
||||
const userId = getUserId(request);
|
||||
const tenantId = getTenantId(request);
|
||||
|
||||
// 生成 OSS 存储 Key(包含 kbId)
|
||||
const storageKey = generatePkbStorageKey(tenantId, userId, kbId, filename);
|
||||
console.log(`📦 OSS 存储路径: ${storageKey}`);
|
||||
|
||||
// 上传到 OSS
|
||||
console.log(`☁️ 上传文件到存储服务...`);
|
||||
let fileUrl = '';
|
||||
try {
|
||||
fileUrl = await storage.upload(storageKey, file);
|
||||
console.log(`✅ 文件已上传到存储服务`);
|
||||
} catch (storageError) {
|
||||
console.error(`❌ 存储服务上传失败:`, storageError);
|
||||
return reply.status(500).send({
|
||||
success: false,
|
||||
message: 'Failed to upload file to storage',
|
||||
});
|
||||
}
|
||||
|
||||
// 调用文档服务处理(传入 storageKey)
|
||||
console.log(`⚙️ 调用文档服务处理文件...`);
|
||||
const document = await documentService.uploadDocument(
|
||||
userId,
|
||||
kbId,
|
||||
@@ -85,7 +133,8 @@ export async function uploadDocument(
|
||||
filename,
|
||||
fileType,
|
||||
fileSizeBytes,
|
||||
'' // fileUrl - 可以上传到OSS后填入
|
||||
fileUrl,
|
||||
storageKey // 新增:存储路径
|
||||
);
|
||||
|
||||
console.log(`✅ 文档上传成功: ${document.id}`);
|
||||
|
||||
@@ -60,6 +60,9 @@ export default async function healthRoutes(fastify: FastifyInstance) {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import { prisma } from '../../../config/database.js';
|
||||
import { logger } from '../../../common/logging/index.js';
|
||||
import { extractionClient } from '../../../common/document/ExtractionClient.js';
|
||||
import { ingestDocument as ragIngestDocument } from './ragService.js';
|
||||
import { storage } from '../../../common/storage/index.js';
|
||||
|
||||
/**
|
||||
* 文档服务
|
||||
@@ -11,6 +12,15 @@ import { ingestDocument as ragIngestDocument } from './ragService.js';
|
||||
|
||||
/**
|
||||
* 上传文档到知识库
|
||||
*
|
||||
* @param userId - 用户ID
|
||||
* @param kbId - 知识库ID
|
||||
* @param file - 文件内容 Buffer
|
||||
* @param filename - 原始文件名
|
||||
* @param fileType - MIME 类型
|
||||
* @param fileSizeBytes - 文件大小(字节)
|
||||
* @param fileUrl - 文件访问 URL(签名URL)
|
||||
* @param storageKey - OSS 存储路径(新增)
|
||||
*/
|
||||
export async function uploadDocument(
|
||||
userId: string,
|
||||
@@ -19,7 +29,8 @@ export async function uploadDocument(
|
||||
filename: string,
|
||||
fileType: string,
|
||||
fileSizeBytes: number,
|
||||
fileUrl: string
|
||||
fileUrl: string,
|
||||
storageKey?: string // 新增:OSS 存储路径
|
||||
) {
|
||||
// 1. 验证知识库权限
|
||||
const knowledgeBase = await prisma.knowledgeBase.findFirst({
|
||||
@@ -65,7 +76,7 @@ export async function uploadDocument(
|
||||
fileType,
|
||||
fileSizeBytes,
|
||||
fileUrl,
|
||||
difyDocumentId: '', // 不再使用
|
||||
storageKey: storageKey || '', // OSS 存储路径
|
||||
status: 'uploading',
|
||||
progress: 0,
|
||||
},
|
||||
@@ -107,10 +118,10 @@ export async function uploadDocument(
|
||||
});
|
||||
|
||||
// 7. 更新文档记录 - pgvector 模式立即完成
|
||||
// 注意:storageKey 已在创建时设置,这里不需要更新
|
||||
const updatedDocument = await prisma.document.update({
|
||||
where: { id: document.id },
|
||||
data: {
|
||||
difyDocumentId: ingestResult.documentId || '',
|
||||
status: 'completed',
|
||||
progress: 100,
|
||||
// 提取信息
|
||||
@@ -234,13 +245,23 @@ export async function deleteDocument(userId: string, documentId: string) {
|
||||
|
||||
logger.info(`[PKB] 删除文档: documentId=${documentId}`);
|
||||
|
||||
// 1.5 删除 OSS 文件
|
||||
if (document.storageKey) {
|
||||
try {
|
||||
await storage.delete(document.storageKey);
|
||||
logger.info(`[PKB] OSS 文件已删除: ${document.storageKey}`);
|
||||
} catch (storageError) {
|
||||
logger.warn(`[PKB] OSS 文件删除失败,继续删除数据库记录`, { storageError });
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 删除 EKB 中的文档和 Chunks
|
||||
try {
|
||||
// 查找 EKB 文档(通过 filename 和 kbId 匹配)
|
||||
const ekbDoc = await prisma.ekbDocument.findFirst({
|
||||
where: {
|
||||
filename: document.filename,
|
||||
kb: {
|
||||
knowledgeBase: {
|
||||
ownerId: userId,
|
||||
name: document.knowledgeBase.name,
|
||||
},
|
||||
@@ -311,7 +332,7 @@ export async function reprocessDocument(userId: string, documentId: string) {
|
||||
const ekbDoc = await prisma.ekbDocument.findFirst({
|
||||
where: {
|
||||
filename: document.filename,
|
||||
kb: {
|
||||
knowledgeBase: {
|
||||
ownerId: userId,
|
||||
name: document.knowledgeBase.name,
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user