- Complete knowledge base list and detail pages - Complete document upload component - Fix CORS config (add PUT/DELETE method support) - Fix file upload issues (disabled state and beforeUpload return value) - Add detailed debug logs (cleaned up) - Create Day 21-22 completion summary document
572 lines
15 KiB
Markdown
572 lines
15 KiB
Markdown
# 知识库需求调整说明
|
||
|
||
## 📋 需求变更
|
||
|
||
### 原需求(PRD原文)
|
||
> "后期考虑,增加基于大规模(1000篇以内)文献的读取、识别、内容提取的工作"
|
||
|
||
**理解:** 这给人的印象是需要处理海量文献,技术难度极高。
|
||
|
||
### 实际需求(明确后)
|
||
✅ **每个用户最多创建 3个知识库**
|
||
✅ **每个知识库最多上传 50个文件**
|
||
✅ **主要格式:PDF、DOCX**
|
||
✅ **单用户最大文档量:150个文件**
|
||
|
||
---
|
||
|
||
## 🎯 影响分析
|
||
|
||
### 技术难度大幅降低
|
||
|
||
| 维度 | 原理解(1000篇+) | 实际需求(150个/用户) | 影响 |
|
||
|------|-----------------|---------------------|------|
|
||
| **向量数据库** | 需要高性能集群 | Dify内置Qdrant足够 | ✅ 简化 |
|
||
| **文档处理** | 需要分布式处理 | 单机异步处理即可 | ✅ 简化 |
|
||
| **检索性能** | 需要优化索引 | 默认配置即可 | ✅ 简化 |
|
||
| **存储成本** | 需要大容量存储 | 标准对象存储 | ✅ 降低 |
|
||
| **技术难度** | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ | ✅ 降低 |
|
||
|
||
### Dify完全够用
|
||
|
||
**Dify的能力:**
|
||
- ✅ 单个知识库支持上万个文档片段
|
||
- ✅ 自动文档解析(PDF、Word、TXT等)
|
||
- ✅ 内置向量化(支持多种Embedding模型)
|
||
- ✅ 混合检索(关键词 + 语义检索)
|
||
- ✅ 重排序(Reranking)
|
||
- ✅ 答案溯源
|
||
|
||
**我们的需求:**
|
||
- 50个文件/知识库(远低于Dify上限)
|
||
- PDF、DOCX格式(Dify原生支持)
|
||
- **结论:Dify完全满足需求!**
|
||
|
||
---
|
||
|
||
## 💾 数据库设计
|
||
|
||
### 知识库表结构
|
||
|
||
```sql
|
||
-- 知识库表
|
||
CREATE TABLE knowledge_bases (
|
||
id VARCHAR(50) PRIMARY KEY,
|
||
user_id VARCHAR(50) NOT NULL,
|
||
name VARCHAR(100) NOT NULL,
|
||
description TEXT,
|
||
|
||
-- Dify知识库ID
|
||
dify_dataset_id VARCHAR(100) NOT NULL,
|
||
|
||
-- 统计信息
|
||
file_count INT DEFAULT 0,
|
||
total_size_bytes BIGINT DEFAULT 0,
|
||
|
||
created_at TIMESTAMP DEFAULT NOW(),
|
||
updated_at TIMESTAMP DEFAULT NOW(),
|
||
|
||
FOREIGN KEY (user_id) REFERENCES users(id),
|
||
|
||
-- 限制:每个用户最多3个知识库
|
||
CONSTRAINT check_kb_limit CHECK (
|
||
(SELECT COUNT(*) FROM knowledge_bases WHERE user_id = NEW.user_id) <= 3
|
||
)
|
||
);
|
||
|
||
-- 为用户ID创建索引
|
||
CREATE INDEX idx_kb_user ON knowledge_bases(user_id);
|
||
|
||
-- 文档表
|
||
CREATE TABLE documents (
|
||
id VARCHAR(50) PRIMARY KEY,
|
||
kb_id VARCHAR(50) NOT NULL,
|
||
user_id VARCHAR(50) NOT NULL,
|
||
|
||
filename VARCHAR(255) NOT NULL,
|
||
file_type VARCHAR(20) NOT NULL, -- pdf, docx
|
||
file_size_bytes BIGINT NOT NULL,
|
||
file_url TEXT NOT NULL, -- 对象存储URL
|
||
|
||
-- Dify文档ID
|
||
dify_document_id VARCHAR(100) NOT NULL,
|
||
|
||
-- 处理状态
|
||
status VARCHAR(20) DEFAULT 'uploading', -- uploading, processing, completed, failed
|
||
progress INT DEFAULT 0, -- 0-100
|
||
error_message TEXT,
|
||
|
||
-- 处理结果
|
||
segments_count INT DEFAULT 0, -- 切分的段落数
|
||
tokens_count INT DEFAULT 0, -- token数量
|
||
|
||
uploaded_at TIMESTAMP DEFAULT NOW(),
|
||
processed_at TIMESTAMP,
|
||
|
||
FOREIGN KEY (kb_id) REFERENCES knowledge_bases(id) ON DELETE CASCADE,
|
||
FOREIGN KEY (user_id) REFERENCES users(id),
|
||
|
||
-- 限制:每个知识库最多50个文件
|
||
CONSTRAINT check_doc_limit CHECK (
|
||
(SELECT COUNT(*) FROM documents WHERE kb_id = NEW.kb_id) <= 50
|
||
)
|
||
);
|
||
|
||
-- 索引
|
||
CREATE INDEX idx_doc_kb ON documents(kb_id);
|
||
CREATE INDEX idx_doc_user ON documents(user_id);
|
||
CREATE INDEX idx_doc_status ON documents(status);
|
||
```
|
||
|
||
### 用户配额表
|
||
|
||
```sql
|
||
-- 用户配额表(可选,用于更灵活的配额管理)
|
||
CREATE TABLE user_quotas (
|
||
user_id VARCHAR(50) PRIMARY KEY,
|
||
|
||
-- 知识库配额
|
||
kb_quota INT DEFAULT 3, -- 允许创建的知识库数量
|
||
kb_used INT DEFAULT 0, -- 已使用
|
||
|
||
-- 文件配额(每个知识库)
|
||
files_per_kb_quota INT DEFAULT 50,
|
||
|
||
-- 存储配额(字节)
|
||
storage_quota_bytes BIGINT DEFAULT 1073741824, -- 1GB
|
||
storage_used_bytes BIGINT DEFAULT 0,
|
||
|
||
updated_at TIMESTAMP DEFAULT NOW(),
|
||
|
||
FOREIGN KEY (user_id) REFERENCES users(id)
|
||
);
|
||
```
|
||
|
||
---
|
||
|
||
## 🔧 实现方案
|
||
|
||
### 1. 知识库创建(带限制检查)
|
||
|
||
```typescript
|
||
// backend/src/services/knowledge-base.service.ts
|
||
|
||
export class KnowledgeBaseService {
|
||
|
||
/**
|
||
* 创建知识库
|
||
*/
|
||
async createKnowledgeBase(userId: string, data: CreateKBDto) {
|
||
// 1. 检查用户知识库数量限制
|
||
const userKbCount = await db.knowledge_bases.count({
|
||
where: { user_id: userId }
|
||
});
|
||
|
||
if (userKbCount >= 3) {
|
||
throw new Error('已达到知识库数量上限(3个)');
|
||
}
|
||
|
||
// 2. 在Dify中创建知识库
|
||
const difyDataset = await difyClient.createDataset({
|
||
name: data.name,
|
||
description: data.description,
|
||
indexing_technique: 'high_quality', // 高质量索引
|
||
permission: 'only_me'
|
||
});
|
||
|
||
// 3. 在数据库中保存记录
|
||
const kb = await db.knowledge_bases.create({
|
||
id: generateId('kb'),
|
||
user_id: userId,
|
||
name: data.name,
|
||
description: data.description,
|
||
dify_dataset_id: difyDataset.id,
|
||
file_count: 0,
|
||
total_size_bytes: 0
|
||
});
|
||
|
||
return kb;
|
||
}
|
||
|
||
/**
|
||
* 上传文档
|
||
*/
|
||
async uploadDocument(userId: string, kbId: string, file: File) {
|
||
// 1. 检查知识库是否存在且属于该用户
|
||
const kb = await db.knowledge_bases.findOne({
|
||
where: { id: kbId, user_id: userId }
|
||
});
|
||
|
||
if (!kb) {
|
||
throw new Error('知识库不存在或无权限');
|
||
}
|
||
|
||
// 2. 检查文件数量限制
|
||
const docCount = await db.documents.count({
|
||
where: { kb_id: kbId }
|
||
});
|
||
|
||
if (docCount >= 50) {
|
||
throw new Error('知识库文件数量已达上限(50个)');
|
||
}
|
||
|
||
// 3. 检查文件格式
|
||
const allowedTypes = ['application/pdf', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'];
|
||
if (!allowedTypes.includes(file.mimetype)) {
|
||
throw new Error('仅支持PDF和DOCX格式');
|
||
}
|
||
|
||
// 4. 检查文件大小(单个文件最大50MB)
|
||
const maxSize = 50 * 1024 * 1024; // 50MB
|
||
if (file.size > maxSize) {
|
||
throw new Error('文件大小超过限制(最大50MB)');
|
||
}
|
||
|
||
// 5. 上传到对象存储
|
||
const fileUrl = await objectStorage.upload(file);
|
||
|
||
// 6. 创建文档记录
|
||
const doc = await db.documents.create({
|
||
id: generateId('doc'),
|
||
kb_id: kbId,
|
||
user_id: userId,
|
||
filename: file.originalname,
|
||
file_type: path.extname(file.originalname).slice(1),
|
||
file_size_bytes: file.size,
|
||
file_url: fileUrl,
|
||
status: 'uploading',
|
||
dify_document_id: '' // 稍后更新
|
||
});
|
||
|
||
// 7. 异步提交到Dify处理
|
||
this.processDocumentAsync(doc.id, kb.dify_dataset_id, fileUrl);
|
||
|
||
return doc;
|
||
}
|
||
|
||
/**
|
||
* 异步处理文档
|
||
*/
|
||
private async processDocumentAsync(docId: string, difyDatasetId: string, fileUrl: string) {
|
||
try {
|
||
// 1. 更新状态为处理中
|
||
await db.documents.update({
|
||
where: { id: docId },
|
||
data: { status: 'processing' }
|
||
});
|
||
|
||
// 2. 提交到Dify处理
|
||
const difyDoc = await difyClient.uploadDocument({
|
||
dataset_id: difyDatasetId,
|
||
file_url: fileUrl,
|
||
indexing_technique: 'high_quality',
|
||
process_rule: {
|
||
mode: 'automatic',
|
||
rules: {
|
||
pre_processing_rules: [
|
||
{ id: 'remove_extra_spaces', enabled: true },
|
||
{ id: 'remove_urls_emails', enabled: false } // 保留医学文献中的引用
|
||
],
|
||
segmentation: {
|
||
separator: '\n\n',
|
||
max_tokens: 500 // 每段最大500 tokens
|
||
}
|
||
}
|
||
}
|
||
});
|
||
|
||
// 3. 保存Dify文档ID
|
||
await db.documents.update({
|
||
where: { id: docId },
|
||
data: { dify_document_id: difyDoc.id }
|
||
});
|
||
|
||
// 4. 轮询处理状态
|
||
let status = 'processing';
|
||
while (status === 'processing') {
|
||
await sleep(2000); // 2秒后重试
|
||
|
||
const result = await difyClient.getDocumentStatus({
|
||
dataset_id: difyDatasetId,
|
||
document_id: difyDoc.id
|
||
});
|
||
|
||
status = result.indexing_status;
|
||
|
||
// 更新进度
|
||
await db.documents.update({
|
||
where: { id: docId },
|
||
data: {
|
||
progress: Math.round((result.completed_segments / result.total_segments) * 100)
|
||
}
|
||
});
|
||
}
|
||
|
||
// 5. 处理完成
|
||
if (status === 'completed') {
|
||
const result = await difyClient.getDocumentStatus({
|
||
dataset_id: difyDatasetId,
|
||
document_id: difyDoc.id
|
||
});
|
||
|
||
await db.documents.update({
|
||
where: { id: docId },
|
||
data: {
|
||
status: 'completed',
|
||
progress: 100,
|
||
segments_count: result.total_segments,
|
||
tokens_count: result.tokens,
|
||
processed_at: new Date()
|
||
}
|
||
});
|
||
|
||
// 更新知识库统计
|
||
await this.updateKbStats(docId);
|
||
|
||
} else {
|
||
// 处理失败
|
||
await db.documents.update({
|
||
where: { id: docId },
|
||
data: {
|
||
status: 'failed',
|
||
error_message: result.error || '文档处理失败'
|
||
}
|
||
});
|
||
}
|
||
|
||
} catch (error) {
|
||
// 异常处理
|
||
await db.documents.update({
|
||
where: { id: docId },
|
||
data: {
|
||
status: 'failed',
|
||
error_message: error.message
|
||
}
|
||
});
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 检索知识库
|
||
*/
|
||
async queryKnowledgeBase(userId: string, kbId: string, query: string) {
|
||
// 1. 验证权限
|
||
const kb = await db.knowledge_bases.findOne({
|
||
where: { id: kbId, user_id: userId }
|
||
});
|
||
|
||
if (!kb) {
|
||
throw new Error('知识库不存在或无权限');
|
||
}
|
||
|
||
// 2. 调用Dify检索
|
||
const results = await difyClient.queryKnowledgeBase({
|
||
dataset_id: kb.dify_dataset_id,
|
||
query,
|
||
retrieval_model: {
|
||
search_method: 'hybrid_search', // 混合检索
|
||
reranking_enable: true, // 启用重排序
|
||
top_k: 5, // 返回前5个结果
|
||
score_threshold: 0.5 // 相似度阈值
|
||
}
|
||
});
|
||
|
||
// 3. 格式化返回结果
|
||
return results.records.map(record => ({
|
||
content: record.segment.content,
|
||
score: record.score,
|
||
document: {
|
||
id: record.segment.document.id,
|
||
name: record.segment.document.name,
|
||
position: record.segment.position // 在文档中的位置
|
||
}
|
||
}));
|
||
}
|
||
|
||
/**
|
||
* 获取用户的知识库列表
|
||
*/
|
||
async getUserKnowledgeBases(userId: string) {
|
||
const kbs = await db.knowledge_bases.findMany({
|
||
where: { user_id: userId },
|
||
include: {
|
||
_count: {
|
||
select: { documents: true }
|
||
}
|
||
}
|
||
});
|
||
|
||
return kbs.map(kb => ({
|
||
id: kb.id,
|
||
name: kb.name,
|
||
description: kb.description,
|
||
file_count: kb.file_count,
|
||
total_size_mb: (kb.total_size_bytes / 1024 / 1024).toFixed(2),
|
||
created_at: kb.created_at,
|
||
quota: {
|
||
used: kb.file_count,
|
||
limit: 50
|
||
}
|
||
}));
|
||
}
|
||
|
||
/**
|
||
* 删除文档
|
||
*/
|
||
async deleteDocument(userId: string, docId: string) {
|
||
// 1. 验证权限
|
||
const doc = await db.documents.findOne({
|
||
where: { id: docId, user_id: userId },
|
||
include: { knowledge_base: true }
|
||
});
|
||
|
||
if (!doc) {
|
||
throw new Error('文档不存在或无权限');
|
||
}
|
||
|
||
// 2. 从Dify删除
|
||
await difyClient.deleteDocument({
|
||
dataset_id: doc.knowledge_base.dify_dataset_id,
|
||
document_id: doc.dify_document_id
|
||
});
|
||
|
||
// 3. 从对象存储删除
|
||
await objectStorage.delete(doc.file_url);
|
||
|
||
// 4. 从数据库删除
|
||
await db.documents.delete({ where: { id: docId } });
|
||
|
||
// 5. 更新知识库统计
|
||
await this.updateKbStats(doc.kb_id);
|
||
}
|
||
|
||
/**
|
||
* 更新知识库统计信息
|
||
*/
|
||
private async updateKbStats(kbId: string) {
|
||
const stats = await db.documents.aggregate({
|
||
where: { kb_id: kbId, status: 'completed' },
|
||
_count: true,
|
||
_sum: { file_size_bytes: true }
|
||
});
|
||
|
||
await db.knowledge_bases.update({
|
||
where: { id: kbId },
|
||
data: {
|
||
file_count: stats._count,
|
||
total_size_bytes: stats._sum.file_size_bytes || 0
|
||
}
|
||
});
|
||
}
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 📊 成本影响
|
||
|
||
### 存储成本(降低)
|
||
|
||
**原估算(1000篇文献):**
|
||
- 假设每篇10MB
|
||
- 总存储:10GB+
|
||
- 成本:¥200+/月
|
||
|
||
**实际需求(150个文件/用户):**
|
||
```
|
||
单用户存储:
|
||
- 150个文件 × 平均5MB = 750MB
|
||
- 1000个用户 = 750GB
|
||
|
||
月度成本(阿里云OSS):
|
||
- 存储:750GB × ¥0.12/GB = ¥90/月
|
||
- 流量:假设100GB/月 × ¥0.5/GB = ¥50/月
|
||
- 总计:¥140/月
|
||
```
|
||
|
||
**成本节省:约¥60/月**
|
||
|
||
### 处理性能(提升)
|
||
|
||
**小规模知识库的优势:**
|
||
- ✅ 检索速度更快(< 1s)
|
||
- ✅ 向量化处理更快(单文件 < 30s)
|
||
- ✅ 无需复杂的性能优化
|
||
|
||
---
|
||
|
||
## 🎯 技术选型确认
|
||
|
||
### Dify完全满足需求 ✅
|
||
|
||
| 功能 | 需求 | Dify能力 | 结论 |
|
||
|------|------|---------|------|
|
||
| 知识库数量 | 3个/用户 | 无限制 | ✅ 满足 |
|
||
| 文件数量 | 50个/知识库 | 上千个 | ✅ 满足 |
|
||
| 文件格式 | PDF、DOCX | 支持20+格式 | ✅ 满足 |
|
||
| 向量化 | 自动 | 内置支持 | ✅ 满足 |
|
||
| 检索 | 混合检索 | 支持 | ✅ 满足 |
|
||
| 答案溯源 | 需要 | 支持 | ✅ 满足 |
|
||
|
||
**结论:无需考虑其他RAG方案,Dify完全够用!**
|
||
|
||
---
|
||
|
||
## 📝 API设计
|
||
|
||
### RESTful API
|
||
|
||
```typescript
|
||
// 知识库管理
|
||
GET /api/knowledge-bases // 获取用户的知识库列表
|
||
POST /api/knowledge-bases // 创建知识库
|
||
GET /api/knowledge-bases/:id // 获取知识库详情
|
||
PUT /api/knowledge-bases/:id // 更新知识库
|
||
DELETE /api/knowledge-bases/:id // 删除知识库
|
||
|
||
// 文档管理
|
||
GET /api/knowledge-bases/:kbId/documents // 获取文档列表
|
||
POST /api/knowledge-bases/:kbId/documents // 上传文档
|
||
GET /api/knowledge-bases/:kbId/documents/:docId // 获取文档详情
|
||
DELETE /api/knowledge-bases/:kbId/documents/:docId // 删除文档
|
||
|
||
// 检索
|
||
POST /api/knowledge-bases/:kbId/query // 检索知识库
|
||
|
||
// 配额查询
|
||
GET /api/users/me/quotas // 获取用户配额信息
|
||
```
|
||
|
||
---
|
||
|
||
## ✅ 总结
|
||
|
||
### 需求明确后的影响
|
||
|
||
| 方面 | 原理解 | 实际需求 | 影响 |
|
||
|------|--------|---------|------|
|
||
| **技术难度** | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ | 大幅降低 |
|
||
| **开发成本** | 需自建RAG | 用Dify即可 | 节省30天+ |
|
||
| **运营成本** | ¥200+/月 | ¥140/月 | 节省30% |
|
||
| **性能要求** | 需要优化 | 默认配置 | 简化 |
|
||
| **风险** | 高 | 低 | 降低 |
|
||
|
||
### 最终方案
|
||
|
||
**✅ 使用Dify处理知识库,完全满足需求!**
|
||
- 每用户3个知识库
|
||
- 每知识库50个文件
|
||
- PDF、DOCX格式
|
||
- 自动向量化、检索、答案溯源
|
||
|
||
**无需考虑其他RAG方案!**
|
||
|
||
---
|
||
|
||
**文档版本:v1.0**
|
||
**更新时间:2025-10-10**
|
||
|
||
|
||
|
||
|