feat: 完善Dify知识库管理召回测试模块,优化知识库上传文件时的分段配置设置

This commit is contained in:
PingChuan
2025-12-03 12:03:09 +08:00
parent 0f49426a2e
commit a13f3b3635
29 changed files with 4016 additions and 1880 deletions
@@ -0,0 +1,92 @@
import { useState, useEffect, useCallback } from 'react';
import { message } from 'antd';
import type { FormInstance } from 'antd';
import type { Dataset } from '~/api/dify-dataset/type/datasetTypes';
import { updateDatasetName } from '~/api/dify-dataset/api/datasetApi';
/**
* 知识库设置状态管理 Hook
*/
export function useDatasetSettings(
dataset: Dataset | null,
form: FormInstance,
onDatasetUpdated: (dataset: Dataset) => void
) {
const [saving, setSaving] = useState(false);
const [hasChanges, setHasChanges] = useState(false);
// 初始化表单数据
useEffect(() => {
if (dataset) {
form.setFieldsValue({
name: dataset.name,
description: dataset.description || '',
});
setHasChanges(false);
}
}, [dataset, form]);
/**
* 处理表单值变化
*/
const handleValuesChange = useCallback(() => {
const values = form.getFieldsValue();
const changed =
values.name !== dataset?.name ||
values.description !== (dataset?.description || '');
setHasChanges(changed);
}, [form, dataset]);
/**
* 保存设置
*/
const handleSave = useCallback(async () => {
if (!dataset) {
message.error('知识库不存在');
return;
}
try {
const values = await form.validateFields();
setSaving(true);
// 目前只支持修改名称
const updatedDataset = await updateDatasetName(dataset.id, values.name);
message.success('保存成功');
onDatasetUpdated(updatedDataset);
setHasChanges(false);
} catch (err: any) {
console.error('保存设置失败:', err);
message.error(err.message || '保存失败');
} finally {
setSaving(false);
}
}, [dataset, form, onDatasetUpdated]);
/**
* 重置表单
*/
const handleReset = useCallback(() => {
if (dataset) {
form.setFieldsValue({
name: dataset.name,
description: dataset.description || '',
});
setHasChanges(false);
}
}, [dataset, form]);
return {
// 状态
saving,
hasChanges,
// 方法
handleValuesChange,
handleSave,
handleReset,
};
}
export type UseDatasetSettingsReturn = ReturnType<typeof useDatasetSettings>;
@@ -0,0 +1,123 @@
import { message } from 'antd';
import { useCallback, useEffect, useState } from 'react';
import { updateDocumentWithSettings } from '~/api/dify-dataset/api/documentApi';
import { fetchSegments } from '~/api/dify-dataset/api/segmentApi';
import type { Segment } from '~/api/dify-dataset/type';
import type { Document } from '~/api/dify-dataset/type/documentTypes';
import type { DocumentDetailSegmentationSettings } from '~/types/dify-dataset-manager/document-detail';
import { DEFAULT_DOCUMENT_DETAIL_SETTINGS } from '~/types/dify-dataset-manager/document-detail';
/**
* 文档详情状态管理 Hook
*/
export function useDocumentDetail(datasetId: string, document: Document | null) {
// 分段设置状态
const [settings, setSettings] = useState<DocumentDetailSegmentationSettings>(DEFAULT_DOCUMENT_DETAIL_SETTINGS);
// 预览状态
const [previewSegments, setPreviewSegments] = useState<Segment[]>([]);
const [previewLoading, setPreviewLoading] = useState(false);
const [showPreview, setShowPreview] = useState(false);
// 保存状态
const [saving, setSaving] = useState(false);
// 当文档变化时重置设置
useEffect(() => {
if (document) {
// 可以从文档中读取已有的设置,这里使用默认值
setSettings(DEFAULT_DOCUMENT_DETAIL_SETTINGS);
setPreviewSegments([]);
setShowPreview(false);
}
}, [document?.id]);
/**
* 更新设置
*/
const updateSettings = useCallback((key: keyof DocumentDetailSegmentationSettings, value: any) => {
setSettings(prev => ({ ...prev, [key]: value }));
}, []);
/**
* 重置设置
*/
const handleReset = useCallback(() => {
setSettings(DEFAULT_DOCUMENT_DETAIL_SETTINGS);
setPreviewSegments([]);
setShowPreview(false);
}, []);
/**
* 预览分段
*/
const handlePreview = useCallback(async () => {
if (!document) return;
setPreviewLoading(true);
setShowPreview(true);
try {
// 获取当前文档的分段作为预览
const response = await fetchSegments(datasetId, document.id, 1, 50);
setPreviewSegments(response.data || []);
if (response.data?.length === 0) {
message.info('该文档暂无分段数据');
}
} catch (err: any) {
console.error('预览分段失败:', err);
message.error(err.message || '预览失败');
} finally {
setPreviewLoading(false);
}
}, [datasetId, document]);
/**
* 保存并处理
*/
const handleSaveAndProcess = useCallback(async () => {
if (!document) return;
setSaving(true);
try {
await updateDocumentWithSettings(datasetId, document.id, {
indexing_technique: 'high_quality',
process_rule: {
mode: 'custom',
rules: {
pre_processing_rules: [
{ id: 'remove_extra_spaces', enabled: settings.removeExtraSpaces },
{ id: 'remove_urls_emails', enabled: settings.removeUrlsEmails },
],
segmentation: {
separator: settings.separator.replace(/\\n/g, '\n'),
max_tokens: settings.maxTokens,
},
},
},
});
message.success('设置已保存,文档正在重新处理...');
} catch (err: any) {
console.error('保存设置失败:', err);
message.error(err.message || '保存失败');
} finally {
setSaving(false);
}
}, [datasetId, document, settings]);
return {
// 状态
settings,
previewSegments,
previewLoading,
showPreview,
saving,
// 方法
updateSettings,
handleReset,
handlePreview,
handleSaveAndProcess,
};
}
export type UseDocumentDetailReturn = ReturnType<typeof useDocumentDetail>;
@@ -0,0 +1,156 @@
import { useState, useCallback } from 'react';
import { message } from 'antd';
import type { Document, IndexingStatus } from '~/api/dify-dataset/type/documentTypes';
import { deleteDocument, toggleDocumentStatus } from '~/api/dify-dataset/api/documentApi';
import {
CheckCircleOutlined,
SyncOutlined,
ClockCircleOutlined,
PauseCircleOutlined,
ExclamationCircleOutlined,
} from '@ant-design/icons';
import type { StatusConfig } from '~/types/dify-dataset-manager/document-list';
/**
* 文档列表状态管理 Hook
*/
export function useDocumentList(
datasetId: string,
onDocumentDeleted: (documentId: string) => void,
onDocumentStatusChanged: (documentId: string, enabled: boolean) => void,
onRefresh: () => void
) {
const [searchValue, setSearchValue] = useState('');
const [deletingId, setDeletingId] = useState<string | null>(null);
const [showUploadPage, setShowUploadPage] = useState(false);
/**
* 获取状态标签配置
*/
const getStatusConfig = useCallback((status: IndexingStatus): StatusConfig => {
const configs: Record<IndexingStatus, StatusConfig> = {
completed: { color: 'success', icon: <CheckCircleOutlined />, text: '已完成' },
indexing: { color: 'processing', icon: <SyncOutlined spin />, text: '索引中' },
waiting: { color: 'warning', icon: <ClockCircleOutlined />, text: '等待中' },
parsing: { color: 'processing', icon: <SyncOutlined spin />, text: '解析中' },
cleaning: { color: 'processing', icon: <SyncOutlined spin />, text: '清洗中' },
splitting: { color: 'processing', icon: <SyncOutlined spin />, text: '分段中' },
paused: { color: 'default', icon: <PauseCircleOutlined />, text: '已暂停' },
error: { color: 'error', icon: <ExclamationCircleOutlined />, text: '错误' },
};
return configs[status] || { color: 'default', icon: null, text: status };
}, []);
/**
* 格式化日期
*/
const formatDate = useCallback((timestamp: number) => {
return new Date(timestamp * 1000).toLocaleString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
});
}, []);
/**
* 格式化数字
*/
const formatNumber = useCallback((num: number) => {
if (num >= 10000) {
return (num / 10000).toFixed(1) + 'w';
}
if (num >= 1000) {
return (num / 1000).toFixed(1) + 'k';
}
return num.toString();
}, []);
/**
* 处理删除文档
*/
const handleDelete = useCallback(async (documentId: string) => {
setDeletingId(documentId);
try {
await deleteDocument(datasetId, documentId);
message.success('删除成功');
onDocumentDeleted(documentId);
} catch (err: any) {
console.error('删除文档失败:', err);
message.error(err.message || '删除失败');
} finally {
setDeletingId(null);
}
}, [datasetId, onDocumentDeleted]);
/**
* 处理启用/禁用文档
*/
const handleToggleStatus = useCallback(async (documentId: string, enabled: boolean) => {
try {
await toggleDocumentStatus(datasetId, documentId, enabled);
message.success(enabled ? '已启用' : '已禁用');
onDocumentStatusChanged(documentId, enabled);
} catch (err: any) {
console.error('切换文档状态失败:', err);
message.error(err.message || '操作失败');
}
}, [datasetId, onDocumentStatusChanged]);
/**
* 点击上传按钮,显示上传页面
*/
const handleUploadClick = useCallback(() => {
if (!datasetId) {
message.error('请先选择知识库');
return;
}
setShowUploadPage(true);
}, [datasetId]);
/**
* 关闭上传页面
*/
const handleUploadClose = useCallback(() => {
setShowUploadPage(false);
}, []);
/**
* 上传成功回调
*/
const handleUploadSuccess = useCallback(() => {
setShowUploadPage(false);
onRefresh();
}, [onRefresh]);
/**
* 过滤文档
*/
const filterDocuments = useCallback((documents: Document[]) => {
return documents.filter((doc) =>
doc.name.toLowerCase().includes(searchValue.toLowerCase())
);
}, [searchValue]);
return {
// 状态
searchValue,
setSearchValue,
deletingId,
showUploadPage,
// 方法
getStatusConfig,
formatDate,
formatNumber,
handleDelete,
handleToggleStatus,
handleUploadClick,
handleUploadClose,
handleUploadSuccess,
filterDocuments,
};
}
export type UseDocumentListReturn = ReturnType<typeof useDocumentList>;
@@ -0,0 +1,533 @@
import type { UploadFile, UploadProps } from 'antd';
import { message } from 'antd';
import { useCallback, useEffect, useRef, useState } from 'react';
import {
fetchIndexingStatus,
updateDocumentByFile,
uploadDocumentWithConfig,
} from '~/api/dify-dataset/api/documentApi';
import { fetchSegments } from '~/api/dify-dataset/api/segmentApi';
import type { IndexingStatus } from '~/api/dify-dataset/type/documentTypes';
import type {
DocumentStage,
SegmentationSettings,
UploadedDocument,
} from '~/types/dify-dataset-manager/document-upload';
import {
DEFAULT_SEGMENTATION_SETTINGS,
INDEXING_STATUS_CONFIG,
} from '~/types/dify-dataset-manager/document-upload';
/**
* 文档上传状态管理 Hook
*/
export function useDocumentUpload(datasetId: string, onClose: () => void, onSuccess: () => void) {
// 步骤控制
const [step, setStep] = useState<1 | 2>(1);
// 文件相关
const [selectedFiles, setSelectedFiles] = useState<File[]>([]);
const [fileList, setFileList] = useState<UploadFile[]>([]);
// 多文档状态管理
const [uploadedDocuments, setUploadedDocuments] = useState<UploadedDocument[]>([]);
// 当前选中查看的文档索引
const [currentDocIndex, setCurrentDocIndex] = useState(0);
// 当前显示的分段设置(来自当前选中的文档)
const [currentSettings, setCurrentSettings] = useState<SegmentationSettings>(DEFAULT_SEGMENTATION_SETTINGS);
// 预览相关
const [previewLoading, setPreviewLoading] = useState(false);
// 轮询定时器(支持多个文档)
const pollingTimersRef = useRef<Map<string, NodeJS.Timeout>>(new Map());
// 状态追赶定时器
const statusCatchUpTimersRef = useRef<Map<string, NodeJS.Timeout>>(new Map());
// 状态顺序
const STATUS_ORDER: IndexingStatus[] = ['waiting', 'parsing', 'cleaning', 'splitting', 'indexing', 'completed'];
/**
* 停止指定文档的轮询
*/
const stopPolling = useCallback((documentId: string) => {
const timer = pollingTimersRef.current.get(documentId);
if (timer) {
clearInterval(timer);
pollingTimersRef.current.delete(documentId);
}
}, []);
/**
* 停止所有轮询
*/
const stopAllPolling = useCallback(() => {
pollingTimersRef.current.forEach(timer => clearInterval(timer));
pollingTimersRef.current.clear();
}, []);
/**
* 加载分段预览
*/
const loadSegmentsPreview = useCallback(async (documentId: string, docIndex: number) => {
setPreviewLoading(true);
try {
const response = await fetchSegments(datasetId, documentId, 1, 50);
const segments = response.data || [];
// 更新对应文档的分段
setUploadedDocuments(prev => prev.map((doc, idx) =>
idx === docIndex ? { ...doc, segments } : doc
));
} catch (err: any) {
console.error('加载分段预览失败:', err);
message.error('加载分段预览失败');
} finally {
setPreviewLoading(false);
}
}, [datasetId]);
// 清理所有轮询定时器
useEffect(() => {
return () => {
pollingTimersRef.current.forEach(timer => clearInterval(timer));
pollingTimersRef.current.clear();
statusCatchUpTimersRef.current.forEach(timer => clearTimeout(timer));
statusCatchUpTimersRef.current.clear();
};
}, []);
/**
* 状态追赶逻辑
*/
useEffect(() => {
uploadedDocuments.forEach((doc, index) => {
// 如果没有真实状态,或者已经完成/错误,或者正在追赶中(有定时器),则跳过
if (!doc.realIndexingStatus || doc.stage === 'error' || statusCatchUpTimersRef.current.has(doc.documentId)) {
return;
}
const currentIndex = STATUS_ORDER.indexOf(doc.indexingStatus);
const targetIndex = STATUS_ORDER.indexOf(doc.realIndexingStatus);
// 如果当前显示状态落后于真实状态
if (currentIndex < targetIndex) {
// 设置定时器,1秒后更新到下一个状态
const timer = setTimeout(() => {
setUploadedDocuments(prev => prev.map((d, idx) => {
if (idx !== index) return d;
const nextStatus = STATUS_ORDER[currentIndex + 1];
const isCompleted = nextStatus === 'completed';
// 如果到达完成状态,且真实状态也是完成,则触发完成逻辑
if (isCompleted && d.realIndexingStatus === 'completed') {
stopPolling(d.documentId);
// 自动加载分段预览
loadSegmentsPreview(d.documentId, index);
return { ...d, indexingStatus: nextStatus, stage: 'completed' };
}
return { ...d, indexingStatus: nextStatus };
}));
// 清除定时器引用
statusCatchUpTimersRef.current.delete(doc.documentId);
}, 1000); // 至少停留1秒
statusCatchUpTimersRef.current.set(doc.documentId, timer);
}
});
}, [uploadedDocuments, stopPolling, loadSegmentsPreview]);
/**
* 轮询索引状态
*/
const pollIndexingStatus = useCallback(async (batch: string, documentId: string, docIndex: number) => {
try {
const response = await fetchIndexingStatus(datasetId, batch);
const documentStatus = response.data?.[0];
if (documentStatus) {
const realStatus = documentStatus.indexing_status as IndexingStatus;
// 更新文档状态(只更新真实状态和统计信息,显示状态由 useEffect 控制)
setUploadedDocuments(prev => prev.map((doc, idx) => {
if (idx !== docIndex) return doc;
// 如果已经是 error 状态,直接更新
if (realStatus === 'error') {
stopPolling(documentId);
return {
...doc,
stage: 'error',
error: documentStatus.error || '处理失败',
realIndexingStatus: realStatus
};
}
return {
...doc,
realIndexingStatus: realStatus,
completedSegments: documentStatus.completed_segments,
totalSegments: documentStatus.total_segments
};
}));
}
} catch (err) {
console.error('获取索引状态失败:', err);
}
}, [datasetId, stopPolling]);
/**
* 开始轮询
*/
const startPolling = useCallback((batch: string, documentId: string, docIndex: number) => {
// 先停止之前的轮询
stopPolling(documentId);
// 开始新的轮询
const timer = setInterval(() => {
pollIndexingStatus(batch, documentId, docIndex);
}, 2000);
pollingTimersRef.current.set(documentId, timer);
// 立即执行一次
pollIndexingStatus(batch, documentId, docIndex);
}, [stopPolling, pollIndexingStatus]);
/**
* 构建上传配置
*/
const buildConfig = useCallback((s: SegmentationSettings) => ({
indexing_technique: s.indexingTechnique,
process_rule: {
mode: 'custom' as const,
rules: {
pre_processing_rules: [
{ id: 'remove_extra_spaces' as const, enabled: s.removeExtraSpaces },
{ id: 'remove_urls_emails' as const, enabled: s.removeUrlsEmails },
],
segmentation: {
separator: s.separator.replace(/\\n/g, '\n'),
max_tokens: s.maxTokens,
},
},
},
}), []);
/**
* 更新当前文档的设置
*/
const updateCurrentSettings = useCallback((key: keyof SegmentationSettings, value: any) => {
setCurrentSettings(prev => {
const newSettings = { ...prev, [key]: value };
// 同步更新到文档列表
setUploadedDocuments(prevDocs => prevDocs.map((doc, idx) =>
idx === currentDocIndex ? { ...doc, settings: newSettings } : doc
));
return newSettings;
});
}, [currentDocIndex]);
/**
* 处理文件选择变化
*/
const handleFileChange: UploadProps['onChange'] = useCallback(({ fileList: newFileList }: { fileList: UploadFile[] }) => {
setFileList(newFileList);
// 提取实际文件对象
const files = newFileList
.filter((f: UploadFile) => f.originFileObj)
.map((f: UploadFile) => f.originFileObj as File);
setSelectedFiles(files);
}, []);
/**
* 移除文件
*/
const handleRemoveFile = useCallback((file: UploadFile) => {
setFileList(prev => {
const newFileList = prev.filter(f => f.uid !== file.uid);
const files = newFileList
.filter(f => f.originFileObj)
.map(f => f.originFileObj as File);
setSelectedFiles(files);
return newFileList;
});
}, []);
/**
* 上传单个文件
*/
const uploadSingleFile = useCallback(async (file: File, index: number): Promise<void> => {
try {
// 更新状态为上传中
setUploadedDocuments(prev => prev.map((doc, idx) =>
idx === index ? { ...doc, stage: 'uploading' as DocumentStage } : doc
));
const config = buildConfig(DEFAULT_SEGMENTATION_SETTINGS);
const result = await uploadDocumentWithConfig(
datasetId,
file,
config,
(percent) => {
setUploadedDocuments(prev => prev.map((doc, idx) =>
idx === index ? { ...doc, uploadProgress: percent } : doc
));
}
);
// 更新文档信息
setUploadedDocuments(prev => prev.map((doc, idx) =>
idx === index ? {
...doc,
documentId: result.document.id,
batch: result.batch,
stage: 'indexing' as DocumentStage,
indexingStatus: 'waiting' as IndexingStatus,
realIndexingStatus: 'waiting' as IndexingStatus, // 初始化真实状态
} : doc
));
// 开始轮询索引状态
startPolling(result.batch, result.document.id, index);
} catch (err: any) {
console.error(`上传文档 ${file.name} 失败:`, err);
setUploadedDocuments(prev => prev.map((doc, idx) =>
idx === index ? {
...doc,
stage: 'error' as DocumentStage,
error: err.message || '上传失败',
} : doc
));
}
}, [datasetId, buildConfig, startPolling]);
/**
* 点击"下一步":立即上传所有文件
*/
const handleNextStep = useCallback(async () => {
if (selectedFiles.length === 0) {
message.warning('请先选择文件');
return;
}
// 初始化所有文档状态
const docs: UploadedDocument[] = selectedFiles.map(file => ({
file,
documentId: '',
batch: '',
stage: 'pending' as DocumentStage,
indexingStatus: 'waiting' as IndexingStatus,
realIndexingStatus: 'waiting' as IndexingStatus, // 初始化真实状态
uploadProgress: 0,
settings: { ...DEFAULT_SEGMENTATION_SETTINGS },
segments: [],
}));
setUploadedDocuments(docs);
setCurrentDocIndex(0);
setCurrentSettings({ ...DEFAULT_SEGMENTATION_SETTINGS });
setStep(2);
// 依次上传所有文件
for (let i = 0; i < selectedFiles.length; i++) {
await uploadSingleFile(selectedFiles[i], i);
}
}, [selectedFiles, uploadSingleFile]);
/**
* 切换查看的文档
*/
const handleDocumentChange = useCallback((docId: string) => {
const index = uploadedDocuments.findIndex(doc => doc.documentId === docId || doc.file.name === docId);
if (index !== -1) {
setCurrentDocIndex(index);
const doc = uploadedDocuments[index];
setCurrentSettings(doc.settings);
}
}, [uploadedDocuments]);
/**
* 修改参数后重新处理当前文档
*/
const handleReprocess = useCallback(async () => {
const currentDoc = uploadedDocuments[currentDocIndex];
if (!currentDoc || !currentDoc.documentId) return;
// 更新状态
setUploadedDocuments(prev => prev.map((doc, idx) =>
idx === currentDocIndex ? {
...doc,
stage: 'uploading' as DocumentStage,
uploadProgress: 0,
segments: [],
} : doc
));
try {
const config = buildConfig(currentSettings);
const result = await updateDocumentByFile(
datasetId,
currentDoc.documentId,
currentDoc.file,
config,
(percent) => {
setUploadedDocuments(prev => prev.map((doc, idx) =>
idx === currentDocIndex ? { ...doc, uploadProgress: percent } : doc
));
}
);
// 更新 batch
setUploadedDocuments(prev => prev.map((doc, idx) =>
idx === currentDocIndex ? {
...doc,
batch: result.batch,
stage: 'indexing' as DocumentStage,
indexingStatus: 'waiting' as IndexingStatus,
realIndexingStatus: 'waiting' as IndexingStatus, // 初始化真实状态
} : doc
));
startPolling(result.batch, currentDoc.documentId, currentDocIndex);
} catch (err: any) {
console.error('重新处理失败:', err);
setUploadedDocuments(prev => prev.map((doc, idx) =>
idx === currentDocIndex ? {
...doc,
stage: 'error' as DocumentStage,
error: err.message || '重新处理失败',
} : doc
));
message.error(err.message || '重新处理失败');
}
}, [uploadedDocuments, currentDocIndex, currentSettings, datasetId, buildConfig, startPolling]);
/**
* 返回上一步
*/
const handlePrevStep = useCallback(() => {
// 检查是否有文档正在处理
const hasProcessing = uploadedDocuments.some(doc =>
doc.stage === 'uploading' || doc.stage === 'indexing'
);
if (hasProcessing) {
message.warning('还有文档正在处理中,请等待完成');
return;
}
stopAllPolling();
setStep(1);
setUploadedDocuments([]);
setCurrentDocIndex(0);
setCurrentSettings(DEFAULT_SEGMENTATION_SETTINGS);
}, [uploadedDocuments, stopAllPolling]);
/**
* 返回文档列表
*/
const handleGoToDocuments = useCallback(() => {
stopAllPolling();
const hasCompleted = uploadedDocuments.some(doc => doc.stage === 'completed');
if (hasCompleted) {
onSuccess();
}
onClose();
}, [uploadedDocuments, stopAllPolling, onSuccess, onClose]);
/**
* 获取当前文档
*/
const getCurrentDocument = useCallback((): UploadedDocument | null => {
return uploadedDocuments[currentDocIndex] || null;
}, [uploadedDocuments, currentDocIndex]);
/**
* 获取当前文档的进度
*/
const getCurrentProgress = useCallback(() => {
const doc = getCurrentDocument();
if (!doc) return 0;
if (doc.stage === 'uploading') {
return doc.uploadProgress;
}
if (doc.stage === 'indexing' || doc.stage === 'completed') {
return INDEXING_STATUS_CONFIG[doc.indexingStatus]?.percent || 0;
}
return 0;
}, [getCurrentDocument]);
/**
* 获取当前文档的状态文本
*/
const getStatusText = useCallback(() => {
const doc = getCurrentDocument();
if (!doc) return '';
if (doc.stage === 'uploading') {
return `正在上传... ${doc.uploadProgress}%`;
}
if (doc.stage === 'indexing') {
const baseText = INDEXING_STATUS_CONFIG[doc.indexingStatus]?.text || '处理中...';
// 如果有分段信息,且处于分段或索引阶段,显示进度
if ((doc.indexingStatus === 'splitting' || doc.indexingStatus === 'indexing') &&
doc.totalSegments && doc.totalSegments > 0) {
return `${baseText} (${doc.completedSegments || 0}/${doc.totalSegments})`;
}
return baseText;
}
if (doc.stage === 'completed') {
return `处理完成 (${doc.totalSegments || doc.segments?.length || 0} 段)`;
}
if (doc.stage === 'error') {
return doc.error || '处理失败';
}
return '';
}, [getCurrentDocument]);
/**
* 判断当前文档是否正在处理
*/
const isCurrentDocProcessing = useCallback(() => {
const doc = getCurrentDocument();
return doc?.stage === 'uploading' || doc?.stage === 'indexing';
}, [getCurrentDocument]);
/**
* 获取所有文档的完成状态统计
*/
const getCompletionStats = useCallback(() => {
const completed = uploadedDocuments.filter(doc => doc.stage === 'completed').length;
const total = uploadedDocuments.length;
return { completed, total };
}, [uploadedDocuments]);
return {
// 状态
step,
selectedFiles,
fileList,
uploadedDocuments,
currentDocIndex,
currentSettings,
previewLoading,
// 方法
handleFileChange,
handleRemoveFile,
handleNextStep,
handleDocumentChange,
handleReprocess,
handlePrevStep,
handleGoToDocuments,
updateCurrentSettings,
// 计算属性方法
getCurrentDocument,
getCurrentProgress,
getStatusText,
isCurrentDocProcessing,
getCompletionStats,
};
}
export type UseDocumentUploadReturn = ReturnType<typeof useDocumentUpload>;
+202
View File
@@ -0,0 +1,202 @@
import { useState, useEffect, useCallback } from 'react';
import { message } from 'antd';
import type { Dataset } from '~/api/dify-dataset/type/datasetTypes';
import type { Document } from '~/api/dify-dataset/type/documentTypes';
import { fetchDatasets } from '~/api/dify-dataset/api/datasetApi';
import { fetchDocuments } from '~/api/dify-dataset/api/documentApi';
import type { MenuTab } from '~/types/dify-dataset-manager/layout';
import { DEFAULT_DOCUMENT_PAGE_SIZE } from '~/types/dify-dataset-manager/index';
/**
* 知识库管理器状态管理 Hook
*/
export function useDatasetManager() {
// 知识库状态
const [dataset, setDataset] = useState<Dataset | null>(null);
const [loadingDataset, setLoadingDataset] = useState(true);
// 文档状态
const [documents, setDocuments] = useState<Document[]>([]);
const [loadingDocuments, setLoadingDocuments] = useState(false);
const [documentTotal, setDocumentTotal] = useState(0);
const [documentPage, setDocumentPage] = useState(1);
const [documentPageSize] = useState(DEFAULT_DOCUMENT_PAGE_SIZE);
// 初始化状态
const [inited, setInited] = useState(false);
const [error, setError] = useState<string | null>(null);
// 菜单状态
const [activeTab, setActiveTab] = useState<MenuTab>('documents');
// 选中的文档(用于查看文档详情)
const [selectedDocument, setSelectedDocument] = useState<Document | null>(null);
/**
* 加载文档列表
*/
const loadDocuments = useCallback(async (datasetId: string, page: number = 1) => {
if (!datasetId) return;
setLoadingDocuments(true);
try {
console.log('[DatasetManager] 加载文档列表:', { datasetId, page });
const response = await fetchDocuments(datasetId, page, documentPageSize);
console.log('[DatasetManager] 文档列表响应:', response);
if (response && response.data) {
setDocuments(response.data);
setDocumentTotal(response.total);
setDocumentPage(page);
}
} catch (err: any) {
console.error('[DatasetManager] 加载文档列表失败:', err);
message.error('加载文档列表失败');
} finally {
setLoadingDocuments(false);
}
}, [documentPageSize]);
/**
* 加载知识库(获取第一个知识库)
*/
const loadDataset = useCallback(async () => {
setLoadingDataset(true);
try {
console.log('[DatasetManager] 加载知识库...');
const response = await fetchDatasets(1, 1);
console.log('[DatasetManager] 知识库响应:', response);
if (response && response.data && response.data.length > 0) {
const firstDataset = response.data[0];
setDataset(firstDataset);
// 立即加载文档
await loadDocuments(firstDataset.id, 1);
} else {
setError('未找到知识库,请先在Dify中创建知识库');
}
} catch (err: any) {
console.error('[DatasetManager] 加载知识库失败:', err);
setError(err.message || '加载知识库失败');
message.error('加载知识库失败');
} finally {
setLoadingDataset(false);
setInited(true);
}
}, [loadDocuments]);
/**
* 处理文档页码变化
*/
const handlePageChange = useCallback((page: number) => {
if (dataset) {
loadDocuments(dataset.id, page);
}
}, [dataset, loadDocuments]);
/**
* 处理文档删除
*/
const handleDocumentDeleted = useCallback((documentId: string) => {
setDocuments((prev) => prev.filter((doc) => doc.id !== documentId));
setDocumentTotal((prev) => prev - 1);
// 更新知识库的文档数量
setDataset((prev) => {
if (prev) {
return {
...prev,
document_count: prev.document_count - 1
};
}
return prev;
});
}, []);
/**
* 处理文档状态变化
*/
const handleDocumentStatusChanged = useCallback((documentId: string, enabled: boolean) => {
setDocuments((prev) =>
prev.map((doc) =>
doc.id === documentId ? { ...doc, enabled } : doc
)
);
}, []);
/**
* 刷新文档列表
*/
const handleRefresh = useCallback(() => {
if (dataset) {
loadDocuments(dataset.id, documentPage);
}
}, [dataset, documentPage, loadDocuments]);
/**
* 查看文档详情(分段管理)
*/
const handleViewDocument = useCallback((doc: Document) => {
console.log('[DatasetManager] 查看文档详情:', doc);
setSelectedDocument(doc);
}, []);
/**
* 返回文档列表
*/
const handleBackToDocuments = useCallback(() => {
setSelectedDocument(null);
}, []);
/**
* 处理菜单切换
*/
const handleTabChange = useCallback((tab: MenuTab) => {
setActiveTab(tab);
// 切换菜单时清除选中的文档
if (tab !== 'documents') {
setSelectedDocument(null);
}
}, []);
/**
* 处理知识库更新
*/
const handleDatasetUpdated = useCallback((updatedDataset: Dataset) => {
setDataset(updatedDataset);
}, []);
// 初始化
useEffect(() => {
loadDataset();
}, [loadDataset]);
return {
// 状态
dataset,
loadingDataset,
documents,
loadingDocuments,
documentTotal,
documentPage,
documentPageSize,
inited,
error,
activeTab,
selectedDocument,
// 方法
loadDataset,
loadDocuments,
handlePageChange,
handleDocumentDeleted,
handleDocumentStatusChanged,
handleRefresh,
handleViewDocument,
handleBackToDocuments,
handleTabChange,
handleDatasetUpdated,
};
}
export type UseDatasetManagerReturn = ReturnType<typeof useDatasetManager>;
@@ -0,0 +1,90 @@
import { useState, useCallback } from 'react';
import { message } from 'antd';
import type { RetrieveRecord, RetrievalModel } from '~/api/dify-dataset/type';
import { retrieveDataset } from '~/api/dify-dataset/api/segmentApi';
import { DIFY_CONFIG } from '~/config/api-config';
import type { SearchMethod } from '~/types/dify-dataset-manager/retrieve-test';
/**
* 构建完整的 retrieval_model 参数(匹配 Dify API 规范)
* 根据检索方式启用 Reranking(语义搜索和混合搜索需要启用)
*/
function buildRetrievalModel(searchMethod: SearchMethod, topK: number): RetrievalModel {
// 语义搜索和混合搜索需要启用 Reranking
const needReranking = searchMethod === 'semantic_search' || searchMethod === 'hybrid_search';
return {
search_method: searchMethod,
reranking_enable: needReranking,
reranking_mode: needReranking ? null : null,
reranking_model: {
reranking_provider_name: DIFY_CONFIG.rerankingProviderName,
reranking_model_name: DIFY_CONFIG.rerankingModelName,
},
weights: null,
top_k: topK,
score_threshold_enabled: false,
score_threshold: null,
};
}
/**
* 召回测试状态管理 Hook
*/
export function useRetrieveTest(datasetId: string) {
const [searchQuery, setSearchQuery] = useState('');
const [retrieveResults, setRetrieveResults] = useState<RetrieveRecord[]>([]);
const [retrieving, setRetrieving] = useState(false);
// 默认使用语义搜索
const [searchMethod, setSearchMethod] = useState<SearchMethod>('semantic_search');
const [topK, setTopK] = useState<number>(5);
/**
* 执行检索
*/
const handleRetrieve = useCallback(async () => {
if (!searchQuery.trim()) {
message.warning('请输入检索关键词');
return;
}
if (!datasetId) {
message.warning('知识库ID不存在');
return;
}
setRetrieving(true);
try {
const retrievalModel = buildRetrievalModel(searchMethod, topK);
console.log('[Hook] 检索参数:', { datasetId, query: searchQuery, retrievalModel });
const response = await retrieveDataset(datasetId, searchQuery, retrievalModel);
setRetrieveResults(response.records || []);
if (response.records?.length === 0) {
message.info('未找到匹配的结果');
}
} catch (err: any) {
console.error('检索失败:', err);
message.error(err.message || '检索失败');
} finally {
setRetrieving(false);
}
}, [datasetId, searchQuery, searchMethod, topK]);
return {
// 状态
searchQuery,
setSearchQuery,
retrieveResults,
retrieving,
searchMethod,
setSearchMethod,
topK,
setTopK,
// 方法
handleRetrieve,
};
}
export type UseRetrieveTestReturn = ReturnType<typeof useRetrieveTest>;