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:
2026-01-22 22:02:20 +08:00
parent 483c62fb6f
commit 9c96f75c52
309 changed files with 4583 additions and 172 deletions

View File

@@ -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}`);

View File

@@ -60,6 +60,9 @@ export default async function healthRoutes(fastify: FastifyInstance) {

View File

@@ -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,
},