Merge branch 'PingChuan' into shiy-login
This commit is contained in:
@@ -225,6 +225,44 @@ export async function fetchUploadFileInfo(
|
|||||||
return response.data;
|
return response.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 下载文档原始文件
|
||||||
|
* 通过代理路由下载 Dify 知识库中的原始文件
|
||||||
|
*
|
||||||
|
* @param uploadFileInfo - 上传文件信息(从 fetchUploadFileInfo 获取)
|
||||||
|
* @returns File 对象
|
||||||
|
*/
|
||||||
|
export async function downloadOriginalFile(
|
||||||
|
uploadFileInfo: UploadFileInfo
|
||||||
|
): Promise<File> {
|
||||||
|
if (!uploadFileInfo.download_url) {
|
||||||
|
throw new Error('无法获取原始文件下载地址');
|
||||||
|
}
|
||||||
|
|
||||||
|
// download_url 格式: /files/xxx/file-preview?...
|
||||||
|
// 转换为代理路由: /api/dataset/dify-files/xxx/file-preview?...
|
||||||
|
const downloadPath = uploadFileInfo.download_url.replace(/^\/files\//, '');
|
||||||
|
const proxyUrl = `${API_URL}/dify-files/${downloadPath}`;
|
||||||
|
|
||||||
|
console.log('[Dataset Client] 下载原始文件:', {
|
||||||
|
originalUrl: uploadFileInfo.download_url,
|
||||||
|
proxyUrl,
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = await axios.get(proxyUrl, {
|
||||||
|
responseType: 'blob',
|
||||||
|
withCredentials: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const file = new File(
|
||||||
|
[response.data],
|
||||||
|
uploadFileInfo.name || 'document',
|
||||||
|
{ type: uploadFileInfo.mime_type || 'application/octet-stream' }
|
||||||
|
);
|
||||||
|
|
||||||
|
return file;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 预处理规则 ID
|
* 预处理规则 ID
|
||||||
*/
|
*/
|
||||||
@@ -244,6 +282,7 @@ export interface PreProcessingRule {
|
|||||||
export interface SegmentationConfig {
|
export interface SegmentationConfig {
|
||||||
separator: string;
|
separator: string;
|
||||||
max_tokens: number;
|
max_tokens: number;
|
||||||
|
chunk_overlap?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -3,12 +3,12 @@ import {
|
|||||||
Button,
|
Button,
|
||||||
InputNumber,
|
InputNumber,
|
||||||
Checkbox,
|
Checkbox,
|
||||||
Select,
|
|
||||||
Card,
|
Card,
|
||||||
Empty,
|
Empty,
|
||||||
Spin,
|
Spin,
|
||||||
Divider,
|
Divider,
|
||||||
Tooltip,
|
Tooltip,
|
||||||
|
Progress,
|
||||||
} from 'antd';
|
} from 'antd';
|
||||||
import {
|
import {
|
||||||
QuestionCircleOutlined,
|
QuestionCircleOutlined,
|
||||||
@@ -17,6 +17,7 @@ import {
|
|||||||
} from '@ant-design/icons';
|
} from '@ant-design/icons';
|
||||||
import { useDocumentDetail } from '~/hooks/dify-dataset-manager/document-detail';
|
import { useDocumentDetail } from '~/hooks/dify-dataset-manager/document-detail';
|
||||||
import type { DocumentDetailProps } from '~/types/dify-dataset-manager/document-detail';
|
import type { DocumentDetailProps } from '~/types/dify-dataset-manager/document-detail';
|
||||||
|
import { INDEXING_STATUS_CONFIG } from '~/types/dify-dataset-manager/document-detail';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 文档详情组件
|
* 文档详情组件
|
||||||
@@ -32,6 +33,8 @@ export default function DocumentDetail({
|
|||||||
previewLoading,
|
previewLoading,
|
||||||
showPreview,
|
showPreview,
|
||||||
saving,
|
saving,
|
||||||
|
isProcessing,
|
||||||
|
indexingStatus,
|
||||||
updateSettings,
|
updateSettings,
|
||||||
handleReset,
|
handleReset,
|
||||||
handlePreview,
|
handlePreview,
|
||||||
@@ -56,7 +59,7 @@ export default function DocumentDetail({
|
|||||||
<h3 className="section-title">分段设置</h3>
|
<h3 className="section-title">分段设置</h3>
|
||||||
|
|
||||||
{/* 分块模式 */}
|
{/* 分块模式 */}
|
||||||
<div className="setting-item mode-selector">
|
{/* <div className="setting-item mode-selector">
|
||||||
<div className="mode-option active">
|
<div className="mode-option active">
|
||||||
<div className="mode-icon">
|
<div className="mode-icon">
|
||||||
<i className="ri-text-spacing"></i>
|
<i className="ri-text-spacing"></i>
|
||||||
@@ -66,13 +69,13 @@ export default function DocumentDetail({
|
|||||||
<span className="mode-desc">通用文本分块模式,检索和召回的块是相同的</span>
|
<span className="mode-desc">通用文本分块模式,检索和召回的块是相同的</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div> */}
|
||||||
|
|
||||||
{/* 分段标识符 */}
|
{/* 分段标识符 */}
|
||||||
<div className="setting-item">
|
<div className="setting-item">
|
||||||
<label className="setting-label">
|
<label className="setting-label">
|
||||||
分段标识符
|
分段标识符
|
||||||
<Tooltip title="系统会在遇到指定分隔符时自动分段,默认值为 \n\n(按段落分段)">
|
<Tooltip title="分隔符是用于分隔文本的字符。\n\n和 \n 是常用于分隔段落和行的分隔符。用逗号连接分隔符(\n\n,\n)当段落超过最大块长度时,会按行进行分割。你也可以使用自定义的特殊分隔符(例如 ***)">
|
||||||
<QuestionCircleOutlined className="help-icon" />
|
<QuestionCircleOutlined className="help-icon" />
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</label>
|
</label>
|
||||||
@@ -80,6 +83,7 @@ export default function DocumentDetail({
|
|||||||
value={settings.separator}
|
value={settings.separator}
|
||||||
onChange={(e) => updateSettings('separator', e.target.value)}
|
onChange={(e) => updateSettings('separator', e.target.value)}
|
||||||
placeholder="\n\n"
|
placeholder="\n\n"
|
||||||
|
disabled={isProcessing}
|
||||||
className="setting-input"
|
className="setting-input"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -98,6 +102,28 @@ export default function DocumentDetail({
|
|||||||
onChange={(value) => updateSettings('maxTokens', value || 500)}
|
onChange={(value) => updateSettings('maxTokens', value || 500)}
|
||||||
min={100}
|
min={100}
|
||||||
max={4000}
|
max={4000}
|
||||||
|
disabled={isProcessing}
|
||||||
|
className="setting-input-number"
|
||||||
|
/>
|
||||||
|
<span className="input-suffix">characters</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 分段重叠长度 */}
|
||||||
|
<div className="setting-item">
|
||||||
|
<label className="setting-label">
|
||||||
|
分段重叠长度
|
||||||
|
<Tooltip title="设置分段之间的重叠长度可以保留分段之间的语义关系,提升召回效果建议设置为最大分段长度的10%-25%">
|
||||||
|
<QuestionCircleOutlined className="help-icon" />
|
||||||
|
</Tooltip>
|
||||||
|
</label>
|
||||||
|
<div className="setting-input-with-suffix">
|
||||||
|
<InputNumber
|
||||||
|
value={settings.chunkOverlap}
|
||||||
|
onChange={(value) => updateSettings('chunkOverlap', value || 50)}
|
||||||
|
min={0}
|
||||||
|
max={500}
|
||||||
|
disabled={isProcessing}
|
||||||
className="setting-input-number"
|
className="setting-input-number"
|
||||||
/>
|
/>
|
||||||
<span className="input-suffix">characters</span>
|
<span className="input-suffix">characters</span>
|
||||||
@@ -115,6 +141,7 @@ export default function DocumentDetail({
|
|||||||
<Checkbox
|
<Checkbox
|
||||||
checked={settings.removeExtraSpaces}
|
checked={settings.removeExtraSpaces}
|
||||||
onChange={(e) => updateSettings('removeExtraSpaces', e.target.checked)}
|
onChange={(e) => updateSettings('removeExtraSpaces', e.target.checked)}
|
||||||
|
disabled={isProcessing}
|
||||||
>
|
>
|
||||||
替换掉连续的空格、换行符和制表符
|
替换掉连续的空格、换行符和制表符
|
||||||
</Checkbox>
|
</Checkbox>
|
||||||
@@ -122,6 +149,7 @@ export default function DocumentDetail({
|
|||||||
<Checkbox
|
<Checkbox
|
||||||
checked={settings.removeUrlsEmails}
|
checked={settings.removeUrlsEmails}
|
||||||
onChange={(e) => updateSettings('removeUrlsEmails', e.target.checked)}
|
onChange={(e) => updateSettings('removeUrlsEmails', e.target.checked)}
|
||||||
|
disabled={isProcessing}
|
||||||
>
|
>
|
||||||
删除所有 URL 和电子邮件地址
|
删除所有 URL 和电子邮件地址
|
||||||
</Checkbox>
|
</Checkbox>
|
||||||
@@ -130,27 +158,25 @@ export default function DocumentDetail({
|
|||||||
|
|
||||||
<Divider />
|
<Divider />
|
||||||
|
|
||||||
{/* Q&A 分段 */}
|
{/* 索引方式 */}
|
||||||
<div className="settings-section">
|
<div className="settings-section">
|
||||||
<div className="qa-segment-row">
|
<h3 className="section-title">索引方式</h3>
|
||||||
<Checkbox
|
<div className="index-options">
|
||||||
checked={settings.useQASegment}
|
<div
|
||||||
onChange={(e) => updateSettings('useQASegment', e.target.checked)}
|
className={`index-option ${settings.indexingTechnique === 'high_quality' ? 'active' : ''} ${isProcessing ? 'disabled' : ''}`}
|
||||||
|
onClick={() => !isProcessing && updateSettings('indexingTechnique', 'high_quality')}
|
||||||
>
|
>
|
||||||
使用 Q&A 分段,语言
|
<span className="option-radio"></span>
|
||||||
</Checkbox>
|
<span className="option-label">高质量</span>
|
||||||
<Select
|
<span className="option-badge recommended">推荐</span>
|
||||||
value={settings.qaLanguage}
|
</div>
|
||||||
onChange={(value) => updateSettings('qaLanguage', value)}
|
<div
|
||||||
disabled={!settings.useQASegment}
|
className={`index-option ${settings.indexingTechnique === 'economy' ? 'active' : ''} ${isProcessing ? 'disabled' : ''}`}
|
||||||
style={{ width: 120 }}
|
onClick={() => !isProcessing && updateSettings('indexingTechnique', 'economy')}
|
||||||
options={[
|
>
|
||||||
{ value: 'Chinese', label: 'Chinese' },
|
<span className="option-radio"></span>
|
||||||
{ value: 'English', label: 'English' },
|
<span className="option-label">经济</span>
|
||||||
{ value: 'Japanese', label: 'Japanese' },
|
</div>
|
||||||
{ value: 'Korean', label: 'Korean' },
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -160,12 +186,14 @@ export default function DocumentDetail({
|
|||||||
icon={<EyeOutlined />}
|
icon={<EyeOutlined />}
|
||||||
onClick={handlePreview}
|
onClick={handlePreview}
|
||||||
loading={previewLoading}
|
loading={previewLoading}
|
||||||
|
disabled={isProcessing}
|
||||||
>
|
>
|
||||||
预览块
|
预览块
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
icon={<ReloadOutlined />}
|
icon={<ReloadOutlined />}
|
||||||
onClick={handleReset}
|
onClick={handleReset}
|
||||||
|
disabled={isProcessing}
|
||||||
>
|
>
|
||||||
重置
|
重置
|
||||||
</Button>
|
</Button>
|
||||||
@@ -179,12 +207,10 @@ export default function DocumentDetail({
|
|||||||
type="primary"
|
type="primary"
|
||||||
onClick={handleSaveAndProcess}
|
onClick={handleSaveAndProcess}
|
||||||
loading={saving}
|
loading={saving}
|
||||||
|
disabled={isProcessing}
|
||||||
block
|
block
|
||||||
>
|
>
|
||||||
保存并处理
|
{isProcessing ? '处理中...' : '保存并处理'}
|
||||||
</Button>
|
|
||||||
<Button block style={{ marginTop: 8 }}>
|
|
||||||
取消
|
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -195,12 +221,7 @@ export default function DocumentDetail({
|
|||||||
title={
|
title={
|
||||||
<div className="preview-header">
|
<div className="preview-header">
|
||||||
<span>预览</span>
|
<span>预览</span>
|
||||||
<Select
|
<div>{document.name}</div>
|
||||||
value={document.name}
|
|
||||||
style={{ width: 200 }}
|
|
||||||
disabled
|
|
||||||
options={[{ value: document.name, label: document.name }]}
|
|
||||||
/>
|
|
||||||
<span className="segment-count">
|
<span className="segment-count">
|
||||||
{showPreview ? `${previewSegments.length} 段块` : '0 段块'}
|
{showPreview ? `${previewSegments.length} 段块` : '0 段块'}
|
||||||
</span>
|
</span>
|
||||||
@@ -208,36 +229,59 @@ export default function DocumentDetail({
|
|||||||
}
|
}
|
||||||
className="preview-card"
|
className="preview-card"
|
||||||
>
|
>
|
||||||
{previewLoading ? (
|
{/* 处理进度显示 */}
|
||||||
<div className="preview-loading">
|
{isProcessing && indexingStatus && (
|
||||||
<Spin size="large" />
|
<div className="processing-status">
|
||||||
<div className="loading-text">加载中...</div>
|
<div className="processing-title">
|
||||||
</div>
|
<Spin size="small" />
|
||||||
) : !showPreview ? (
|
<span>正在处理文档...</span>
|
||||||
<div className="preview-empty">
|
</div>
|
||||||
<div className="empty-icon">
|
<Progress
|
||||||
<EyeOutlined />
|
percent={INDEXING_STATUS_CONFIG[indexingStatus]?.percent || 0}
|
||||||
|
status="active"
|
||||||
|
strokeColor={{ '0%': '#00684a', '100%': '#52c41a' }}
|
||||||
|
/>
|
||||||
|
<div className="status-text">
|
||||||
|
{INDEXING_STATUS_CONFIG[indexingStatus]?.text || '处理中...'}
|
||||||
</div>
|
</div>
|
||||||
<p>点击左侧的"预览块"按钮来预览</p>
|
|
||||||
</div>
|
</div>
|
||||||
) : previewSegments.length === 0 ? (
|
)}
|
||||||
<Empty description="暂无分段数据" />
|
|
||||||
) : (
|
{/* 预览内容 */}
|
||||||
<div className="preview-segments">
|
{!isProcessing && (
|
||||||
{previewSegments.map((segment, index) => (
|
<>
|
||||||
<div key={segment.id} className="segment-item">
|
{previewLoading ? (
|
||||||
<div className="segment-header">
|
<div className="preview-loading">
|
||||||
<span className="segment-index">#{index + 1}</span>
|
<Spin size="large" />
|
||||||
<span className="segment-chars">
|
<div className="loading-text">加载中...</div>
|
||||||
{segment.word_count} 字符
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="segment-content">
|
|
||||||
{segment.content}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
))}
|
) : !showPreview ? (
|
||||||
</div>
|
<div className="preview-empty">
|
||||||
|
<div className="empty-icon">
|
||||||
|
<EyeOutlined />
|
||||||
|
</div>
|
||||||
|
<p>点击左侧的"预览块"按钮来预览</p>
|
||||||
|
</div>
|
||||||
|
) : previewSegments.length === 0 ? (
|
||||||
|
<Empty description="暂无分段数据" />
|
||||||
|
) : (
|
||||||
|
<div className="preview-segments">
|
||||||
|
{previewSegments.map((segment, index) => (
|
||||||
|
<div key={segment.id} className="segment-item">
|
||||||
|
<div className="segment-header">
|
||||||
|
<span className="segment-index">#{index + 1}</span>
|
||||||
|
<span className="segment-chars">
|
||||||
|
{segment.word_count} 字符
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="segment-content">
|
||||||
|
{segment.content}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,29 +1,29 @@
|
|||||||
import {
|
import {
|
||||||
Button,
|
CloudUploadOutlined,
|
||||||
Input,
|
|
||||||
Table,
|
|
||||||
Tag,
|
|
||||||
Space,
|
|
||||||
Tooltip,
|
|
||||||
Popconfirm,
|
|
||||||
Switch,
|
|
||||||
Empty,
|
|
||||||
Spin,
|
|
||||||
} from 'antd';
|
|
||||||
import {
|
|
||||||
SearchOutlined,
|
|
||||||
ReloadOutlined,
|
|
||||||
DeleteOutlined,
|
DeleteOutlined,
|
||||||
FileTextOutlined,
|
FileTextOutlined,
|
||||||
CloudUploadOutlined,
|
ReloadOutlined,
|
||||||
EyeOutlined,
|
SearchOutlined,
|
||||||
|
UnorderedListOutlined
|
||||||
} from '@ant-design/icons';
|
} from '@ant-design/icons';
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Empty,
|
||||||
|
Input,
|
||||||
|
Popconfirm,
|
||||||
|
Space,
|
||||||
|
Spin,
|
||||||
|
Switch,
|
||||||
|
Table,
|
||||||
|
Tag,
|
||||||
|
Tooltip,
|
||||||
|
} from 'antd';
|
||||||
import type { ColumnsType } from 'antd/es/table';
|
import type { ColumnsType } from 'antd/es/table';
|
||||||
import type { Document, IndexingStatus } from '~/api/dify-dataset/type/documentTypes';
|
import type { Document, IndexingStatus } from '~/api/dify-dataset/type/documentTypes';
|
||||||
import { useDocumentList } from '~/hooks/dify-dataset-manager/document-list';
|
import { useDocumentList } from '~/hooks/dify-dataset-manager/document-list';
|
||||||
import type { DocumentListProps } from '~/types/dify-dataset-manager/document-list';
|
import type { DocumentListProps } from '~/types/dify-dataset-manager/document-list';
|
||||||
import DocumentUpload from './document-upload';
|
|
||||||
import '../../styles/components/dify-dataset-manager/index.css';
|
import '../../styles/components/dify-dataset-manager/index.css';
|
||||||
|
import DocumentUpload from './document-upload';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 文档列表组件
|
* 文档列表组件
|
||||||
@@ -128,11 +128,11 @@ export default function DocumentList({
|
|||||||
width: 120,
|
width: 120,
|
||||||
render: (_, record) => (
|
render: (_, record) => (
|
||||||
<Space size="small">
|
<Space size="small">
|
||||||
<Tooltip title="查看分段">
|
<Tooltip title="分段设置">
|
||||||
<Button
|
<Button
|
||||||
type="text"
|
type="text"
|
||||||
size="small"
|
size="small"
|
||||||
icon={<EyeOutlined />}
|
icon={<UnorderedListOutlined />}
|
||||||
onClick={() => onViewDocument?.(record)}
|
onClick={() => onViewDocument?.(record)}
|
||||||
/>
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|||||||
@@ -237,7 +237,7 @@ export default function DocumentUpload({
|
|||||||
<div className="setting-item">
|
<div className="setting-item">
|
||||||
<label className="setting-label">
|
<label className="setting-label">
|
||||||
分段重叠长度
|
分段重叠长度
|
||||||
<Tooltip title="相邻分段之间重叠的字符数,有助于保持上下文连贯性">
|
<Tooltip title="设置分段之间的重叠长度可以保留分段之间的语义关系,提升召回效果建议设置为最大分段长度的10%-25%">
|
||||||
<QuestionCircleOutlined className="help-icon" />
|
<QuestionCircleOutlined className="help-icon" />
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</label>
|
</label>
|
||||||
|
|||||||
@@ -1,8 +1,13 @@
|
|||||||
import { message } from 'antd';
|
import { message } from 'antd';
|
||||||
import { useCallback, useEffect, useState } from 'react';
|
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||||
import { updateDocumentWithSettings } from '~/api/dify-dataset/api/documentApi';
|
import {
|
||||||
|
fetchUploadFileInfo,
|
||||||
|
downloadOriginalFile,
|
||||||
|
updateDocumentByFile,
|
||||||
|
fetchIndexingStatus,
|
||||||
|
} from '~/api/dify-dataset/api/documentApi';
|
||||||
import { fetchSegments } from '~/api/dify-dataset/api/segmentApi';
|
import { fetchSegments } from '~/api/dify-dataset/api/segmentApi';
|
||||||
import type { Segment } from '~/api/dify-dataset/type';
|
import type { Segment, IndexingStatus } from '~/api/dify-dataset/type';
|
||||||
import type { Document } from '~/api/dify-dataset/type/documentTypes';
|
import type { Document } from '~/api/dify-dataset/type/documentTypes';
|
||||||
import type { DocumentDetailSegmentationSettings } from '~/types/dify-dataset-manager/document-detail';
|
import type { DocumentDetailSegmentationSettings } from '~/types/dify-dataset-manager/document-detail';
|
||||||
import { DEFAULT_DOCUMENT_DETAIL_SETTINGS } from '~/types/dify-dataset-manager/document-detail';
|
import { DEFAULT_DOCUMENT_DETAIL_SETTINGS } from '~/types/dify-dataset-manager/document-detail';
|
||||||
@@ -22,6 +27,73 @@ export function useDocumentDetail(datasetId: string, document: Document | null)
|
|||||||
// 保存状态
|
// 保存状态
|
||||||
const [saving, setSaving] = useState(false);
|
const [saving, setSaving] = useState(false);
|
||||||
|
|
||||||
|
// 处理状态(嵌入进度)
|
||||||
|
const [isProcessing, setIsProcessing] = useState(false);
|
||||||
|
const [indexingStatus, setIndexingStatus] = useState<IndexingStatus | null>(null);
|
||||||
|
const pollingTimerRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 停止轮询
|
||||||
|
*/
|
||||||
|
const stopPolling = useCallback(() => {
|
||||||
|
if (pollingTimerRef.current) {
|
||||||
|
clearInterval(pollingTimerRef.current);
|
||||||
|
pollingTimerRef.current = null;
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 轮询索引状态
|
||||||
|
*/
|
||||||
|
const pollIndexingStatus = useCallback(async (batch: string) => {
|
||||||
|
try {
|
||||||
|
const response = await fetchIndexingStatus(datasetId, batch);
|
||||||
|
const status = response.data?.[0];
|
||||||
|
|
||||||
|
if (status) {
|
||||||
|
setIndexingStatus(status.indexing_status);
|
||||||
|
|
||||||
|
if (status.indexing_status === 'completed') {
|
||||||
|
// 停止轮询
|
||||||
|
stopPolling();
|
||||||
|
setIsProcessing(false);
|
||||||
|
message.success('文档处理完成');
|
||||||
|
|
||||||
|
// 刷新分段预览
|
||||||
|
setPreviewLoading(true);
|
||||||
|
try {
|
||||||
|
const segmentResponse = await fetchSegments(datasetId, document?.id || '', 1, 50);
|
||||||
|
setPreviewSegments(segmentResponse.data || []);
|
||||||
|
setShowPreview(true);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('刷新分段失败:', err);
|
||||||
|
} finally {
|
||||||
|
setPreviewLoading(false);
|
||||||
|
}
|
||||||
|
} else if (status.indexing_status === 'error') {
|
||||||
|
// 停止轮询
|
||||||
|
stopPolling();
|
||||||
|
setIsProcessing(false);
|
||||||
|
message.error(status.error || '处理失败');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('获取索引状态失败:', err);
|
||||||
|
}
|
||||||
|
}, [datasetId, document?.id, stopPolling]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 开始轮询
|
||||||
|
*/
|
||||||
|
const startPolling = useCallback((batch: string) => {
|
||||||
|
stopPolling();
|
||||||
|
pollingTimerRef.current = setInterval(() => {
|
||||||
|
pollIndexingStatus(batch);
|
||||||
|
}, 2000);
|
||||||
|
// 立即执行一次
|
||||||
|
pollIndexingStatus(batch);
|
||||||
|
}, [stopPolling, pollIndexingStatus]);
|
||||||
|
|
||||||
// 当文档变化时重置设置
|
// 当文档变化时重置设置
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (document) {
|
if (document) {
|
||||||
@@ -29,8 +101,18 @@ export function useDocumentDetail(datasetId: string, document: Document | null)
|
|||||||
setSettings(DEFAULT_DOCUMENT_DETAIL_SETTINGS);
|
setSettings(DEFAULT_DOCUMENT_DETAIL_SETTINGS);
|
||||||
setPreviewSegments([]);
|
setPreviewSegments([]);
|
||||||
setShowPreview(false);
|
setShowPreview(false);
|
||||||
|
setIsProcessing(false);
|
||||||
|
setIndexingStatus(null);
|
||||||
|
stopPolling();
|
||||||
}
|
}
|
||||||
}, [document?.id]);
|
}, [document?.id, stopPolling]);
|
||||||
|
|
||||||
|
// 组件卸载时清理定时器
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
stopPolling();
|
||||||
|
};
|
||||||
|
}, [stopPolling]);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 更新设置
|
* 更新设置
|
||||||
@@ -73,14 +155,27 @@ export function useDocumentDetail(datasetId: string, document: Document | null)
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 保存并处理
|
* 保存并处理
|
||||||
|
* 流程:获取原始文件 → 下载 → 用新参数重新上传 → 轮询嵌入状态
|
||||||
*/
|
*/
|
||||||
const handleSaveAndProcess = useCallback(async () => {
|
const handleSaveAndProcess = useCallback(async () => {
|
||||||
if (!document) return;
|
if (!document) return;
|
||||||
|
|
||||||
|
setIsProcessing(true);
|
||||||
setSaving(true);
|
setSaving(true);
|
||||||
|
setIndexingStatus('waiting');
|
||||||
try {
|
try {
|
||||||
await updateDocumentWithSettings(datasetId, document.id, {
|
// 1. 获取原始文件信息
|
||||||
indexing_technique: 'high_quality',
|
message.loading({ content: '正在获取原始文件信息...', key: 'save-process' });
|
||||||
|
const uploadFileInfo = await fetchUploadFileInfo(datasetId, document.id);
|
||||||
|
|
||||||
|
// 2. 下载原始文件(通过代理路由)
|
||||||
|
message.loading({ content: '正在下载原始文件...', key: 'save-process' });
|
||||||
|
const file = await downloadOriginalFile(uploadFileInfo);
|
||||||
|
|
||||||
|
// 3. 用新参数重新上传
|
||||||
|
message.loading({ content: '正在应用新配置并重新处理...', key: 'save-process' });
|
||||||
|
const result = await updateDocumentByFile(datasetId, document.id, file, {
|
||||||
|
indexing_technique: settings.indexingTechnique,
|
||||||
process_rule: {
|
process_rule: {
|
||||||
mode: 'custom',
|
mode: 'custom',
|
||||||
rules: {
|
rules: {
|
||||||
@@ -91,18 +186,25 @@ export function useDocumentDetail(datasetId: string, document: Document | null)
|
|||||||
segmentation: {
|
segmentation: {
|
||||||
separator: settings.separator.replace(/\\n/g, '\n'),
|
separator: settings.separator.replace(/\\n/g, '\n'),
|
||||||
max_tokens: settings.maxTokens,
|
max_tokens: settings.maxTokens,
|
||||||
|
chunk_overlap: settings.chunkOverlap,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
message.success('设置已保存,文档正在重新处理...');
|
|
||||||
|
message.success({ content: '文档正在处理中...', key: 'save-process' });
|
||||||
|
|
||||||
|
// 4. 开始轮询嵌入状态
|
||||||
|
startPolling(result.batch);
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
console.error('保存设置失败:', err);
|
console.error('保存设置失败:', err);
|
||||||
message.error(err.message || '保存失败');
|
message.error({ content: err.message || '保存失败', key: 'save-process' });
|
||||||
|
setIsProcessing(false);
|
||||||
|
setIndexingStatus(null);
|
||||||
} finally {
|
} finally {
|
||||||
setSaving(false);
|
setSaving(false);
|
||||||
}
|
}
|
||||||
}, [datasetId, document, settings]);
|
}, [datasetId, document, settings, startPolling]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
// 状态
|
// 状态
|
||||||
@@ -111,6 +213,8 @@ export function useDocumentDetail(datasetId: string, document: Document | null)
|
|||||||
previewLoading,
|
previewLoading,
|
||||||
showPreview,
|
showPreview,
|
||||||
saving,
|
saving,
|
||||||
|
isProcessing,
|
||||||
|
indexingStatus,
|
||||||
|
|
||||||
// 方法
|
// 方法
|
||||||
updateSettings,
|
updateSettings,
|
||||||
|
|||||||
@@ -0,0 +1,80 @@
|
|||||||
|
import { type LoaderFunctionArgs } from '@remix-run/node';
|
||||||
|
import { API_BASE_URL } from '~/config/api-config';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET /api/dataset/dify-files/*
|
||||||
|
* 代理 Dify 文件下载请求
|
||||||
|
*
|
||||||
|
* 用于下载 Dify 知识库中的原始文件
|
||||||
|
* 将 /api/dataset/dify-files/xxx 转发到 Dify 的 /files/xxx
|
||||||
|
*/
|
||||||
|
export async function loader({ request, params }: LoaderFunctionArgs) {
|
||||||
|
try {
|
||||||
|
const { getUserSession } = await import("~/api/login/auth.server");
|
||||||
|
const { frontendJWT } = await getUserSession(request);
|
||||||
|
|
||||||
|
if (!frontendJWT) {
|
||||||
|
return new Response(
|
||||||
|
JSON.stringify({ error: 'JWT认证失败,请重新登录' }),
|
||||||
|
{ status: 401, headers: { 'Content-Type': 'application/json' } }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取完整路径(包含查询参数)
|
||||||
|
const url = new URL(request.url);
|
||||||
|
const filePath = params['*'] || '';
|
||||||
|
const queryString = url.search;
|
||||||
|
|
||||||
|
console.log('[API] Dify File Proxy:', { filePath, queryString });
|
||||||
|
|
||||||
|
// 构建 Dify 文件下载 URL
|
||||||
|
// 使用专门的文件下载代理路由 /dify_file/
|
||||||
|
// 因为 Dify 文件 API (/files/) 不需要 /v1 前缀,与其他 API 不同
|
||||||
|
const difyFileUrl = `${API_BASE_URL}/dify_file/${filePath}${queryString}`;
|
||||||
|
|
||||||
|
console.log('[API] Proxying to:', difyFileUrl);
|
||||||
|
|
||||||
|
// 转发请求到 Dify
|
||||||
|
const response = await fetch(difyFileUrl, {
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer ${frontendJWT}`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
console.error('[API] Dify File Proxy - Error:', response.status, response.statusText);
|
||||||
|
return new Response(
|
||||||
|
JSON.stringify({ error: `文件下载失败: ${response.statusText}` }),
|
||||||
|
{ status: response.status, headers: { 'Content-Type': 'application/json' } }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取文件内容
|
||||||
|
const fileBuffer = await response.arrayBuffer();
|
||||||
|
|
||||||
|
// 返回文件,保持原始的 Content-Type
|
||||||
|
const contentType = response.headers.get('Content-Type') || 'application/octet-stream';
|
||||||
|
const contentDisposition = response.headers.get('Content-Disposition');
|
||||||
|
|
||||||
|
const headers: Record<string, string> = {
|
||||||
|
'Content-Type': contentType,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (contentDisposition) {
|
||||||
|
headers['Content-Disposition'] = contentDisposition;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Response(fileBuffer, {
|
||||||
|
status: 200,
|
||||||
|
headers,
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('[API] Dify File Proxy - Error:', error.message);
|
||||||
|
return new Response(
|
||||||
|
JSON.stringify({ error: error.message || 'Failed to download file' }),
|
||||||
|
{ status: 500, headers: { 'Content-Type': 'application/json' } }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1977,6 +1977,17 @@
|
|||||||
margin-bottom: 24px;
|
margin-bottom: 24px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.processing-status .processing-title {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 10px;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #1a1a1a;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
.processing-file {
|
.processing-file {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { Segment } from '~/api/dify-dataset/type';
|
import type { Segment, IndexingStatus } from '~/api/dify-dataset/type';
|
||||||
import type { Document } from '~/api/dify-dataset/type/documentTypes';
|
import type { Document } from '~/api/dify-dataset/type/documentTypes';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -11,20 +11,22 @@ export interface DocumentDetailProps {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 分段设置配置(文档详情专用)
|
* 分段设置配置(文档详情专用)
|
||||||
* 注意:Dify API 支持的参数有限
|
* 注意:update-by-file API 支持的参数
|
||||||
* - separator: ✅ 支持
|
* - separator: ✅ 支持
|
||||||
* - maxTokens: ✅ 支持
|
* - maxTokens: ✅ 支持
|
||||||
|
* - chunkOverlap: ✅ 支持(分段重叠长度)
|
||||||
* - removeExtraSpaces: ✅ 支持
|
* - removeExtraSpaces: ✅ 支持
|
||||||
* - removeUrlsEmails: ✅ 支持
|
* - removeUrlsEmails: ✅ 支持
|
||||||
* - useQASegment: ⚠️ 需要 doc_form: "qa_model"
|
* - indexingTechnique: ✅ 支持(high_quality/economy)
|
||||||
|
* - doc_form/doc_language: ❌ 不支持(仅 create-by-file 支持)
|
||||||
*/
|
*/
|
||||||
export interface DocumentDetailSegmentationSettings {
|
export interface DocumentDetailSegmentationSettings {
|
||||||
separator: string;
|
separator: string;
|
||||||
maxTokens: number;
|
maxTokens: number;
|
||||||
|
chunkOverlap: number;
|
||||||
removeExtraSpaces: boolean;
|
removeExtraSpaces: boolean;
|
||||||
removeUrlsEmails: boolean;
|
removeUrlsEmails: boolean;
|
||||||
useQASegment: boolean;
|
indexingTechnique: 'high_quality' | 'economy';
|
||||||
qaLanguage: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -33,10 +35,10 @@ export interface DocumentDetailSegmentationSettings {
|
|||||||
export const DEFAULT_DOCUMENT_DETAIL_SETTINGS: DocumentDetailSegmentationSettings = {
|
export const DEFAULT_DOCUMENT_DETAIL_SETTINGS: DocumentDetailSegmentationSettings = {
|
||||||
separator: '\\n\\n',
|
separator: '\\n\\n',
|
||||||
maxTokens: 500,
|
maxTokens: 500,
|
||||||
|
chunkOverlap: 50,
|
||||||
removeExtraSpaces: true,
|
removeExtraSpaces: true,
|
||||||
removeUrlsEmails: false,
|
removeUrlsEmails: false,
|
||||||
useQASegment: false,
|
indexingTechnique: 'high_quality',
|
||||||
qaLanguage: 'Chinese',
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -48,4 +50,20 @@ export interface DocumentDetailState {
|
|||||||
previewLoading: boolean;
|
previewLoading: boolean;
|
||||||
showPreview: boolean;
|
showPreview: boolean;
|
||||||
saving: boolean;
|
saving: boolean;
|
||||||
|
isProcessing: boolean;
|
||||||
|
indexingStatus: IndexingStatus | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 索引状态配置
|
||||||
|
*/
|
||||||
|
export const INDEXING_STATUS_CONFIG: Record<IndexingStatus, { text: string; percent: number }> = {
|
||||||
|
waiting: { text: '等待处理...', percent: 10 },
|
||||||
|
parsing: { text: '解析文档...', percent: 30 },
|
||||||
|
cleaning: { text: '清洗文本...', percent: 50 },
|
||||||
|
splitting: { text: '分段处理...', percent: 70 },
|
||||||
|
indexing: { text: '建立索引...', percent: 85 },
|
||||||
|
completed: { text: '处理完成', percent: 100 },
|
||||||
|
paused: { text: '已暂停', percent: 0 },
|
||||||
|
error: { text: '处理失败', percent: 0 },
|
||||||
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user