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:
@@ -259,6 +259,9 @@ checkDCTables();
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -16,3 +16,6 @@ CREATE SCHEMA IF NOT EXISTS capability_schema;
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -211,6 +211,9 @@ createAiHistoryTable()
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -198,6 +198,9 @@ createToolCTable()
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -195,6 +195,9 @@ createToolCTable()
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -319,3 +319,6 @@ main()
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -126,3 +126,6 @@ main()
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
163
backend/scripts/test-oss.ts
Normal file
163
backend/scripts/test-oss.ts
Normal file
@@ -0,0 +1,163 @@
|
||||
/**
|
||||
* OSS存储适配器测试脚本
|
||||
*
|
||||
* 使用方法:
|
||||
* 1. 配置环境变量(.env 文件)
|
||||
* 2. 运行:npx tsx scripts/test-oss.ts
|
||||
*
|
||||
* 测试项:
|
||||
* - 上传文件(按 MVP 目录结构)
|
||||
* - 下载文件
|
||||
* - 检查文件存在
|
||||
* - 获取签名URL
|
||||
* - 【可选】删除文件
|
||||
*/
|
||||
|
||||
import dotenv from 'dotenv'
|
||||
import path from 'path'
|
||||
import fs from 'fs'
|
||||
import { fileURLToPath } from 'url'
|
||||
import { randomUUID } from 'crypto'
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url)
|
||||
const __dirname = path.dirname(__filename)
|
||||
|
||||
// 加载环境变量
|
||||
dotenv.config({ path: path.join(__dirname, '../.env') })
|
||||
|
||||
import { StorageFactory } from '../src/common/storage/StorageFactory.js'
|
||||
|
||||
/**
|
||||
* 生成存储 Key(按 MVP 目录结构)
|
||||
*
|
||||
* 格式:tenants/{tenantId}/users/{userId}/{module}/{uuid}.{ext}
|
||||
*/
|
||||
function generateStorageKey(
|
||||
tenantId: string,
|
||||
userId: string | null,
|
||||
module: string,
|
||||
filename: string
|
||||
): string {
|
||||
const uuid = randomUUID().replace(/-/g, '').substring(0, 16)
|
||||
const ext = path.extname(filename)
|
||||
|
||||
if (userId) {
|
||||
// 用户私有数据
|
||||
return `tenants/${tenantId}/users/${userId}/${module}/${uuid}${ext}`
|
||||
} else {
|
||||
// 租户共享数据
|
||||
return `tenants/${tenantId}/shared/${module}/${uuid}${ext}`
|
||||
}
|
||||
}
|
||||
|
||||
async function main() {
|
||||
console.log('='.repeat(60))
|
||||
console.log('OSS 存储适配器测试 - MVP 目录结构验证')
|
||||
console.log('='.repeat(60))
|
||||
|
||||
// 检查环境变量
|
||||
const storageType = process.env.STORAGE_TYPE || 'local'
|
||||
console.log(`\n📦 存储类型: ${storageType}`)
|
||||
|
||||
if (storageType === 'oss') {
|
||||
console.log(` Region: ${process.env.OSS_REGION}`)
|
||||
console.log(` Bucket: ${process.env.OSS_BUCKET}`)
|
||||
console.log(` Internal: ${process.env.OSS_INTERNAL}`)
|
||||
}
|
||||
|
||||
// 获取存储实例
|
||||
const storage = StorageFactory.getInstance()
|
||||
console.log(`\n✅ 存储实例创建成功`)
|
||||
|
||||
// ============================================================
|
||||
// 测试文件:真实 PDF 文献
|
||||
// ============================================================
|
||||
const testPdfPath = path.join(__dirname, '../../docs/06-测试文档/Ihl 2011.pdf')
|
||||
|
||||
if (!fs.existsSync(testPdfPath)) {
|
||||
console.error(`\n❌ 测试文件不存在: ${testPdfPath}`)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
const pdfBuffer = fs.readFileSync(testPdfPath)
|
||||
console.log(`\n📄 测试文件: Ihl 2011.pdf`)
|
||||
console.log(` 大小: ${(pdfBuffer.length / 1024).toFixed(2)} KB`)
|
||||
|
||||
// ============================================================
|
||||
// 按 MVP 目录结构生成 Key
|
||||
// ============================================================
|
||||
// 模拟:租户 yizhengxun,用户 test-user-001,模块 pkb
|
||||
const tenantId = 'yizhengxun'
|
||||
const userId = 'test-user-001'
|
||||
const module = 'pkb'
|
||||
|
||||
const storageKey = generateStorageKey(tenantId, userId, module, 'Ihl 2011.pdf')
|
||||
|
||||
console.log(`\n📁 目录结构验证:`)
|
||||
console.log(` 租户ID: ${tenantId}`)
|
||||
console.log(` 用户ID: ${userId}`)
|
||||
console.log(` 模块: ${module}`)
|
||||
console.log(` 生成Key: ${storageKey}`)
|
||||
|
||||
try {
|
||||
// 1. 上传测试
|
||||
console.log(`\n📤 上传文件到 OSS...`)
|
||||
const uploadUrl = await storage.upload(storageKey, pdfBuffer)
|
||||
console.log(` ✅ 上传成功!`)
|
||||
console.log(` 签名URL: ${uploadUrl.substring(0, 80)}...`)
|
||||
|
||||
// 2. 检查存在
|
||||
console.log(`\n🔍 验证文件存在...`)
|
||||
const exists = await storage.exists(storageKey)
|
||||
console.log(` 存在: ${exists ? '✅ 是' : '❌ 否'}`)
|
||||
|
||||
// 3. 下载验证
|
||||
console.log(`\n📥 下载验证...`)
|
||||
const downloadBuffer = await storage.download(storageKey)
|
||||
const sizeMatch = downloadBuffer.length === pdfBuffer.length
|
||||
console.log(` 下载大小: ${(downloadBuffer.length / 1024).toFixed(2)} KB`)
|
||||
console.log(` 大小匹配: ${sizeMatch ? '✅ 是' : '❌ 否'}`)
|
||||
|
||||
// 4. 获取URL(不带原始文件名)
|
||||
console.log(`\n🔗 获取访问URL...`)
|
||||
const url = storage.getUrl(storageKey)
|
||||
console.log(` URL: ${url.substring(0, 80)}...`)
|
||||
|
||||
// 5. 获取URL(带原始文件名 - 下载时恢复文件名)
|
||||
console.log(`\n📎 获取带原始文件名的URL...`)
|
||||
// 类型断言访问 OSSAdapter 的 getSignedUrl 方法
|
||||
const ossAdapter = storage as any
|
||||
if (typeof ossAdapter.getSignedUrl === 'function') {
|
||||
const urlWithFilename = ossAdapter.getSignedUrl(storageKey, 3600, 'Ihl 2011.pdf')
|
||||
console.log(` 原始文件名: Ihl 2011.pdf`)
|
||||
console.log(` URL: ${urlWithFilename.substring(0, 80)}...`)
|
||||
console.log(` ✅ 下载此URL时,浏览器会保存为 "Ihl 2011.pdf"`)
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 🔴 不删除文件!保留在 OSS 中供验证
|
||||
// ============================================================
|
||||
console.log(`\n⚠️ 文件已保留在 OSS 中,不删除!`)
|
||||
console.log(` 请登录阿里云 OSS 控制台查看:`)
|
||||
console.log(` https://oss.console.aliyun.com/bucket/oss-cn-beijing/ai-clinical-data-dev/object`)
|
||||
console.log(`\n 文件路径: ${storageKey}`)
|
||||
|
||||
// 测试完成
|
||||
console.log('\n' + '='.repeat(60))
|
||||
console.log('🎉 测试完成!请到 OSS 控制台验证目录结构')
|
||||
console.log('='.repeat(60))
|
||||
|
||||
// 输出完整信息供验证
|
||||
console.log(`\n📋 验证信息:`)
|
||||
console.log(` Bucket: ai-clinical-data-dev`)
|
||||
console.log(` Key: ${storageKey}`)
|
||||
console.log(` 预期目录: tenants/yizhengxun/users/test-user-001/pkb/`)
|
||||
|
||||
} catch (error) {
|
||||
console.error('\n❌ 测试失败:', error)
|
||||
process.exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
// 运行测试
|
||||
main().catch(console.error)
|
||||
@@ -342,6 +342,9 @@ runTests().catch(error => {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -92,3 +92,6 @@ testAPI().catch(console.error);
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -122,3 +122,6 @@ testDeepSearch().catch(console.error);
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -307,6 +307,9 @@ verifySchemas()
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user