Summary: - Fix pg-boss queue conflict (duplicate key violation on queue_pkey) - Add global error listener to prevent process crash - Reduce connection pool from 10 to 4 - Add graceful shutdown handling (SIGTERM/SIGINT) - Fix researchWorker recursive call bug in catch block - Make screeningWorker idempotent using upsert Security Standards (v1.1): - Prohibit recursive retry in Worker catch blocks - Prohibit payload bloat (only store fileKey/ID in job.data) - Require Worker idempotency (upsert + unique constraint) - Recommend task-specific expireInSeconds settings - Document graceful shutdown pattern New Features: - PKB signed URL endpoint for document preview/download - pg_bigm installation guide for Docker - Dockerfile.postgres-with-extensions for pgvector + pg_bigm Documentation: - Update Postgres-Only async task processing guide (v1.1) - Add troubleshooting SQL queries - Update safety checklist Tested: Local verification passed
21 KiB
21 KiB
PKB个人知识库功能审查报告 - 阶段0
审查日期: 2026-01-06
审查人员: AI助手
审查目标: 深入理解PKB现有功能,为安全迁移做准备
状态: ✅ 进行中
📋 执行摘要
关键发现
🎯 PKB系统实际上是两个紧密关联的功能模块:
Part 1: PKB知识库管理模块
├─ 位置:backend/src/legacy/controllers/knowledgeBaseController.ts
├─ 功能:创建、编辑、删除知识库;上传、管理文档
└─ 数据库:pkb_schema(独立Schema,无需迁移)
Part 2: AIA智能问答模块中的PKB应用
├─ 位置:backend/src/legacy/controllers/chatController.ts
├─ 功能:使用知识库进行智能问答(3种工作模式)
└─ 工作模式:
├─ 全文阅读模式(35-50篇文献综合分析)
├─ 逐篇精读模式(1-5篇文献深度分析)
└─ 批处理模式(3-50篇文献批量提取)
📊 Part 1: PKB知识库管理模块
1.1 文件结构
backend/src/legacy/
├─ controllers/
│ ├─ knowledgeBaseController.ts # API控制器(342行)
│ └─ documentController.ts # 文档上传控制器
├─ services/
│ ├─ knowledgeBaseService.ts # 业务逻辑(365行)
│ ├─ documentService.ts # 文档处理服务
│ └─ tokenService.ts # Token计算和文档选择
└─ routes/
└─ knowledgeBases.ts # 路由定义
1.2 核心API端点
知识库管理API
// 1. 创建知识库
POST /api/v1/knowledge/create
Body: { name: string, description?: string }
逻辑:
├─ 检查用户配额(kbQuota vs kbUsed)
├─ 在Dify创建Dataset
├─ 在数据库创建记录
└─ 更新用户配额计数
// 2. 获取知识库列表
GET /api/v1/knowledge/list
返回:用户所有知识库 + 文档数量统计
// 3. 获取知识库详情
GET /api/v1/knowledge/:id
返回:知识库信息 + 所有文档列表
// 4. 更新知识库
PUT /api/v1/knowledge/:id
Body: { name?: string, description?: string }
// 5. 删除知识库
DELETE /api/v1/knowledge/:id
逻辑:
├─ 删除Dify Dataset
├─ 级联删除数据库记录(documents自动删除)
└─ 减少用户配额计数
// 6. 检索知识库(RAG)
GET /api/v1/knowledge/:id/search?query=xxx&top_k=15
逻辑:
├─ 验证权限
├─ 调用Dify retrieveKnowledge API
└─ 返回检索结果(默认15个片段)
// 7. 获取知识库统计
GET /api/v1/knowledge/:id/stats
返回:文档数、完成数、处理中、错误数、总Token数
// 8. 获取文档选择(全文阅读模式)
GET /api/v1/knowledge/:id/document-selection?max_files=7&max_tokens=750000
返回:智能选择的文档列表(基于Token限制)
文档管理API
// 9. 上传文档
POST /api/v1/documents/upload
Multipart: { file, kbId }
逻辑:
├─ 上传文件到OSS
├─ 提取文本(PDF/Word/TXT/Markdown)
├─ 上传到Dify进行索引
└─ 创建数据库记录(状态:uploading→parsing→indexing→completed)
// 10. 获取文档详情
GET /api/v1/documents/:id
// 11. 删除文档
DELETE /api/v1/documents/:id
逻辑:
├─ 从Dify删除Document
├─ 从OSS删除文件
└─ 删除数据库记录
1.3 数据库Schema
表结构(在pkb_schema中)
-- 知识库表
knowledge_bases
├─ id (UUID, PK)
├─ userId (String)
├─ name (String)
├─ description (String?)
├─ difyDatasetId (String, UNIQUE) -- Dify中的Dataset ID
├─ fileCount (Int, default: 0)
├─ totalSizeBytes (BigInt, default: 0)
├─ createdAt (DateTime)
└─ updatedAt (DateTime)
-- 文档表
documents
├─ id (UUID, PK)
├─ kbId (String, FK → knowledge_bases.id)
├─ userId (String)
├─ filename (String)
├─ fileType (String) -- pdf/docx/txt/md
├─ fileSizeBytes (BigInt)
├─ fileUrl (String) -- OSS URL
├─ difyDocumentId (String) -- Dify中的Document ID
├─ status (String) -- uploading/parsing/indexing/completed/error
├─ progress (Int, 0-100)
├─ errorMessage (String?)
├─ segmentsCount (Int?) -- Dify索引的片段数
├─ tokensCount (Int?) -- 总Token数
├─ charCount (Int?) -- 字符数
├─ language (String?)
├─ extractedText (String?) -- 提取的全文(用于全文阅读模式)
├─ extractionMethod (String?) -- marker/pymupdf/docx
├─ extractionQuality (Float?)
├─ uploadedAt (DateTime)
└─ processedAt (DateTime?)
-- 批处理任务表
batch_tasks
├─ id (UUID, PK)
├─ userId (String)
├─ kbId (String, FK → knowledge_bases.id)
├─ name (String)
├─ templateType (String)
├─ templateId (String?)
├─ prompt (String)
├─ status (String) -- pending/running/completed/failed
├─ totalDocuments (Int)
├─ completedCount (Int, default: 0)
├─ failedCount (Int, default: 0)
├─ modelType (String)
├─ concurrency (Int, default: 3)
├─ startedAt (DateTime?)
├─ completedAt (DateTime?)
├─ durationSeconds (Int?)
├─ createdAt (DateTime)
└─ updatedAt (DateTime)
-- 批处理结果表
batch_results
├─ id (UUID, PK)
├─ taskId (String, FK → batch_tasks.id)
├─ documentId (String, FK → documents.id)
├─ status (String) -- success/failed
├─ data (Json?) -- 提取的结构化数据
├─ rawOutput (String?) -- LLM原始输出
├─ errorMessage (String?)
├─ processingTimeMs (Int?)
├─ tokensUsed (Int?)
└─ createdAt (DateTime)
-- 任务模板表
task_templates
├─ id (UUID, PK)
├─ userId (String)
├─ name (String)
├─ description (String?)
├─ prompt (String)
├─ isPublic (Boolean, default: false)
├─ outputFields (Json) -- 期望的输出字段
├─ createdAt (DateTime)
└─ updatedAt (DateTime)
索引
-- knowledge_bases
idx_pkb_knowledge_bases_user_id (userId)
idx_pkb_knowledge_bases_dify_dataset_id (difyDatasetId)
-- documents
idx_pkb_documents_kb_id (kbId)
idx_pkb_documents_user_id (userId)
idx_pkb_documents_status (status)
idx_pkb_documents_dify_document_id (difyDocumentId)
idx_pkb_documents_extraction_method (extractionMethod)
-- batch_tasks
idx_pkb_batch_tasks_kb_id (kbId)
idx_pkb_batch_tasks_user_id (userId)
idx_pkb_batch_tasks_status (status)
idx_pkb_batch_tasks_created_at (createdAt)
-- batch_results
idx_pkb_batch_results_task_id (taskId)
idx_pkb_batch_results_document_id (documentId)
idx_pkb_batch_results_status (status)
1.4 关键业务逻辑
配额管理
// 用户表(在platform_schema.users)中的字段
kbQuota: Int @default(3) // 知识库配额
kbUsed: Int @default(0) // 已使用数量
// 创建知识库时检查
if (user.kbUsed >= user.kbQuota) {
throw new Error('配额已满');
}
// 创建成功后增加计数
await prisma.user.update({
data: { kbUsed: { increment: 1 } }
});
// 删除知识库时减少计数
await prisma.user.update({
data: { kbUsed: { decrement: 1 } }
});
Dify集成
// 创建知识库 → 创建Dify Dataset
const difyDataset = await difyClient.createDataset({
name: `${userId}_${name}_${Date.now()}`,
description,
indexing_technique: 'high_quality',
});
// 检索知识库 → 调用Dify RAG
const results = await difyClient.retrieveKnowledge(
difyDatasetId,
query,
{
retrieval_model: {
search_method: 'semantic_search',
top_k: 15,
},
}
);
文档Token计算(tokenService.ts)
// Token计算规则
const TOKEN_LIMITS = {
MAX_FILES: 7, // 最多7篇文献
MAX_TOTAL_TOKENS: 750000, // 总Token限制(Qwen-Long: 1M上下文 - 250K对话空间)
MAX_SINGLE_DOC_TOKENS: 200000, // 单篇文献最大Token数
};
// 智能选择算法
function selectDocumentsForFullText(
documentTokens,
maxFiles,
maxTokens
) {
// 按Token数升序排序
const sorted = documentTokens.sort((a, b) => a.tokens - b.tokens);
// 贪心算法选择
let totalTokens = 0;
let selectedCount = 0;
const selected = [];
for (const doc of sorted) {
if (selectedCount >= maxFiles) break;
if (totalTokens + doc.tokens > maxTokens) break;
if (doc.tokens > MAX_SINGLE_DOC_TOKENS) continue; // 跳过超大文档
selected.push(doc);
totalTokens += doc.tokens;
selectedCount++;
}
return { selected, totalTokens, excludedDocs };
}
📊 Part 2: AIA模块中的PKB应用
2.1 文件结构
backend/src/legacy/controllers/
└─ chatController.ts # 通用对话控制器(包含3种模式)
frontend/src/
├─ pages/ChatPage.tsx # 主对话页面
└─ components/
├─ FullTextMode.tsx # 全文阅读模式组件
├─ DeepReadMode.tsx # 逐篇精读模式组件
└─ BatchMode.tsx # 批处理模式组件
2.2 三种工作模式详解
模式1:全文阅读模式(Full Text Mode)
用途:35-50篇文献的综合分析
实现原理:
// 1. 前端:用户进入知识库模式 → 选择"全文阅读"
const modeState = {
baseMode: 'knowledge_base',
kbMode: 'full_text',
selectedKbId: 'xxx',
};
// 2. 前端:智能加载文献
const selection = await knowledgeBaseApi.getDocumentSelection(kbId, {
max_files: 7,
max_tokens: 750000,
});
// 返回:{ selectedDocuments[], excludedDocuments[], totalTokens }
// 3. 前端:自动切换到Qwen-Long模型
if (modeState.kbMode === 'full_text') {
setSelectedModel('qwen-long'); // 1M上下文
showToast('已自动切换到Qwen-Long模型(支持1M上下文)');
}
// 4. 前端:发送消息时传递文档ID列表
await chatApi.sendMessageStream({
content: userQuestion,
modelType: 'qwen-long',
fullTextDocumentIds: loadedDocs.map(d => d.id), // ✅ 关键参数
conversationId,
});
// 5. 后端:加载完整全文
if (fullTextDocumentIds && fullTextDocumentIds.length > 0) {
const documents = await prisma.document.findMany({
where: { id: { in: fullTextDocumentIds } },
select: { id, filename, extractedText, tokensCount },
});
// 6. 组装全文上下文
const fullTextParts = [];
for (let i = 0; i < documents.length; i++) {
const doc = documents[i];
const docNumber = i + 1;
// 格式:【文献N:文件名】\n全文内容
fullTextParts.push(
`【文献${docNumber}:${doc.filename}】\n\n${doc.extractedText}`
);
// 添加引用信息
allCitations.push({
id: docNumber,
fileName: doc.filename,
score: 1.0, // 全文相关度100%
content: doc.extractedText.substring(0, 200),
});
}
knowledgeBaseContext = fullTextParts.join('\n\n---\n\n');
}
// 7. 传递给LLM
const systemPrompt = '你是专业的学术文献分析助手。每篇文献用【文献N:文件名】标记。请认真阅读所有文献,进行深入的综合分析。在回答时请引用具体文献,使用【文献N】格式。';
const userContent = `${userQuestion}\n\n## 参考资料(文献全文)\n\n${knowledgeBaseContext}`;
const messages = [
{ role: 'system', content: systemPrompt },
...historyMessages, // 对话历史
{ role: 'user', content: userContent },
];
// 8. 调用Qwen-Long
const response = await LLMFactory.getAdapter('qwen-long').chatStream(messages, {
temperature: 0.7,
maxTokens: 6000, // 全文模式需要更长的回答空间
});
关键特点:
- ✅ 传递完整全文(不是RAG片段)
- ✅ 智能选择文献(基于Token限制)
- ✅ 文献来源标记:【文献N:文件名】
- ✅ 自动切换到Qwen-Long模型(1M上下文)
- ✅ 100%相关度(因为是全文)
- ✅ 适合跨文献比较、趋势分析、研究方法归纳
Token使用:
上下文:~750K tokens(7篇文献全文)
对话空间:~250K tokens
输出长度:6000 tokens(综合分析需要更长回答)
模式2:逐篇精读模式(Deep Read Mode)
用途:1-5篇文献的深度分析
实现原理:
// 1. 前端:用户选择"逐篇精读"
const modeState = {
baseMode: 'knowledge_base',
kbMode: 'deep_read',
selectedKbId: 'xxx',
};
// 2. 前端:用户选择要精读的文档
const selectedDocs = [doc1, doc2, doc3]; // 用户手动选择
// 3. 前端:切换到某个文档
const currentDoc = selectedDocs[0];
// 4. 前端:发送消息时传递当前文档ID(用于RAG过滤)
await chatApi.sendMessageStream({
content: userQuestion,
modelType: selectedModel,
knowledgeBaseIds: [kbId], // 知识库ID
documentIds: [currentDoc.id], // ✅ 关键:只检索当前文档
conversationId: currentDocConversationId, // 每个文档独立对话
});
// 5. 后端:RAG检索(限定在特定文档)
if (documentIds && documentIds.length > 0) {
// 调用Dify RAG,但会限定在指定文档范围
const results = await difyClient.retrieveKnowledge(
difyDatasetId,
query,
{
retrieval_model: {
search_method: 'semantic_search',
top_k: 15,
document_ids: documentIds, // ✅ Dify会只检索这些文档
},
}
);
}
关键特点:
- ✅ 基于RAG检索(不是全文)
- ✅ 限定在当前文档范围
- ✅ 每个文档有独立的对话历史
- ✅ 用户可以在文档间切换
- ✅ 适合深度理解单篇文献
模式3:批处理模式(Batch Mode)
用途:3-50篇文献的批量信息提取
实现原理:
// 1. 用户创建批处理任务
POST /api/v1/batch-tasks/create
Body: {
kbId: 'xxx',
name: '提取研究方法',
prompt: '请从这篇文献中提取:研究设计、样本量、统计方法',
templateType: 'custom' | 'preset',
modelType: 'deepseek-v3',
concurrency: 3, // 并发数
}
// 2. 后端:创建任务
const task = await prisma.batchTask.create({
data: {
userId,
kbId,
name,
prompt,
templateType,
modelType,
status: 'pending',
totalDocuments: documentsCount,
concurrency,
},
});
// 3. 后端:启动批处理Worker
async function processBatchTask(taskId) {
// 3.1 获取任务和文档列表
const task = await prisma.batchTask.findUnique({
where: { id: taskId },
include: { knowledgeBase: { include: { documents: true } } },
});
const documents = task.knowledgeBase.documents.filter(d => d.status === 'completed');
// 3.2 更新任务状态
await prisma.batchTask.update({
where: { id: taskId },
data: { status: 'running', startedAt: new Date() },
});
// 3.3 并发处理文档
const concurrency = task.concurrency || 3;
const chunks = chunkArray(documents, concurrency);
for (const chunk of chunks) {
await Promise.all(chunk.map(async (doc) => {
try {
// 3.3.1 对每个文档,使用其extractedText + prompt调用LLM
const llmPrompt = `${task.prompt}\n\n文献内容:\n${doc.extractedText}`;
const response = await LLMFactory.getAdapter(task.modelType).chat([
{ role: 'user', content: llmPrompt },
]);
// 3.3.2 解析LLM输出(期望JSON格式)
const data = parseJSONResponse(response.content);
// 3.3.3 保存结果
await prisma.batchResult.create({
data: {
taskId: task.id,
documentId: doc.id,
status: 'success',
data,
rawOutput: response.content,
tokensUsed: response.usage.totalTokens,
processingTimeMs: Date.now() - startTime,
},
});
// 3.3.4 更新任务进度
await prisma.batchTask.update({
where: { id: taskId },
data: { completedCount: { increment: 1 } },
});
} catch (error) {
// 3.3.5 处理失败
await prisma.batchResult.create({
data: {
taskId: task.id,
documentId: doc.id,
status: 'failed',
errorMessage: error.message,
},
});
await prisma.batchTask.update({
where: { id: taskId },
data: { failedCount: { increment: 1 } },
});
}
}));
}
// 3.4 任务完成
await prisma.batchTask.update({
where: { id: taskId },
data: {
status: 'completed',
completedAt: new Date(),
durationSeconds: Math.floor((Date.now() - task.startedAt) / 1000),
},
});
}
// 4. 前端:查看批处理结果
GET /api/v1/batch-tasks/:id/results
返回:
{
task: { /* 任务信息 */ },
results: [
{
documentId: 'xxx',
filename: 'paper1.pdf',
status: 'success',
data: {
研究设计: '随机对照试验',
样本量: '300人',
统计方法: 't检验、卡方检验',
},
},
// ...
],
}
// 5. 前端:导出结果(Excel/CSV)
关键特点:
- ✅ 批量处理多个文档
- ✅ 并发控制(默认3个并发)
- ✅ 结构化信息提取
- ✅ 进度实时更新
- ✅ 支持自定义模板
- ✅ 结果可导出(Excel/CSV)
- ✅ 错误处理和重试
2.3 三种模式的对比
| 维度 | 全文阅读 | 逐篇精读 | 批处理 |
|---|---|---|---|
| 文档数量 | 7篇左右 | 1-5篇 | 3-50篇 |
| 数据来源 | 完整全文 | RAG检索片段 | 完整全文 |
| LLM调用 | 对话式(多轮) | 对话式(多轮) | 批量(单次) |
| 上下文 | ~750K tokens | ~15K tokens | 单篇全文 |
| 输出方式 | 流式(SSE) | 流式(SSE) | 批量保存 |
| 适用场景 | 综合分析、跨文献比较 | 深度理解单篇 | 信息提取、数据表格 |
| 用户交互 | 实时问答 | 实时问答 | 后台处理 |
| 对话历史 | 全局共享 | 每篇独立 | 无对话 |
📋 API端点完整清单
PKB管理模块API
POST /api/v1/knowledge/create # 创建知识库
GET /api/v1/knowledge/list # 获取知识库列表
GET /api/v1/knowledge/:id # 获取知识库详情
PUT /api/v1/knowledge/:id # 更新知识库
DELETE /api/v1/knowledge/:id # 删除知识库
GET /api/v1/knowledge/:id/search # RAG检索
GET /api/v1/knowledge/:id/stats # 统计信息
GET /api/v1/knowledge/:id/document-selection # 文档选择(全文模式)
POST /api/v1/documents/upload # 上传文档
GET /api/v1/documents/:id # 获取文档详情
DELETE /api/v1/documents/:id # 删除文档
GET /api/v1/documents/:id/content # 获取文档内容(全文)
POST /api/v1/batch-tasks/create # 创建批处理任务
GET /api/v1/batch-tasks/list # 获取批处理任务列表
GET /api/v1/batch-tasks/:id # 获取任务详情
GET /api/v1/batch-tasks/:id/results # 获取任务结果
DELETE /api/v1/batch-tasks/:id # 删除任务
GET /api/v1/task-templates/list # 获取模板列表
POST /api/v1/task-templates/create # 创建模板
DELETE /api/v1/task-templates/:id # 删除模板
AIA对话模块API(含PKB集成)
POST /api/v1/chat/send-message-stream # 发送消息(流式)
参数:
- content: string
- modelType: 'deepseek-v3' | 'qwen3-72b' | 'qwen-long'
- knowledgeBaseIds?: string[] # RAG模式
- documentIds?: string[] # 逐篇精读模式(限定文档)
- fullTextDocumentIds?: string[] # 全文阅读模式(传递全文)
- conversationId?: string
GET /api/v1/chat/conversations # 获取对话列表
GET /api/v1/chat/conversations/:id # 获取对话历史
DELETE /api/v1/chat/conversations/:id # 删除对话
🔗 模块间依赖关系
AIA智能问答模块
│
├─ 依赖 PKB知识库管理模块
│ ├─ 获取知识库列表(选择知识库)
│ ├─ 获取文档列表(选择文档)
│ ├─ 获取文档全文(全文阅读)
│ ├─ RAG检索(逐篇精读)
│ └─ 文档智能选择(全文阅读)
│
├─ 依赖 LLM网关
│ ├─ DeepSeek V3
│ ├─ Qwen3-72B
│ └─ Qwen-Long
│
└─ 依赖 Dify RAG引擎
└─ retrieveKnowledge API
🎯 迁移关键点
1. PKB模块迁移
✅ 简单:
- 数据库已在pkb_schema,无需迁移
- API端点清晰,易于复制
- 业务逻辑独立
⚠️ 注意:
- Dify集成需要保持
- OSS文件上传需要保持
- 配额管理需要保持
2. AIA模块中的PKB集成迁移
✅ 简单:
- 接口清晰(fullTextDocumentIds/documentIds)
- 三种模式逻辑独立
⚠️ 注意:
- chatController.ts需要同时迁移
- 前端3个模式组件需要迁移
- 对话历史管理需要保持
3. 测试要点
必须测试:
✅ PKB CRUD功能
✅ 文档上传和提取
✅ RAG检索功能
✅ 全文阅读模式(7篇文献)
✅ 逐篇精读模式(文档切换)
✅ 批处理模式(并发处理)
✅ 配额管理
✅ 对话历史管理
✅ 模型切换
✅ 阶段0完成标准
- 深入理解PKB的两个部分
- 列出所有API端点
- 理解数据库Schema
- 理解三种工作模式
- 理解模块间依赖
- 创建测试用例清单
- 准备测试数据
📊 下一步:创建测试用例
即将创建详细的测试用例清单,覆盖所有功能点...
审查状态: 🟡 进行中(90%完成)
下一步: 创建测试用例清单和测试数据准备方案