feat(admin): Implement System Knowledge Base management module

Features:

- Backend: SystemKbService with full CRUD (knowledge bases + documents)

- Backend: 8 RESTful API endpoints (list/detail/create/update/delete/upload/download)

- Backend: OSS storage integration (system/knowledge-bases/{kbId}/{docId})

- Backend: RAG engine integration (document parsing, chunking, vectorization)

- Frontend: SystemKbListPage with card-based layout

- Frontend: SystemKbDetailPage with document management table

- Frontend: Master-Detail UX pattern for better user experience

- Document upload (single/batch), download (preserving original filename), delete

Technical:

- Database migration for system_knowledge_bases and system_kb_documents tables

- OSSAdapter.getSignedUrl with Content-Disposition for original filename

- Reuse RAG engine from common/rag for document processing

Tested: Local environment verified, all features working
This commit is contained in:
2026-01-28 21:57:44 +08:00
parent 3a4aa9123c
commit 0d9e6b9922
28 changed files with 2827 additions and 247 deletions

View File

@@ -0,0 +1,36 @@
const { PrismaClient } = require('@prisma/client');
const fs = require('fs');
const path = require('path');
const prisma = new PrismaClient();
async function main() {
const sqlPath = path.join(__dirname, 'prisma/migrations/20260128_add_system_knowledge_base/migration.sql');
const sql = fs.readFileSync(sqlPath, 'utf-8');
// 按语句分割执行
const statements = sql.split(';').filter(s => s.trim());
console.log(`Executing ${statements.length} SQL statements...`);
for (let i = 0; i < statements.length; i++) {
const stmt = statements[i].trim();
if (!stmt) continue;
try {
await prisma.$executeRawUnsafe(stmt);
console.log(`[${i + 1}/${statements.length}] OK`);
} catch (error) {
if (error.message.includes('already exists')) {
console.log(`[${i + 1}/${statements.length}] SKIPPED (already exists)`);
} else {
console.error(`[${i + 1}/${statements.length}] ERROR:`, error.message);
}
}
}
console.log('\nDone!');
await prisma.$disconnect();
}
main();

View File

@@ -0,0 +1,54 @@
-- Add knowledge_config field to prompt_templates
ALTER TABLE "capability_schema"."prompt_templates" ADD COLUMN "knowledge_config" JSONB;
-- CreateTable: system_knowledge_bases
CREATE TABLE "capability_schema"."system_knowledge_bases" (
"id" TEXT NOT NULL,
"code" VARCHAR(50) NOT NULL,
"name" VARCHAR(100) NOT NULL,
"description" TEXT,
"category" VARCHAR(50),
"document_count" INTEGER NOT NULL DEFAULT 0,
"total_tokens" INTEGER NOT NULL DEFAULT 0,
"status" VARCHAR(20) NOT NULL DEFAULT 'active',
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL,
CONSTRAINT "system_knowledge_bases_pkey" PRIMARY KEY ("id")
);
-- CreateTable: system_kb_documents
CREATE TABLE "capability_schema"."system_kb_documents" (
"id" TEXT NOT NULL,
"kb_id" TEXT NOT NULL,
"filename" VARCHAR(255) NOT NULL,
"file_path" VARCHAR(500),
"file_size" INTEGER,
"file_type" VARCHAR(50),
"content" TEXT,
"token_count" INTEGER NOT NULL DEFAULT 0,
"status" VARCHAR(20) NOT NULL DEFAULT 'pending',
"error_message" TEXT,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL,
CONSTRAINT "system_kb_documents_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "system_knowledge_bases_code_key" ON "capability_schema"."system_knowledge_bases"("code");
-- CreateIndex
CREATE INDEX "idx_system_kb_category" ON "capability_schema"."system_knowledge_bases"("category");
-- CreateIndex
CREATE INDEX "idx_system_kb_status" ON "capability_schema"."system_knowledge_bases"("status");
-- CreateIndex
CREATE INDEX "idx_system_kb_docs_kb_id" ON "capability_schema"."system_kb_documents"("kb_id");
-- CreateIndex
CREATE INDEX "idx_system_kb_docs_status" ON "capability_schema"."system_kb_documents"("status");
-- AddForeignKey
ALTER TABLE "capability_schema"."system_kb_documents" ADD CONSTRAINT "system_kb_documents_kb_id_fkey" FOREIGN KEY ("kb_id") REFERENCES "capability_schema"."system_knowledge_bases"("id") ON DELETE CASCADE ON UPDATE CASCADE;

View File

@@ -1265,12 +1265,13 @@ enum VerificationType {
/// Prompt模板 - 存储Prompt的元信息
model prompt_templates {
id Int @id @default(autoincrement())
code String @unique /// 唯一标识符,如 'RVW_EDITORIAL'
name String /// 人类可读名称,如 "稿约规范性评估"
module String /// 所属模块: RVW, ASL, DC, IIT, PKB, AIA
description String? /// 描述
variables Json? /// 预期变量列表,如 ["title", "abstract"]
id Int @id @default(autoincrement())
code String @unique /// 唯一标识符,如 'RVW_EDITORIAL'
name String /// 人类可读名称,如 "稿约规范性评估"
module String /// 所属模块: RVW, ASL, DC, IIT, PKB, AIA
description String? /// 描述
variables Json? /// 预期变量列表,如 ["title", "abstract"]
knowledge_config Json? @map("knowledge_config") /// 知识库增强配置
versions prompt_versions[]
@@ -1312,6 +1313,52 @@ enum PromptStatus {
@@schema("capability_schema")
}
/// 系统知识库 - 运营管理的公共知识库,供 Prompt 引用
model system_knowledge_bases {
id String @id @default(uuid())
code String @unique @db.VarChar(50) /// 唯一编码,如 'CONSORT_2010'
name String @db.VarChar(100) /// 名称,如 'CONSORT 2010 声明'
description String? @db.Text /// 描述
category String? @db.VarChar(50) /// 分类: methodology, statistics, crf
document_count Int @default(0) @map("document_count") /// 文档数量
total_tokens Int @default(0) @map("total_tokens") /// 总 Token 数
status String @default("active") @db.VarChar(20) /// 状态: active, archived
documents system_kb_documents[]
created_at DateTime @default(now()) @map("created_at")
updated_at DateTime @updatedAt @map("updated_at")
@@index([category], map: "idx_system_kb_category")
@@index([status], map: "idx_system_kb_status")
@@map("system_knowledge_bases")
@@schema("capability_schema")
}
/// 系统知识库文档 - 知识库中的文档
model system_kb_documents {
id String @id @default(uuid())
kb_id String @map("kb_id") /// 所属知识库ID
filename String @db.VarChar(255) /// 原始文件名
file_path String? @db.VarChar(500) @map("file_path") /// OSS 存储路径
file_size Int? @map("file_size") /// 文件大小(字节)
file_type String? @db.VarChar(50) @map("file_type") /// 文件类型: pdf, docx, md, txt
content String? @db.Text /// 解析后的文本内容
token_count Int @default(0) @map("token_count") /// Token 数量
status String @default("pending") @db.VarChar(20) /// 状态: pending, processing, ready, failed
error_message String? @db.Text @map("error_message") /// 错误信息
knowledge_base system_knowledge_bases @relation(fields: [kb_id], references: [id], onDelete: Cascade)
created_at DateTime @default(now()) @map("created_at")
updated_at DateTime @updatedAt @map("updated_at")
@@index([kb_id], map: "idx_system_kb_docs_kb_id")
@@index([status], map: "idx_system_kb_docs_status")
@@map("system_kb_documents")
@@schema("capability_schema")
}
// ============================================================
// EKB Schema - 知识库引擎 (Enterprise Knowledge Base)
// 参考文档: docs/02-通用能力层/03-RAG引擎/04-数据模型设计.md

View File

@@ -91,10 +91,18 @@ export class OSSAdapter implements StorageAdapter {
try {
const normalizedKey = this.normalizeKey(key)
// 使用 put 方法上传 Buffer适合 < 30MB 文件)
const result = await this.client.put(normalizedKey, buffer)
// 根据文件扩展名设置 Content-Type
const contentType = this.getMimeType(normalizedKey)
console.log(`[OSSAdapter] Upload success: ${normalizedKey}, size=${buffer.length}`)
// 使用 put 方法上传 Buffer适合 < 30MB 文件)
// 必须设置正确的 headers否则二进制文件可能损坏
const result = await this.client.put(normalizedKey, buffer, {
headers: {
'Content-Type': contentType,
},
})
console.log(`[OSSAdapter] Upload success: ${normalizedKey}, size=${buffer.length}, contentType=${contentType}`)
// 返回签名URL假设是私有Bucket
return this.getSignedUrl(normalizedKey)
@@ -104,6 +112,37 @@ export class OSSAdapter implements StorageAdapter {
}
}
/**
* 根据文件扩展名获取 MIME 类型
*/
private getMimeType(key: string): string {
const ext = key.toLowerCase().split('.').pop()
const mimeTypes: Record<string, string> = {
// 文档类型
pdf: 'application/pdf',
doc: 'application/msword',
docx: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
xls: 'application/vnd.ms-excel',
xlsx: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
ppt: 'application/vnd.ms-powerpoint',
pptx: 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
txt: 'text/plain',
md: 'text/markdown',
// 图片类型
jpg: 'image/jpeg',
jpeg: 'image/jpeg',
png: 'image/png',
gif: 'image/gif',
webp: 'image/webp',
svg: 'image/svg+xml',
// 其他
json: 'application/json',
xml: 'application/xml',
zip: 'application/zip',
}
return mimeTypes[ext || ''] || 'application/octet-stream'
}
/**
* 流式上传(适合大文件 > 30MB
*

View File

@@ -108,12 +108,14 @@ logger.info('✅ Prompt管理路由已注册: /api/admin/prompts');
import { tenantRoutes, moduleRoutes } from './modules/admin/routes/tenantRoutes.js';
import { userRoutes } from './modules/admin/routes/userRoutes.js';
import { statsRoutes, userOverviewRoute } from './modules/admin/routes/statsRoutes.js';
import { systemKbRoutes } from './modules/admin/system-kb/index.js';
await fastify.register(tenantRoutes, { prefix: '/api/admin/tenants' });
await fastify.register(moduleRoutes, { prefix: '/api/admin/modules' });
await fastify.register(userRoutes, { prefix: '/api/admin/users' });
await fastify.register(statsRoutes, { prefix: '/api/admin/stats' });
await fastify.register(userOverviewRoute, { prefix: '/api/admin/users' });
logger.info('✅ 运营管理路由已注册: /api/admin/tenants, /api/admin/modules, /api/admin/users, /api/admin/stats');
await fastify.register(systemKbRoutes, { prefix: '/api/v1/admin/system-kb' });
logger.info('✅ 运营管理路由已注册: /api/admin/tenants, /api/admin/modules, /api/admin/users, /api/admin/stats, /api/v1/admin/system-kb');
// ============================================
// 【临时】平台基础设施测试API

View File

@@ -0,0 +1,108 @@
### ===========================================
### System Knowledge Base API Test
### ===========================================
### 使用 VS Code REST Client 扩展运行
@baseUrl = http://localhost:3001
@contentType = application/json
### -----------------------------------------
### 1. Login (获取 Token)
### -----------------------------------------
# @name login
POST {{baseUrl}}/api/v1/auth/login/password
Content-Type: {{contentType}}
{
"phone": "13800000001",
"password": "123456"
}
### 保存 Token
@token = {{login.response.body.data.tokens.accessToken}}
### -----------------------------------------
### 2. 获取知识库列表
### -----------------------------------------
# @name listKbs
GET {{baseUrl}}/api/v1/admin/system-kb
Authorization: Bearer {{token}}
### -----------------------------------------
### 3. 创建知识库
### -----------------------------------------
# @name createKb
POST {{baseUrl}}/api/v1/admin/system-kb
Authorization: Bearer {{token}}
Content-Type: {{contentType}}
{
"code": "CLINICAL_GUIDELINES",
"name": "临床指南知识库",
"description": "存储临床研究相关的指南文档",
"category": "guidelines"
}
### 保存创建的知识库 ID
@kbId = {{createKb.response.body.data.id}}
### -----------------------------------------
### 4. 获取知识库详情
### -----------------------------------------
GET {{baseUrl}}/api/v1/admin/system-kb/{{kbId}}
Authorization: Bearer {{token}}
### -----------------------------------------
### 5. 更新知识库
### -----------------------------------------
PATCH {{baseUrl}}/api/v1/admin/system-kb/{{kbId}}
Authorization: Bearer {{token}}
Content-Type: {{contentType}}
{
"name": "临床指南知识库(更新)",
"description": "存储临床研究相关的指南文档 - 已更新"
}
### -----------------------------------------
### 6. 获取文档列表
### -----------------------------------------
GET {{baseUrl}}/api/v1/admin/system-kb/{{kbId}}/documents
Authorization: Bearer {{token}}
### -----------------------------------------
### 7. 上传文档 (需要文件)
### -----------------------------------------
### 注意:此请求需要使用 multipart/form-data
### 可以使用 curl 或 Postman 测试:
###
### curl -X POST "http://localhost:3001/api/v1/admin/system-kb/{{kbId}}/documents" \
### -H "Authorization: Bearer {{token}}" \
### -F "file=@/path/to/your/document.pdf"
### -----------------------------------------
### 8. 删除知识库
### -----------------------------------------
DELETE {{baseUrl}}/api/v1/admin/system-kb/{{kbId}}
Authorization: Bearer {{token}}
### -----------------------------------------
### 按分类筛选知识库
### -----------------------------------------
GET {{baseUrl}}/api/v1/admin/system-kb?category=guidelines
Authorization: Bearer {{token}}
### -----------------------------------------
### 按状态筛选知识库
### -----------------------------------------
GET {{baseUrl}}/api/v1/admin/system-kb?status=active
Authorization: Bearer {{token}}

View File

@@ -0,0 +1,178 @@
# ===========================================
# System Knowledge Base API Test
# ===========================================
$baseUrl = "http://localhost:3001"
$phone = "13800000001" # SUPER_ADMIN
$password = "123456"
Write-Host "=============================================" -ForegroundColor Cyan
Write-Host "System Knowledge Base API Test" -ForegroundColor Cyan
Write-Host "=============================================" -ForegroundColor Cyan
# 1. Login
Write-Host "`n[1/7] Login..." -ForegroundColor Yellow
$loginBody = @{
phone = $phone
password = $password
} | ConvertTo-Json
try {
$loginResponse = Invoke-RestMethod -Uri "$baseUrl/api/v1/auth/login/password" `
-Method POST `
-Body $loginBody `
-ContentType "application/json"
$token = $loginResponse.data.tokens.accessToken
$userName = $loginResponse.data.user.name
Write-Host " [OK] Login success! User: $userName" -ForegroundColor Green
} catch {
Write-Host " [FAIL] Login failed: $($_.Exception.Message)" -ForegroundColor Red
exit 1
}
$headers = @{
"Authorization" = "Bearer $token"
}
# 2. List Knowledge Bases (should be empty or have existing ones)
Write-Host "`n[2/7] List Knowledge Bases..." -ForegroundColor Yellow
try {
$listResponse = Invoke-RestMethod -Uri "$baseUrl/api/v1/admin/system-kb" `
-Method GET `
-Headers $headers
$kbCount = $listResponse.data.Count
Write-Host " [OK] Success! Total: $kbCount knowledge bases" -ForegroundColor Green
if ($kbCount -gt 0) {
Write-Host " ----------------------------" -ForegroundColor Gray
$listResponse.data | ForEach-Object {
Write-Host " - $($_.code): $($_.name) ($($_.documentCount) docs)" -ForegroundColor Gray
}
}
} catch {
Write-Host " [FAIL]: $($_.Exception.Message)" -ForegroundColor Red
}
# 3. Create Knowledge Base
Write-Host "`n[3/7] Create Knowledge Base..." -ForegroundColor Yellow
$testCode = "TEST_KB_$(Get-Date -Format 'yyyyMMddHHmmss')"
$createBody = @{
code = $testCode
name = "Test Knowledge Base"
description = "This is a test knowledge base"
category = "test"
} | ConvertTo-Json
try {
$createResponse = Invoke-RestMethod -Uri "$baseUrl/api/v1/admin/system-kb" `
-Method POST `
-Body $createBody `
-ContentType "application/json" `
-Headers $headers
$kbId = $createResponse.data.id
Write-Host " [OK] Created! ID: $kbId" -ForegroundColor Green
Write-Host " Code: $($createResponse.data.code)" -ForegroundColor Gray
Write-Host " Name: $($createResponse.data.name)" -ForegroundColor Gray
} catch {
Write-Host " [FAIL]: $($_.Exception.Message)" -ForegroundColor Red
exit 1
}
# 4. Get Knowledge Base Detail
Write-Host "`n[4/7] Get Knowledge Base Detail..." -ForegroundColor Yellow
try {
$detailResponse = Invoke-RestMethod -Uri "$baseUrl/api/v1/admin/system-kb/$kbId" `
-Method GET `
-Headers $headers
Write-Host " [OK] Success!" -ForegroundColor Green
Write-Host " ----------------------------" -ForegroundColor Gray
Write-Host " ID: $($detailResponse.data.id)" -ForegroundColor Gray
Write-Host " Code: $($detailResponse.data.code)" -ForegroundColor Gray
Write-Host " Name: $($detailResponse.data.name)" -ForegroundColor Gray
Write-Host " Description: $($detailResponse.data.description)" -ForegroundColor Gray
Write-Host " Status: $($detailResponse.data.status)" -ForegroundColor Gray
} catch {
Write-Host " [FAIL]: $($_.Exception.Message)" -ForegroundColor Red
}
# 5. Update Knowledge Base
Write-Host "`n[5/7] Update Knowledge Base..." -ForegroundColor Yellow
$updateBody = @{
name = "Updated Test Knowledge Base"
description = "Updated description"
} | ConvertTo-Json
try {
$updateResponse = Invoke-RestMethod -Uri "$baseUrl/api/v1/admin/system-kb/$kbId" `
-Method PATCH `
-Body $updateBody `
-ContentType "application/json" `
-Headers $headers
Write-Host " [OK] Updated!" -ForegroundColor Green
Write-Host " New Name: $($updateResponse.data.name)" -ForegroundColor Gray
Write-Host " New Description: $($updateResponse.data.description)" -ForegroundColor Gray
} catch {
Write-Host " [FAIL]: $($_.Exception.Message)" -ForegroundColor Red
}
# 6. List Documents (should be empty)
Write-Host "`n[6/7] List Documents..." -ForegroundColor Yellow
try {
$docsResponse = Invoke-RestMethod -Uri "$baseUrl/api/v1/admin/system-kb/$kbId/documents" `
-Method GET `
-Headers $headers
$docCount = $docsResponse.data.Count
Write-Host " [OK] Success! Total: $docCount documents" -ForegroundColor Green
} catch {
Write-Host " [FAIL]: $($_.Exception.Message)" -ForegroundColor Red
}
# 7. Delete Knowledge Base
Write-Host "`n[7/7] Delete Knowledge Base..." -ForegroundColor Yellow
try {
$deleteResponse = Invoke-RestMethod -Uri "$baseUrl/api/v1/admin/system-kb/$kbId" `
-Method DELETE `
-Headers $headers
Write-Host " [OK] Deleted!" -ForegroundColor Green
} catch {
Write-Host " [FAIL]: $($_.Exception.Message)" -ForegroundColor Red
}
# Verify deletion
Write-Host "`n[Verify] Check deletion..." -ForegroundColor Yellow
try {
$verifyResponse = Invoke-RestMethod -Uri "$baseUrl/api/v1/admin/system-kb/$kbId" `
-Method GET `
-Headers $headers
Write-Host " [WARN] KB still exists!" -ForegroundColor Yellow
} catch {
if ($_.Exception.Response.StatusCode -eq 404) {
Write-Host " [OK] KB successfully deleted (404)" -ForegroundColor Green
} else {
Write-Host " [OK] KB deleted (error: $($_.Exception.Message))" -ForegroundColor Green
}
}
Write-Host "`n=============================================" -ForegroundColor Cyan
Write-Host "Test Complete!" -ForegroundColor Cyan
Write-Host "=============================================" -ForegroundColor Cyan
Write-Host "`nNote: Document upload test requires a file." -ForegroundColor Yellow
Write-Host "Use test-system-kb-upload.ps1 to test file upload." -ForegroundColor Yellow

View File

@@ -0,0 +1,168 @@
# ===========================================
# System Knowledge Base - Document Upload Test
# ===========================================
# 测试文档上传功能,包含 OSS 存储和 RAG 向量化
$baseUrl = "http://localhost:3001"
$phone = "13800000001" # SUPER_ADMIN
$password = "123456"
$testFile = "D:\MyCursor\AIclinicalresearch\docs\06-测试文档\近红外光谱NIRS队列研究举例.pdf"
Write-Host "=============================================" -ForegroundColor Cyan
Write-Host "System KB - Document Upload Test" -ForegroundColor Cyan
Write-Host "=============================================" -ForegroundColor Cyan
# 检查测试文件是否存在
if (-not (Test-Path $testFile)) {
Write-Host "[ERROR] Test file not found: $testFile" -ForegroundColor Red
exit 1
}
$fileInfo = Get-Item $testFile
Write-Host "Test file: $($fileInfo.Name)" -ForegroundColor Gray
Write-Host "File size: $([math]::Round($fileInfo.Length / 1024, 2)) KB" -ForegroundColor Gray
# 1. Login
Write-Host "`n[1/6] Login..." -ForegroundColor Yellow
$loginBody = @{
phone = $phone
password = $password
} | ConvertTo-Json
try {
$loginResponse = Invoke-RestMethod -Uri "$baseUrl/api/v1/auth/login/password" `
-Method POST `
-Body $loginBody `
-ContentType "application/json"
$token = $loginResponse.data.tokens.accessToken
Write-Host " [OK] Login success!" -ForegroundColor Green
} catch {
Write-Host " [FAIL] Login failed: $($_.Exception.Message)" -ForegroundColor Red
exit 1
}
$headers = @{
"Authorization" = "Bearer $token"
}
# 2. Create Knowledge Base
Write-Host "`n[2/6] Create Knowledge Base..." -ForegroundColor Yellow
$testCode = "TEST_UPLOAD_$(Get-Date -Format 'yyyyMMddHHmmss')"
$createBody = @{
code = $testCode
name = "文档上传测试知识库"
description = "用于测试文档上传和 RAG 向量化功能"
category = "test"
} | ConvertTo-Json -Depth 10
try {
$createResponse = Invoke-RestMethod -Uri "$baseUrl/api/v1/admin/system-kb" `
-Method POST `
-Body $createBody `
-ContentType "application/json; charset=utf-8" `
-Headers $headers
$kbId = $createResponse.data.id
Write-Host " [OK] Created! ID: $kbId" -ForegroundColor Green
Write-Host " Code: $($createResponse.data.code)" -ForegroundColor Gray
} catch {
Write-Host " [FAIL]: $($_.Exception.Message)" -ForegroundColor Red
exit 1
}
# 3. Upload Document
Write-Host "`n[3/6] Upload Document (may take a while for RAG processing)..." -ForegroundColor Yellow
try {
# 使用 curl 上传文件PowerShell 的 Invoke-RestMethod 对 multipart 支持不好)
$uploadResult = curl.exe -s -X POST "$baseUrl/api/v1/admin/system-kb/$kbId/documents" `
-H "Authorization: Bearer $token" `
-F "file=@$testFile" | ConvertFrom-Json
if ($uploadResult.success) {
Write-Host " [OK] Upload success!" -ForegroundColor Green
Write-Host " ----------------------------" -ForegroundColor Gray
Write-Host " Doc ID: $($uploadResult.data.docId)" -ForegroundColor White
Write-Host " Filename: $($uploadResult.data.filename)" -ForegroundColor White
Write-Host " Chunks: $($uploadResult.data.chunkCount)" -ForegroundColor White
Write-Host " Tokens: $($uploadResult.data.tokenCount)" -ForegroundColor White
Write-Host " Duration: $($uploadResult.data.duration) ms" -ForegroundColor White
$docId = $uploadResult.data.docId
} else {
Write-Host " [FAIL]: $($uploadResult.error)" -ForegroundColor Red
$docId = $null
}
} catch {
Write-Host " [FAIL]: $($_.Exception.Message)" -ForegroundColor Red
$docId = $null
}
# 4. List Documents
Write-Host "`n[4/6] List Documents..." -ForegroundColor Yellow
try {
$docsResponse = Invoke-RestMethod -Uri "$baseUrl/api/v1/admin/system-kb/$kbId/documents" `
-Method GET `
-Headers $headers
$docCount = $docsResponse.data.Count
Write-Host " [OK] Total: $docCount documents" -ForegroundColor Green
if ($docCount -gt 0) {
Write-Host " ----------------------------" -ForegroundColor Gray
$docsResponse.data | ForEach-Object {
Write-Host " - $($_.filename) | Status: $($_.status) | Tokens: $($_.tokenCount)" -ForegroundColor Gray
Write-Host " Path: $($_.filePath)" -ForegroundColor DarkGray
}
}
} catch {
Write-Host " [FAIL]: $($_.Exception.Message)" -ForegroundColor Red
}
# 5. Get KB Detail (check updated stats)
Write-Host "`n[5/6] Get KB Detail (verify stats updated)..." -ForegroundColor Yellow
try {
$detailResponse = Invoke-RestMethod -Uri "$baseUrl/api/v1/admin/system-kb/$kbId" `
-Method GET `
-Headers $headers
Write-Host " [OK] Success!" -ForegroundColor Green
Write-Host " ----------------------------" -ForegroundColor Gray
Write-Host " Document Count: $($detailResponse.data.documentCount)" -ForegroundColor White
Write-Host " Total Tokens: $($detailResponse.data.totalTokens)" -ForegroundColor White
} catch {
Write-Host " [FAIL]: $($_.Exception.Message)" -ForegroundColor Red
}
# 6. Cleanup - Delete KB
Write-Host "`n[6/6] Cleanup - Delete Knowledge Base..." -ForegroundColor Yellow
try {
$deleteResponse = Invoke-RestMethod -Uri "$baseUrl/api/v1/admin/system-kb/$kbId" `
-Method DELETE `
-Headers $headers
Write-Host " [OK] Deleted!" -ForegroundColor Green
} catch {
Write-Host " [FAIL]: $($_.Exception.Message)" -ForegroundColor Red
}
Write-Host "`n=============================================" -ForegroundColor Cyan
Write-Host "Test Complete!" -ForegroundColor Cyan
Write-Host "=============================================" -ForegroundColor Cyan
if ($docId) {
Write-Host "`n[Summary]" -ForegroundColor Green
Write-Host " - Document uploaded successfully" -ForegroundColor White
Write-Host " - RAG vectorization completed" -ForegroundColor White
Write-Host " - OSS path: system/knowledge-bases/$kbId/$docId.pdf" -ForegroundColor White
} else {
Write-Host "`n[Summary]" -ForegroundColor Yellow
Write-Host " - Document upload may have failed" -ForegroundColor White
Write-Host " - Check backend logs for details" -ForegroundColor White
}

View File

@@ -0,0 +1,7 @@
/**
* 系统知识库模块导出
*/
export { systemKbRoutes } from './systemKbRoutes.js';
export { SystemKbService, getSystemKbService } from './systemKbService.js';
export * from './systemKbController.js';

View File

@@ -0,0 +1,337 @@
/**
* 系统知识库控制器
*/
import { FastifyRequest, FastifyReply } from 'fastify';
import { getSystemKbService } from './systemKbService.js';
import { prisma } from '../../../config/database.js';
import { logger } from '../../../common/logging/index.js';
// ==================== 类型定义 ====================
interface CreateKbBody {
code: string;
name: string;
description?: string;
category?: string;
}
interface UpdateKbBody {
name?: string;
description?: string;
category?: string;
}
interface ListKbQuery {
category?: string;
status?: string;
}
interface KbIdParams {
id: string;
}
interface DocIdParams {
id: string;
docId: string;
}
// ==================== 控制器函数 ====================
/**
* 获取知识库列表
*/
export async function listKnowledgeBases(
request: FastifyRequest<{ Querystring: ListKbQuery }>,
reply: FastifyReply
) {
try {
const { category, status } = request.query;
const service = getSystemKbService(prisma);
const kbs = await service.listKnowledgeBases({ category, status });
return reply.send({
success: true,
data: kbs,
});
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
logger.error('获取知识库列表失败', { error: message });
return reply.status(500).send({
success: false,
error: message,
});
}
}
/**
* 获取知识库详情
*/
export async function getKnowledgeBase(
request: FastifyRequest<{ Params: KbIdParams }>,
reply: FastifyReply
) {
try {
const { id } = request.params;
const service = getSystemKbService(prisma);
const kb = await service.getKnowledgeBase(id);
if (!kb) {
return reply.status(404).send({
success: false,
error: '知识库不存在',
});
}
return reply.send({
success: true,
data: kb,
});
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
logger.error('获取知识库详情失败', { error: message });
return reply.status(500).send({
success: false,
error: message,
});
}
}
/**
* 创建知识库
*/
export async function createKnowledgeBase(
request: FastifyRequest<{ Body: CreateKbBody }>,
reply: FastifyReply
) {
try {
const { code, name, description, category } = request.body;
if (!code || !name) {
return reply.status(400).send({
success: false,
error: '编码和名称为必填项',
});
}
const service = getSystemKbService(prisma);
const kb = await service.createKnowledgeBase({ code, name, description, category });
return reply.status(201).send({
success: true,
data: kb,
});
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
logger.error('创建知识库失败', { error: message });
if (message.includes('已存在')) {
return reply.status(409).send({
success: false,
error: message,
});
}
return reply.status(500).send({
success: false,
error: message,
});
}
}
/**
* 更新知识库
*/
export async function updateKnowledgeBase(
request: FastifyRequest<{ Params: KbIdParams; Body: UpdateKbBody }>,
reply: FastifyReply
) {
try {
const { id } = request.params;
const { name, description, category } = request.body;
const service = getSystemKbService(prisma);
const kb = await service.updateKnowledgeBase(id, { name, description, category });
return reply.send({
success: true,
data: kb,
});
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
logger.error('更新知识库失败', { error: message });
return reply.status(500).send({
success: false,
error: message,
});
}
}
/**
* 删除知识库
*/
export async function deleteKnowledgeBase(
request: FastifyRequest<{ Params: KbIdParams }>,
reply: FastifyReply
) {
try {
const { id } = request.params;
const service = getSystemKbService(prisma);
await service.deleteKnowledgeBase(id);
return reply.send({
success: true,
message: '删除成功',
});
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
logger.error('删除知识库失败', { error: message });
return reply.status(500).send({
success: false,
error: message,
});
}
}
/**
* 获取文档列表
*/
export async function listDocuments(
request: FastifyRequest<{ Params: KbIdParams }>,
reply: FastifyReply
) {
try {
const { id } = request.params;
const service = getSystemKbService(prisma);
const docs = await service.listDocuments(id);
return reply.send({
success: true,
data: docs,
});
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
logger.error('获取文档列表失败', { error: message });
return reply.status(500).send({
success: false,
error: message,
});
}
}
/**
* 上传文档
*/
export async function uploadDocument(
request: FastifyRequest<{ Params: KbIdParams }>,
reply: FastifyReply
) {
try {
const { id: kbId } = request.params;
// 处理 multipart 文件上传
const data = await request.file();
if (!data) {
return reply.status(400).send({
success: false,
error: '请上传文件',
});
}
const filename = data.filename;
const fileBuffer = await data.toBuffer();
// 验证文件类型
const allowedTypes = ['pdf', 'docx', 'doc', 'txt', 'md'];
const ext = filename.toLowerCase().split('.').pop();
if (!ext || !allowedTypes.includes(ext)) {
return reply.status(400).send({
success: false,
error: `不支持的文件类型: ${ext},支持: ${allowedTypes.join(', ')}`,
});
}
// 验证文件大小(最大 50MB
const maxSize = 50 * 1024 * 1024;
if (fileBuffer.length > maxSize) {
return reply.status(400).send({
success: false,
error: '文件大小超过限制(最大 50MB',
});
}
const service = getSystemKbService(prisma);
const result = await service.uploadDocument(kbId, filename, fileBuffer);
return reply.status(201).send({
success: true,
data: {
docId: result.docId,
filename,
chunkCount: result.ingestResult.chunkCount,
tokenCount: result.ingestResult.tokenCount,
duration: result.ingestResult.duration,
},
});
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
logger.error('上传文档失败', { error: message });
return reply.status(500).send({
success: false,
error: message,
});
}
}
/**
* 删除文档
*/
export async function deleteDocument(
request: FastifyRequest<{ Params: DocIdParams }>,
reply: FastifyReply
) {
try {
const { id: kbId, docId } = request.params;
const service = getSystemKbService(prisma);
await service.deleteDocument(kbId, docId);
return reply.send({
success: true,
message: '删除成功',
});
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
logger.error('删除文档失败', { error: message });
return reply.status(500).send({
success: false,
error: message,
});
}
}
/**
* 下载文档
*/
export async function downloadDocument(
request: FastifyRequest<{ Params: DocIdParams }>,
reply: FastifyReply
) {
try {
const { id: kbId, docId } = request.params;
const service = getSystemKbService(prisma);
const result = await service.getDocumentDownloadUrl(kbId, docId);
return reply.send({
success: true,
data: result,
});
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
logger.error('获取下载链接失败', { error: message });
return reply.status(500).send({
success: false,
error: message,
});
}
}

View File

@@ -0,0 +1,75 @@
/**
* 系统知识库路由
*
* 路由前缀:/api/v1/admin/system-kb
*/
import { FastifyInstance } from 'fastify';
import {
listKnowledgeBases,
getKnowledgeBase,
createKnowledgeBase,
updateKnowledgeBase,
deleteKnowledgeBase,
listDocuments,
uploadDocument,
deleteDocument,
downloadDocument,
} from './systemKbController.js';
import { authenticate, requireRoles } from '../../../common/auth/auth.middleware.js';
export async function systemKbRoutes(fastify: FastifyInstance) {
// 所有路由都需要认证 + SUPER_ADMIN 或 ADMIN 角色
const preHandler = [authenticate, requireRoles('SUPER_ADMIN', 'ADMIN')];
// ==================== 知识库 CRUD ====================
// 获取知识库列表
fastify.get('/', {
preHandler,
}, listKnowledgeBases as any);
// 创建知识库
fastify.post('/', {
preHandler,
}, createKnowledgeBase as any);
// 获取知识库详情
fastify.get('/:id', {
preHandler,
}, getKnowledgeBase as any);
// 更新知识库
fastify.patch('/:id', {
preHandler,
}, updateKnowledgeBase as any);
// 删除知识库
fastify.delete('/:id', {
preHandler,
}, deleteKnowledgeBase as any);
// ==================== 文档管理 ====================
// 获取文档列表
fastify.get('/:id/documents', {
preHandler,
}, listDocuments as any);
// 上传文档
fastify.post('/:id/documents', {
preHandler,
}, uploadDocument as any);
// 删除文档
fastify.delete('/:id/documents/:docId', {
preHandler,
}, deleteDocument as any);
// 下载文档(获取签名 URL
fastify.get('/:id/documents/:docId/download', {
preHandler,
}, downloadDocument as any);
}
export default systemKbRoutes;

View File

@@ -0,0 +1,505 @@
/**
* 系统知识库服务
*
* 运营管理端的公共知识库,供 Prompt 引用
* 复用 RAG 引擎的核心能力
*/
import { PrismaClient } from '@prisma/client';
import path from 'path';
import { logger } from '../../../common/logging/index.js';
import { getDocumentIngestService, type IngestResult } from '../../../common/rag/index.js';
import { storage, OSSAdapter } from '../../../common/storage/index.js';
// ==================== 类型定义 ====================
export interface CreateKbInput {
code: string;
name: string;
description?: string;
category?: string;
}
export interface UpdateKbInput {
name?: string;
description?: string;
category?: string;
}
export interface SystemKb {
id: string;
code: string;
name: string;
description: string | null;
category: string | null;
documentCount: number;
totalTokens: number;
status: string;
createdAt: Date;
updatedAt: Date;
}
export interface SystemKbDocument {
id: string;
kbId: string;
filename: string;
filePath: string | null;
fileSize: number | null;
fileType: string | null;
tokenCount: number;
status: string;
errorMessage: string | null;
createdAt: Date;
}
// ==================== SystemKbService ====================
export class SystemKbService {
private prisma: PrismaClient;
constructor(prisma: PrismaClient) {
this.prisma = prisma;
}
/**
* 获取知识库列表
*/
async listKnowledgeBases(options?: {
category?: string;
status?: string;
}): Promise<SystemKb[]> {
const where: any = {};
if (options?.category) where.category = options.category;
if (options?.status) where.status = options.status;
const kbs = await this.prisma.system_knowledge_bases.findMany({
where,
orderBy: { created_at: 'desc' },
});
return kbs.map(kb => ({
id: kb.id,
code: kb.code,
name: kb.name,
description: kb.description,
category: kb.category,
documentCount: kb.document_count,
totalTokens: kb.total_tokens,
status: kb.status,
createdAt: kb.created_at,
updatedAt: kb.updated_at,
}));
}
/**
* 获取知识库详情
*/
async getKnowledgeBase(id: string): Promise<SystemKb | null> {
const kb = await this.prisma.system_knowledge_bases.findUnique({
where: { id },
});
if (!kb) return null;
return {
id: kb.id,
code: kb.code,
name: kb.name,
description: kb.description,
category: kb.category,
documentCount: kb.document_count,
totalTokens: kb.total_tokens,
status: kb.status,
createdAt: kb.created_at,
updatedAt: kb.updated_at,
};
}
/**
* 创建知识库
*/
async createKnowledgeBase(input: CreateKbInput): Promise<SystemKb> {
const { code, name, description, category } = input;
// 检查 code 是否已存在
const existing = await this.prisma.system_knowledge_bases.findUnique({
where: { code },
});
if (existing) {
throw new Error(`知识库编码 ${code} 已存在`);
}
// 使用事务同时创建两个表的记录
const result = await this.prisma.$transaction(async (tx) => {
// 1. 创建系统知识库记录
const systemKb = await tx.system_knowledge_bases.create({
data: {
code,
name,
description,
category,
},
});
// 2. 在 ekb_schema 创建对应的知识库记录(用于向量存储)
await tx.ekbKnowledgeBase.create({
data: {
id: systemKb.id, // 使用相同的 ID
name,
description,
type: 'SYSTEM',
ownerId: code, // 使用 code 作为 ownerId
config: {
source: 'system_knowledge_base',
systemKbCode: code,
},
},
});
return systemKb;
});
logger.info(`创建系统知识库: ${code}`, { id: result.id });
return {
id: result.id,
code: result.code,
name: result.name,
description: result.description,
category: result.category,
documentCount: result.document_count,
totalTokens: result.total_tokens,
status: result.status,
createdAt: result.created_at,
updatedAt: result.updated_at,
};
}
/**
* 更新知识库
*/
async updateKnowledgeBase(id: string, input: UpdateKbInput): Promise<SystemKb> {
const kb = await this.prisma.system_knowledge_bases.update({
where: { id },
data: {
name: input.name,
description: input.description,
category: input.category,
},
});
// 同步更新 ekb_knowledge_base
await this.prisma.ekbKnowledgeBase.update({
where: { id },
data: {
name: input.name,
description: input.description,
},
}).catch(() => {
// 忽略更新失败(可能不存在)
});
return {
id: kb.id,
code: kb.code,
name: kb.name,
description: kb.description,
category: kb.category,
documentCount: kb.document_count,
totalTokens: kb.total_tokens,
status: kb.status,
createdAt: kb.created_at,
updatedAt: kb.updated_at,
};
}
/**
* 删除知识库
*/
async deleteKnowledgeBase(id: string): Promise<void> {
// 使用事务删除
await this.prisma.$transaction(async (tx) => {
// 1. 删除 ekb_schema 中的数据(级联删除 documents 和 chunks
await tx.ekbKnowledgeBase.delete({
where: { id },
}).catch(() => {
// 忽略删除失败
});
// 2. 删除系统知识库文档记录
await tx.system_kb_documents.deleteMany({
where: { kb_id: id },
});
// 3. 删除系统知识库记录
await tx.system_knowledge_bases.delete({
where: { id },
});
});
logger.info(`删除系统知识库: ${id}`);
}
/**
* 获取文档列表
*/
async listDocuments(kbId: string): Promise<SystemKbDocument[]> {
const docs = await this.prisma.system_kb_documents.findMany({
where: { kb_id: kbId },
orderBy: { created_at: 'desc' },
});
return docs.map(doc => ({
id: doc.id,
kbId: doc.kb_id,
filename: doc.filename,
filePath: doc.file_path,
fileSize: doc.file_size,
fileType: doc.file_type,
tokenCount: doc.token_count,
status: doc.status,
errorMessage: doc.error_message,
createdAt: doc.created_at,
}));
}
/**
* 上传文档
*
* 流程:
* 1. 创建文档记录获取 docId
* 2. 上传文件到 OSS: system/knowledge-bases/{kbId}/{docId}.{ext}
* 3. 调用 RAG 引擎入库(向量化)
* 4. 更新文档状态和统计
*/
async uploadDocument(
kbId: string,
filename: string,
fileBuffer: Buffer
): Promise<{ docId: string; ingestResult: IngestResult }> {
// 1. 检查知识库是否存在
const kb = await this.prisma.system_knowledge_bases.findUnique({
where: { id: kbId },
});
if (!kb) {
throw new Error(`知识库 ${kbId} 不存在`);
}
// 2. 创建文档记录状态pending获取 docId
const doc = await this.prisma.system_kb_documents.create({
data: {
kb_id: kbId,
filename,
file_size: fileBuffer.length,
file_type: this.getFileType(filename),
status: 'processing',
},
});
try {
// 3. 生成 OSS 存储路径并上传
const ossKey = this.generateOssKey(kbId, doc.id, filename);
const ossUrl = await storage.upload(ossKey, fileBuffer);
// 4. 更新 file_path
await this.prisma.system_kb_documents.update({
where: { id: doc.id },
data: { file_path: ossKey },
});
logger.info(`文件已上传到 OSS: ${ossKey}`, { kbId, docId: doc.id });
// 5. 调用 RAG 引擎入库(向量化)
const ingestService = getDocumentIngestService(this.prisma);
const ingestResult = await ingestService.ingestDocument(
{ filename, fileBuffer },
{ kbId, contentType: 'REFERENCE' }
);
if (ingestResult.success) {
// 6. 更新文档状态为 ready
await this.prisma.system_kb_documents.update({
where: { id: doc.id },
data: {
status: 'ready',
token_count: ingestResult.tokenCount || 0,
content: null, // 内容存在 ekb_chunk 中
},
});
// 7. 更新知识库统计
await this.prisma.system_knowledge_bases.update({
where: { id: kbId },
data: {
document_count: { increment: 1 },
total_tokens: { increment: ingestResult.tokenCount || 0 },
},
});
logger.info(`文档上传成功: ${filename}`, {
kbId,
docId: doc.id,
ossKey,
chunks: ingestResult.chunkCount,
tokens: ingestResult.tokenCount,
});
} else {
// 入库失败,但文件已上传到 OSS
await this.prisma.system_kb_documents.update({
where: { id: doc.id },
data: {
status: 'failed',
error_message: ingestResult.error,
},
});
}
return { docId: doc.id, ingestResult };
} catch (error) {
// 异常处理
const errorMessage = error instanceof Error ? error.message : String(error);
await this.prisma.system_kb_documents.update({
where: { id: doc.id },
data: {
status: 'failed',
error_message: errorMessage,
},
});
logger.error(`文档上传失败: ${filename}`, { error: errorMessage });
throw error;
}
}
/**
* 删除文档
*
* 流程:
* 1. 删除 OSS 文件
* 2. 删除 ekb_schema 向量数据
* 3. 删除 system_kb_documents 记录
* 4. 更新知识库统计
*/
async deleteDocument(kbId: string, docId: string): Promise<void> {
const doc = await this.prisma.system_kb_documents.findFirst({
where: { id: docId, kb_id: kbId },
});
if (!doc) {
throw new Error(`文档 ${docId} 不存在`);
}
// 1. 删除 OSS 文件
if (doc.file_path) {
try {
await storage.delete(doc.file_path);
logger.info(`已删除 OSS 文件: ${doc.file_path}`);
} catch (error) {
// OSS 删除失败不阻塞流程,只记录警告
logger.warn(`删除 OSS 文件失败: ${doc.file_path}`, { error });
}
}
// 使用事务删除数据库记录
await this.prisma.$transaction(async (tx) => {
// 2. 查找 ekb_document通过 filename 匹配)
const ekbDoc = await tx.ekbDocument.findFirst({
where: { kbId, filename: doc.filename },
});
if (ekbDoc) {
// 3. 删除 ekb_chunk级联删除
await tx.ekbChunk.deleteMany({
where: { documentId: ekbDoc.id },
});
// 4. 删除 ekb_document
await tx.ekbDocument.delete({
where: { id: ekbDoc.id },
});
}
// 5. 删除系统知识库文档记录
await tx.system_kb_documents.delete({
where: { id: docId },
});
// 6. 更新知识库统计
await tx.system_knowledge_bases.update({
where: { id: kbId },
data: {
document_count: { decrement: 1 },
total_tokens: { decrement: doc.token_count },
},
});
});
logger.info(`删除文档: ${doc.filename}`, { kbId, docId });
}
/**
* 获取文档下载 URL
*/
async getDocumentDownloadUrl(kbId: string, docId: string): Promise<{
url: string;
filename: string;
fileSize: number | null;
}> {
const doc = await this.prisma.system_kb_documents.findFirst({
where: { id: docId, kb_id: kbId },
});
if (!doc) {
throw new Error(`文档 ${docId} 不存在`);
}
if (!doc.file_path) {
throw new Error('文档文件路径不存在');
}
// 获取签名 URL有效期 1 小时),传入原始文件名以设置 Content-Disposition
// 这样浏览器下载时会使用原始文件名而不是 UUID
const ossAdapter = storage as OSSAdapter;
const url = ossAdapter.getSignedUrl(doc.file_path, 3600, doc.filename);
return {
url,
filename: doc.filename,
fileSize: doc.file_size,
};
}
/**
* 生成 OSS 存储路径
*
* 格式system/knowledge-bases/{kbId}/{docId}.{ext}
*
* @param kbId - 知识库 ID
* @param docId - 文档 ID
* @param filename - 原始文件名(用于获取扩展名)
*/
private generateOssKey(kbId: string, docId: string, filename: string): string {
const ext = path.extname(filename).toLowerCase();
return `system/knowledge-bases/${kbId}/${docId}${ext}`;
}
/**
* 获取文件类型
*/
private getFileType(filename: string): string {
const ext = filename.toLowerCase().split('.').pop();
return ext || 'unknown';
}
}
// ==================== 单例工厂 ====================
let serviceInstance: SystemKbService | null = null;
export function getSystemKbService(prisma: PrismaClient): SystemKbService {
if (!serviceInstance) {
serviceInstance = new SystemKbService(prisma);
}
return serviceInstance;
}

104
backend/test-oss-upload.cjs Normal file
View File

@@ -0,0 +1,104 @@
/**
* OSS 上传下载完整性测试
*
* 测试流程:
* 1. 读取本地 PDF 文件
* 2. 上传到 OSS
* 3. 从 OSS 下载
* 4. 比较 MD5 值
*/
require('dotenv').config();
const fs = require('fs');
const crypto = require('crypto');
const OSS = require('ali-oss');
const testFile = 'D:\\MyCursor\\AIclinicalresearch\\docs\\06-测试文档\\近红外光谱NIRS队列研究举例.pdf';
const testKey = 'test/oss-integrity-test.pdf';
async function main() {
console.log('='.repeat(60));
console.log('OSS Upload/Download Integrity Test');
console.log('='.repeat(60));
// 1. 读取本地文件
console.log('\n[1] Reading local file...');
const localBuffer = fs.readFileSync(testFile);
const localMd5 = crypto.createHash('md5').update(localBuffer).digest('hex');
console.log(` File size: ${localBuffer.length} bytes`);
console.log(` MD5: ${localMd5}`);
console.log(` First 20 bytes: ${localBuffer.slice(0, 20).toString('hex')}`);
// 2. 创建 OSS 客户端
console.log('\n[2] Creating OSS client...');
const client = new OSS({
region: process.env.OSS_REGION,
bucket: process.env.OSS_BUCKET,
accessKeyId: process.env.OSS_ACCESS_KEY_ID,
accessKeySecret: process.env.OSS_ACCESS_KEY_SECRET,
endpoint: `${process.env.OSS_REGION}.aliyuncs.com`,
secure: true,
});
console.log(` Region: ${process.env.OSS_REGION}`);
console.log(` Bucket: ${process.env.OSS_BUCKET}`);
// 3. 上传到 OSS
console.log('\n[3] Uploading to OSS...');
try {
const uploadResult = await client.put(testKey, localBuffer, {
headers: {
'Content-Type': 'application/pdf',
},
});
console.log(` Upload success: ${uploadResult.name}`);
} catch (error) {
console.error(` Upload failed:`, error.message);
return;
}
// 4. 从 OSS 下载
console.log('\n[4] Downloading from OSS...');
try {
const downloadResult = await client.get(testKey);
const downloadBuffer = downloadResult.content;
const downloadMd5 = crypto.createHash('md5').update(downloadBuffer).digest('hex');
console.log(` Download size: ${downloadBuffer.length} bytes`);
console.log(` MD5: ${downloadMd5}`);
console.log(` First 20 bytes: ${downloadBuffer.slice(0, 20).toString('hex')}`);
// 5. 比较
console.log('\n[5] Comparing...');
if (localMd5 === downloadMd5) {
console.log(' ✅ MD5 MATCH - File integrity OK!');
} else {
console.log(' ❌ MD5 MISMATCH - File corrupted!');
console.log(` Local: ${localMd5}`);
console.log(` Download: ${downloadMd5}`);
}
// 保存下载的文件用于对比
const outputPath = 'D:\\MyCursor\\AIclinicalresearch\\docs\\06-测试文档\\oss-downloaded-test.pdf';
fs.writeFileSync(outputPath, downloadBuffer);
console.log(`\n Downloaded file saved to: ${outputPath}`);
} catch (error) {
console.error(` Download failed:`, error.message);
return;
}
// 6. 清理
console.log('\n[6] Cleanup...');
try {
await client.delete(testKey);
console.log(' Test file deleted from OSS');
} catch (error) {
console.log(` Cleanup failed: ${error.message}`);
}
console.log('\n' + '='.repeat(60));
console.log('Test Complete!');
console.log('='.repeat(60));
}
main().catch(console.error);

View File

@@ -0,0 +1,22 @@
const { PrismaClient } = require('@prisma/client');
const prisma = new PrismaClient();
async function main() {
const tables = await prisma.$queryRawUnsafe(`
SELECT table_name FROM information_schema.tables
WHERE table_schema = 'capability_schema' ORDER BY table_name
`);
console.log('=== capability_schema Tables ===');
tables.forEach(t => console.log('-', t.table_name));
const cols = await prisma.$queryRawUnsafe(`
SELECT column_name FROM information_schema.columns
WHERE table_schema = 'capability_schema' AND table_name = 'prompt_templates'
ORDER BY ordinal_position
`);
console.log('\n=== prompt_templates Columns ===');
cols.forEach(c => console.log('-', c.column_name));
await prisma.$disconnect();
}
main();