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:
@@ -88,3 +88,6 @@ export async function moduleRoutes(fastify: FastifyInstance) {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -118,3 +118,6 @@ export interface PaginatedResponse<T> {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -165,3 +165,6 @@ export const ROLE_DISPLAY_NAMES: Record<UserRole, string> = {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -240,3 +240,6 @@ async function matchIntent(query: string): Promise<{
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -94,3 +94,6 @@ export async function uploadAttachment(
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -23,3 +23,6 @@ export { aiaRoutes };
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -363,6 +363,9 @@ runTests().catch((error) => {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -304,6 +304,9 @@ runTest()
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -342,6 +342,9 @@ Content-Type: application/json
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -278,6 +278,9 @@ export const conflictDetectionService = new ConflictDetectionService();
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -228,6 +228,9 @@ curl -X POST http://localhost:3000/api/v1/dc/tool-c/test/execute \
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -282,6 +282,9 @@ export const streamAIController = new StreamAIController();
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -191,6 +191,9 @@ logger.info('[SessionMemory] 会话记忆管理器已启动', {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -125,6 +125,9 @@ checkTableStructure();
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -112,6 +112,9 @@ checkProjectConfig().catch(console.error);
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -94,6 +94,9 @@ main();
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -551,6 +551,9 @@ URL: https://iit.xunzhengyixue.com/api/v1/iit/patient-wechat/callback
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -186,6 +186,9 @@ console.log('');
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -503,6 +503,9 @@ export const patientWechatService = new PatientWechatService();
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -148,6 +148,9 @@ testDifyIntegration().catch(error => {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -177,6 +177,9 @@ testIitDatabase()
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -163,6 +163,9 @@ if (hasError) {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -189,6 +189,9 @@ async function testUrlVerification() {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -270,6 +270,9 @@ main().catch((error) => {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -154,6 +154,9 @@ Write-Host ""
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -247,6 +247,9 @@ export interface CachedProtocolRules {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
|
||||
@@ -140,5 +140,8 @@ Content-Type: application/json
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -125,5 +125,8 @@ Write-Host " - 删除任务: DELETE $BaseUrl/api/v1/rvw/tasks/{taskId}" -Foregr
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -39,5 +39,8 @@ export * from './services/utils.js';
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -130,5 +130,8 @@ export function validateAgentSelection(agents: string[]): void {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user