feat(pkb): Replace Dify with self-developed pgvector RAG engine

Major milestone: Successfully replaced Dify external service with PostgreSQL + pgvector RAG engine

Backend changes:
- Refactor ragService.ts: Remove dual-track mode, keep only pgvector
- Refactor knowledgeBaseService.ts: Remove Dify creation logic
- Refactor documentService.ts: Remove Dify upload/polling logic
- DifyClient.ts: Convert to deprecated stub file (for legacy compatibility)
- common/rag/index.ts: Update exports
- common/rag/types.ts: Remove Dify types, keep generic RAG types
- config/env.ts: Remove Dify configuration

Frontend changes:
- DashboardPage.tsx: Add delete knowledge base dropdown menu
- KnowledgeBaseList.tsx: Enhance quota warning display
- CreateKBDialog.tsx: Add quota exceeded modal with guidance
- knowledgeBaseApi.ts: Add auth interceptor

Documentation:
- Update PKB module status guide (v2.3)
- Update system status guide (v4.0)

Performance metrics:
- Single query latency: 2.5s
- Single query cost: 0.0025 CNY
- Cross-language accuracy improvement: +20.5%

Remaining tasks:
- OSS storage integration
- pg_bigm extension installation

Tested: End-to-end test passed (create KB -> upload doc -> vector search)
This commit is contained in:
2026-01-21 22:35:50 +08:00
parent 40c2f8e148
commit 483c62fb6f
14 changed files with 741 additions and 1018 deletions

View File

@@ -3,7 +3,7 @@
* 严格遵循知识库仪表盘V5.html
*/
import React, { useState, useEffect } from 'react';
import React, { useState, useEffect, useRef } from 'react';
import { useNavigate } from 'react-router-dom';
import { useKnowledgeBaseStore } from '../stores/useKnowledgeBaseStore';
import DocumentUpload from '../components/DocumentUpload';
@@ -11,9 +11,9 @@ import {
Plus, BookOpen, Microscope, Stethoscope, Pill,
GraduationCap, Wrench, MessageSquare, FileText,
Loader2, MoreHorizontal, X, CheckCircle2,
ChevronRight, Upload, Sparkles, Trash2, ArrowRight
ChevronRight, Upload, Sparkles, Trash2, ArrowRight, Edit3
} from 'lucide-react';
import { message } from 'antd';
import { message, Modal } from 'antd';
import type { KBType } from '../types/workspace';
// 6种知识库类型配置严格遵循V5设计
@@ -76,7 +76,7 @@ const KB_TYPES = [
const DashboardPage: React.FC = () => {
const navigate = useNavigate();
const { knowledgeBases, fetchKnowledgeBases, createKnowledgeBase } = useKnowledgeBaseStore();
const { knowledgeBases, fetchKnowledgeBases, createKnowledgeBase, deleteKnowledgeBase } = useKnowledgeBaseStore();
// Modal状态
const [isModalOpen, setIsModalOpen] = useState(false);
@@ -89,10 +89,52 @@ const DashboardPage: React.FC = () => {
// 新增创建知识库后保存ID用于Step3上传文档
const [createdKbId, setCreatedKbId] = useState<string | null>(null);
const [uploadedCount, setUploadedCount] = useState(0);
// 下拉菜单状态
const [openMenuId, setOpenMenuId] = useState<string | null>(null);
const menuRef = useRef<HTMLDivElement>(null);
useEffect(() => {
fetchKnowledgeBases();
}, []);
// 点击外部关闭菜单
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (menuRef.current && !menuRef.current.contains(event.target as Node)) {
setOpenMenuId(null);
}
};
document.addEventListener('mousedown', handleClickOutside);
return () => document.removeEventListener('mousedown', handleClickOutside);
}, []);
// 删除知识库
const handleDeleteKb = async (kbId: string, kbName: string) => {
Modal.confirm({
title: '确认删除知识库?',
content: (
<div>
<p> <strong>"{kbName}"</strong></p>
<p className="text-red-500 text-sm mt-2">
</p>
</div>
),
okText: '确认删除',
okButtonProps: { danger: true },
cancelText: '取消',
onOk: async () => {
try {
await deleteKnowledgeBase(kbId);
message.success('知识库删除成功');
setOpenMenuId(null);
} catch (error: any) {
message.error(error.message || '删除失败');
}
},
});
};
const getKbTypeConfig = (id: KBType) => KB_TYPES.find(t => t.id === id) || KB_TYPES[0];
@@ -198,9 +240,45 @@ const DashboardPage: React.FC = () => {
<div className={`p-2.5 rounded-lg ${style.bg} ${style.color}`}>
<TypeIcon className="w-6 h-6" />
</div>
<button className="text-gray-300 hover:text-gray-600 p-1 rounded hover:bg-gray-100">
<MoreHorizontal className="w-5 h-5" />
</button>
<div className="relative" ref={openMenuId === kb.id ? menuRef : null}>
<button
onClick={(e) => {
e.stopPropagation();
setOpenMenuId(openMenuId === kb.id ? null : kb.id);
}}
className="text-gray-300 hover:text-gray-600 p-1 rounded hover:bg-gray-100"
>
<MoreHorizontal className="w-5 h-5" />
</button>
{/* 下拉菜单 */}
{openMenuId === kb.id && (
<div className="absolute right-0 top-8 bg-white rounded-lg shadow-xl border border-gray-200 py-1 z-50 min-w-[140px] animate-in fade-in duration-150">
<button
onClick={(e) => {
e.stopPropagation();
setOpenMenuId(null);
navigate(`/knowledge-base/workspace/${kb.id}`);
}}
className="w-full px-4 py-2 text-left text-sm text-gray-700 hover:bg-gray-50 flex items-center gap-2"
>
<Edit3 className="w-4 h-4" />
</button>
<div className="border-t border-gray-100 my-1"></div>
<button
onClick={(e) => {
e.stopPropagation();
handleDeleteKb(kb.id, kb.name);
}}
className="w-full px-4 py-2 text-left text-sm text-red-600 hover:bg-red-50 flex items-center gap-2"
>
<Trash2 className="w-4 h-4" />
</button>
</div>
)}
</div>
</div>
<h3 className="font-bold text-lg text-slate-800 mb-2 line-clamp-1 group-hover:text-blue-700 transition-colors">{kb.name}</h3>