fix(admin): Fix Prompt management list not showing version info and add debug diagnostics
Summary: - Fix Prompt list API response schema missing activeVersion and draftVersion fields - Fastify was filtering out undefined schema fields, causing version columns to show empty - Add detailed diagnostic logging for Prompt debug mode troubleshooting - Verify debug mode works correctly (DRAFT version is used when debug enabled) Changes: - backend/src/common/prompt/prompt.routes.ts: Add activeVersion and draftVersion to response schema - backend/src/common/prompt/prompt.service.ts: Add diagnostic logs for setDebugMode and get methods - PKB module: Various authentication and document handling fixes from previous session Tested: Debug mode verified working - v2 DRAFT version correctly loaded when debug enabled
This commit is contained in:
@@ -561,6 +561,8 @@ export default FulltextDetailDrawer;
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -154,6 +154,8 @@ export const useAssets = (activeTab: AssetTabType) => {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -144,6 +144,8 @@ export const useRecentTasks = () => {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -343,6 +343,8 @@ export default DropnaDialog;
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -428,6 +428,8 @@ export default MetricTimePanel;
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -314,6 +314,8 @@ export default PivotPanel;
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -114,6 +114,8 @@ export function useSessionStatus({
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -106,6 +106,8 @@ export interface DataStats {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -102,6 +102,8 @@ export type AssetTabType = 'all' | 'processed' | 'raw';
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -176,6 +176,7 @@ export const documentApi = {
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data',
|
||||
},
|
||||
timeout: 120000, // 上传超时设为2分钟
|
||||
onUploadProgress: (progressEvent) => {
|
||||
if (progressEvent.total && onProgress) {
|
||||
const percentCompleted = Math.round((progressEvent.loaded * 100) / progressEvent.total);
|
||||
@@ -183,7 +184,19 @@ export const documentApi = {
|
||||
}
|
||||
},
|
||||
});
|
||||
return response.data.data;
|
||||
|
||||
// 🔑 改进响应处理
|
||||
if (response.data?.success && response.data?.data) {
|
||||
return response.data.data;
|
||||
}
|
||||
|
||||
// 兼容直接返回数据的情况
|
||||
if (response.data?.id) {
|
||||
return response.data;
|
||||
}
|
||||
|
||||
console.warn('[documentApi.upload] 响应格式异常:', response.data);
|
||||
throw new Error('上传响应格式异常');
|
||||
},
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Upload, message, Progress, Card } from 'antd';
|
||||
import { InboxOutlined } from '@ant-design/icons';
|
||||
import React, { useState, useRef } from 'react';
|
||||
import { Upload, message, Progress, Card, Button } from 'antd';
|
||||
import { InboxOutlined, FileTextOutlined, CheckCircleOutlined, CloseCircleOutlined, LoadingOutlined, DeleteOutlined, ClockCircleOutlined } from '@ant-design/icons';
|
||||
import type { UploadProps } from 'antd';
|
||||
import { useKnowledgeBaseStore } from '../stores/useKnowledgeBaseStore';
|
||||
|
||||
@@ -12,6 +12,18 @@ interface DocumentUploadProps {
|
||||
disabled?: boolean;
|
||||
maxDocuments?: number;
|
||||
currentDocumentCount?: number;
|
||||
existingDocuments?: string[]; // 已存在的文档文件名列表
|
||||
}
|
||||
|
||||
// 上传文件状态
|
||||
interface UploadFileStatus {
|
||||
uid: string;
|
||||
name: string;
|
||||
size: number;
|
||||
file: File;
|
||||
status: 'waiting' | 'uploading' | 'success' | 'error';
|
||||
progress: number;
|
||||
errorMessage?: string;
|
||||
}
|
||||
|
||||
const DocumentUpload: React.FC<DocumentUploadProps> = ({
|
||||
@@ -20,9 +32,18 @@ const DocumentUpload: React.FC<DocumentUploadProps> = ({
|
||||
disabled = false,
|
||||
maxDocuments = 50,
|
||||
currentDocumentCount = 0,
|
||||
existingDocuments = [],
|
||||
}) => {
|
||||
const [uploading, setUploading] = useState(false);
|
||||
const [uploadProgress, setUploadProgress] = useState(0);
|
||||
const [uploadQueue, setUploadQueue] = useState<UploadFileStatus[]>([]);
|
||||
const [isProcessing, setIsProcessing] = useState(false);
|
||||
const processingRef = useRef(false); // 防止重复触发
|
||||
|
||||
// 获取当前知识库的文档列表用于查重
|
||||
const { documents } = useKnowledgeBaseStore();
|
||||
const existingFilenames = new Set([
|
||||
...existingDocuments,
|
||||
...documents.map(d => d.filename.toLowerCase())
|
||||
]);
|
||||
|
||||
const allowedTypes = [
|
||||
'application/pdf',
|
||||
@@ -33,107 +54,343 @@ const DocumentUpload: React.FC<DocumentUploadProps> = ({
|
||||
];
|
||||
|
||||
const allowedExtensions = ['.pdf', '.doc', '.docx', '.txt', '.md'];
|
||||
const remainingSlots = maxDocuments - currentDocumentCount;
|
||||
const isAtLimit = remainingSlots <= 0;
|
||||
|
||||
const isAtLimit = currentDocumentCount >= maxDocuments;
|
||||
|
||||
const beforeUpload = (file: File) => {
|
||||
// 检查文档数量限制
|
||||
if (isAtLimit) {
|
||||
message.error(`已达到文档数量上限(${maxDocuments}个)`);
|
||||
return Upload.LIST_IGNORE;
|
||||
}
|
||||
|
||||
// 验证单个文件
|
||||
const validateFile = (file: File, queueNames: Set<string>): string | null => {
|
||||
// 检查文件类型
|
||||
if (!allowedTypes.includes(file.type) && !allowedExtensions.some(ext => file.name.endsWith(ext))) {
|
||||
message.error(`不支持的文件类型。支持:PDF、DOC、DOCX、TXT、MD`);
|
||||
return Upload.LIST_IGNORE;
|
||||
if (!allowedTypes.includes(file.type) && !allowedExtensions.some(ext => file.name.toLowerCase().endsWith(ext))) {
|
||||
return `不支持的文件类型`;
|
||||
}
|
||||
|
||||
// 检查文件大小 (10MB)
|
||||
|
||||
// 检查文件大小
|
||||
const maxSize = 10 * 1024 * 1024;
|
||||
if (file.size > maxSize) {
|
||||
message.error('文件大小不能超过 10MB');
|
||||
return Upload.LIST_IGNORE;
|
||||
return `文件大小超过10MB`;
|
||||
}
|
||||
|
||||
// 不返回任何值,让 customRequest 处理上传
|
||||
};
|
||||
// 检查是否与已上传的文档重名
|
||||
if (existingFilenames.has(file.name.toLowerCase())) {
|
||||
return `文档已存在,请勿重复上传`;
|
||||
}
|
||||
|
||||
const customRequest: UploadProps['customRequest'] = async (options) => {
|
||||
const { file, onSuccess, onError } = options;
|
||||
// 检查是否与队列中的文件重名
|
||||
if (queueNames.has(file.name.toLowerCase())) {
|
||||
return `队列中已有同名文件`;
|
||||
}
|
||||
|
||||
try {
|
||||
setUploading(true);
|
||||
setUploadProgress(0);
|
||||
return null;
|
||||
};
|
||||
|
||||
// 使用 knowledgeBaseStore 的 uploadDocument 方法
|
||||
await useKnowledgeBaseStore.getState().uploadDocument(
|
||||
kbId,
|
||||
file as File,
|
||||
(progress) => {
|
||||
setUploadProgress(progress);
|
||||
}
|
||||
);
|
||||
// 格式化文件大小
|
||||
const formatFileSize = (bytes: number): string => {
|
||||
if (bytes < 1024) return bytes + ' B';
|
||||
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB';
|
||||
return (bytes / 1024 / 1024).toFixed(1) + ' MB';
|
||||
};
|
||||
|
||||
message.success(`${(file as File).name} 上传成功`);
|
||||
// 批量上传前的文件处理
|
||||
const beforeUpload: UploadProps['beforeUpload'] = (file, fileList) => {
|
||||
// 只在第一个文件时处理整个列表,避免重复
|
||||
if (fileList.indexOf(file) !== 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 计算可添加的文件数
|
||||
const currentQueueCount = uploadQueue.filter(f => f.status === 'waiting' || f.status === 'uploading').length;
|
||||
const availableSlots = remainingSlots - currentQueueCount;
|
||||
|
||||
if (availableSlots <= 0) {
|
||||
message.error(`已达到文档数量上限,无法添加更多文件`);
|
||||
return false;
|
||||
}
|
||||
|
||||
// 收集队列中已有的文件名
|
||||
const queueNames = new Set(uploadQueue.map(q => q.name.toLowerCase()));
|
||||
|
||||
// 验证并添加文件
|
||||
const newFiles: UploadFileStatus[] = [];
|
||||
const errors: string[] = [];
|
||||
let skippedCount = 0;
|
||||
|
||||
for (let i = 0; i < Math.min(fileList.length, availableSlots); i++) {
|
||||
const f = fileList[i];
|
||||
const error = validateFile(f, queueNames);
|
||||
|
||||
if (onSuccess) {
|
||||
onSuccess('ok');
|
||||
if (error) {
|
||||
errors.push(`${f.name}: ${error}`);
|
||||
skippedCount++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// 通知父组件刷新文档列表
|
||||
onUploadSuccess();
|
||||
} catch (error: any) {
|
||||
console.error('Upload error:', error);
|
||||
message.error(error.message || '上传失败');
|
||||
|
||||
if (onError) {
|
||||
onError(error);
|
||||
// 添加到临时集合,防止同批次中的重复文件
|
||||
queueNames.add(f.name.toLowerCase());
|
||||
|
||||
newFiles.push({
|
||||
uid: `upload-${Date.now()}-${i}-${Math.random().toString(36).substr(2, 9)}`,
|
||||
name: f.name,
|
||||
size: f.size,
|
||||
file: f,
|
||||
status: 'waiting',
|
||||
progress: 0,
|
||||
});
|
||||
}
|
||||
|
||||
// 超出配额的文件
|
||||
if (fileList.length > availableSlots) {
|
||||
skippedCount += fileList.length - availableSlots;
|
||||
}
|
||||
|
||||
// 显示错误提示
|
||||
if (errors.length > 0) {
|
||||
errors.forEach(err => message.warning(err));
|
||||
}
|
||||
if (fileList.length > availableSlots) {
|
||||
message.warning(`已达到上传配额限制,${fileList.length - availableSlots} 个文件被跳过`);
|
||||
}
|
||||
|
||||
// 添加有效文件到队列
|
||||
if (newFiles.length > 0) {
|
||||
setUploadQueue(prev => [...prev, ...newFiles]);
|
||||
message.info(`已添加 ${newFiles.length} 个文件到上传队列`);
|
||||
}
|
||||
|
||||
return false; // 阻止默认上传
|
||||
};
|
||||
|
||||
// 开始上传
|
||||
const startUpload = async () => {
|
||||
if (processingRef.current) return;
|
||||
|
||||
const waitingFiles = uploadQueue.filter(f => f.status === 'waiting');
|
||||
if (waitingFiles.length === 0) {
|
||||
message.info('没有待上传的文件');
|
||||
return;
|
||||
}
|
||||
|
||||
processingRef.current = true;
|
||||
setIsProcessing(true);
|
||||
|
||||
let successCount = 0;
|
||||
let failCount = 0;
|
||||
|
||||
for (const queueItem of waitingFiles) {
|
||||
// 更新状态为上传中
|
||||
setUploadQueue(prev => prev.map(item =>
|
||||
item.uid === queueItem.uid ? { ...item, status: 'uploading' as const, progress: 0 } : item
|
||||
));
|
||||
|
||||
try {
|
||||
await useKnowledgeBaseStore.getState().uploadDocument(
|
||||
kbId,
|
||||
queueItem.file,
|
||||
(progress) => {
|
||||
setUploadQueue(prev => prev.map(item =>
|
||||
item.uid === queueItem.uid ? { ...item, progress } : item
|
||||
));
|
||||
}
|
||||
);
|
||||
|
||||
// 上传成功
|
||||
setUploadQueue(prev => prev.map(item =>
|
||||
item.uid === queueItem.uid ? { ...item, status: 'success' as const, progress: 100 } : item
|
||||
));
|
||||
successCount++;
|
||||
onUploadSuccess();
|
||||
|
||||
} catch (error: any) {
|
||||
// 上传失败
|
||||
setUploadQueue(prev => prev.map(item =>
|
||||
item.uid === queueItem.uid ? {
|
||||
...item,
|
||||
status: 'error' as const,
|
||||
errorMessage: error.message || '上传失败'
|
||||
} : item
|
||||
));
|
||||
failCount++;
|
||||
}
|
||||
} finally {
|
||||
setUploading(false);
|
||||
setUploadProgress(0);
|
||||
}
|
||||
|
||||
processingRef.current = false;
|
||||
setIsProcessing(false);
|
||||
|
||||
// 显示汇总结果
|
||||
if (successCount > 0 && failCount === 0) {
|
||||
message.success(`全部 ${successCount} 个文件上传成功!`);
|
||||
} else if (successCount > 0 && failCount > 0) {
|
||||
message.warning(`${successCount} 个成功,${failCount} 个失败`);
|
||||
} else if (failCount > 0) {
|
||||
message.error(`${failCount} 个文件上传失败`);
|
||||
}
|
||||
};
|
||||
|
||||
// 移除队列中的文件
|
||||
const removeFromQueue = (uid: string) => {
|
||||
setUploadQueue(prev => prev.filter(item => item.uid !== uid));
|
||||
};
|
||||
|
||||
// 清空队列
|
||||
const clearQueue = () => {
|
||||
setUploadQueue([]);
|
||||
};
|
||||
|
||||
// 获取状态图标
|
||||
const getStatusIcon = (status: UploadFileStatus['status']) => {
|
||||
switch (status) {
|
||||
case 'waiting':
|
||||
return <ClockCircleOutlined style={{ color: '#faad14', fontSize: 16 }} />;
|
||||
case 'uploading':
|
||||
return <LoadingOutlined style={{ color: '#1890ff', fontSize: 16 }} spin />;
|
||||
case 'success':
|
||||
return <CheckCircleOutlined style={{ color: '#52c41a', fontSize: 16 }} />;
|
||||
case 'error':
|
||||
return <CloseCircleOutlined style={{ color: '#ff4d4f', fontSize: 16 }} />;
|
||||
}
|
||||
};
|
||||
|
||||
const waitingCount = uploadQueue.filter(f => f.status === 'waiting').length;
|
||||
const successCount = uploadQueue.filter(f => f.status === 'success').length;
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<Dragger
|
||||
name="file"
|
||||
multiple={false}
|
||||
multiple={true}
|
||||
beforeUpload={beforeUpload}
|
||||
customRequest={customRequest}
|
||||
disabled={disabled || isAtLimit || uploading}
|
||||
disabled={disabled || isAtLimit || isProcessing}
|
||||
showUploadList={false}
|
||||
style={{ marginBottom: uploading ? 16 : 0 }}
|
||||
fileList={[]}
|
||||
openFileDialogOnClick={!isProcessing}
|
||||
style={{ marginBottom: uploadQueue.length > 0 ? 16 : 0 }}
|
||||
>
|
||||
<p className="ant-upload-drag-icon">
|
||||
<InboxOutlined />
|
||||
</p>
|
||||
<p className="ant-upload-text">
|
||||
{isAtLimit ? `已达到文档数量上限(${maxDocuments}个)` : '点击或拖拽文件到此区域上传'}
|
||||
{isAtLimit
|
||||
? `已达到文档数量上限(${maxDocuments}个)`
|
||||
: isProcessing
|
||||
? '正在上传中,请稍候...'
|
||||
: '点击或拖拽文件到此区域批量上传'}
|
||||
</p>
|
||||
<p className="ant-upload-hint">
|
||||
支持格式:PDF、DOC、DOCX、TXT、MD | 单个文件最大10MB
|
||||
</p>
|
||||
<p className="ant-upload-hint" style={{ marginTop: 4, fontSize: 12, color: '#faad14' }}>
|
||||
⚠️ 系统会自动过滤重名文件,避免重复上传
|
||||
</p>
|
||||
<p className="ant-upload-hint" style={{ marginTop: 8, fontSize: 13, color: '#8c8c8c' }}>
|
||||
当前已上传: {currentDocumentCount}/{maxDocuments} 个文档
|
||||
当前已上传: {currentDocumentCount}/{maxDocuments} 个文档(还可上传 {remainingSlots} 个)
|
||||
</p>
|
||||
</Dragger>
|
||||
|
||||
{uploading && (
|
||||
{/* 上传队列 */}
|
||||
{uploadQueue.length > 0 && (
|
||||
<div style={{ marginTop: 16 }}>
|
||||
<Progress
|
||||
percent={uploadProgress}
|
||||
status={uploadProgress === 100 ? 'success' : 'active'}
|
||||
strokeColor={{
|
||||
'0%': '#108ee9',
|
||||
'100%': '#87d068',
|
||||
}}
|
||||
/>
|
||||
<p style={{ marginTop: 8, textAlign: 'center', color: '#595959', fontSize: 13 }}>
|
||||
{uploadProgress < 100 ? '正在上传...' : '上传完成,正在处理...'}
|
||||
</p>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 12 }}>
|
||||
<span style={{ fontWeight: 600, color: '#262626', fontSize: 14 }}>
|
||||
上传队列
|
||||
<span style={{ fontWeight: 400, color: '#8c8c8c', marginLeft: 8 }}>
|
||||
({successCount} 成功 / {uploadQueue.length} 总计)
|
||||
</span>
|
||||
</span>
|
||||
<div>
|
||||
{waitingCount > 0 && !isProcessing && (
|
||||
<Button
|
||||
type="primary"
|
||||
size="small"
|
||||
onClick={startUpload}
|
||||
style={{ marginRight: 8 }}
|
||||
>
|
||||
开始上传 ({waitingCount})
|
||||
</Button>
|
||||
)}
|
||||
<Button size="small" onClick={clearQueue} disabled={isProcessing}>
|
||||
清空队列
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{
|
||||
maxHeight: 300,
|
||||
overflow: 'auto',
|
||||
border: '1px solid #f0f0f0',
|
||||
borderRadius: 8,
|
||||
backgroundColor: '#fafafa'
|
||||
}}>
|
||||
{uploadQueue.map((item) => (
|
||||
<div
|
||||
key={item.uid}
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
padding: '10px 12px',
|
||||
borderBottom: '1px solid #f0f0f0',
|
||||
backgroundColor: item.status === 'error' ? '#fff2f0' : 'transparent'
|
||||
}}
|
||||
>
|
||||
{/* 状态图标 */}
|
||||
<div style={{ marginRight: 12, flexShrink: 0 }}>
|
||||
{getStatusIcon(item.status)}
|
||||
</div>
|
||||
|
||||
{/* 文件信息 */}
|
||||
<div style={{ flex: 1, minWidth: 0 }}>
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
marginBottom: item.status === 'uploading' ? 4 : 0
|
||||
}}>
|
||||
<span style={{
|
||||
fontSize: 13,
|
||||
fontWeight: 500,
|
||||
color: item.status === 'error' ? '#ff4d4f' : '#262626',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
maxWidth: '70%'
|
||||
}}>
|
||||
<FileTextOutlined style={{ marginRight: 6, color: '#ff4d4f' }} />
|
||||
{item.name}
|
||||
</span>
|
||||
<span style={{ fontSize: 12, color: '#8c8c8c', flexShrink: 0 }}>
|
||||
{formatFileSize(item.size)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* 上传进度 */}
|
||||
{item.status === 'uploading' && (
|
||||
<Progress
|
||||
percent={item.progress}
|
||||
size="small"
|
||||
strokeColor="#1890ff"
|
||||
style={{ marginBottom: 0 }}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* 错误信息 */}
|
||||
{item.status === 'error' && item.errorMessage && (
|
||||
<div style={{ fontSize: 12, color: '#ff4d4f', marginTop: 2 }}>
|
||||
{item.errorMessage}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 删除按钮 */}
|
||||
{(item.status === 'waiting' || item.status === 'error') && !isProcessing && (
|
||||
<Button
|
||||
type="text"
|
||||
size="small"
|
||||
icon={<DeleteOutlined />}
|
||||
onClick={() => removeFromQueue(item.uid)}
|
||||
style={{ marginLeft: 8, color: '#8c8c8c' }}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
@@ -141,4 +398,3 @@ const DocumentUpload: React.FC<DocumentUploadProps> = ({
|
||||
};
|
||||
|
||||
export default DocumentUpload;
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import React, { useState, useEffect } from 'react';
|
||||
import { Button, Table, Progress, message, Card, Steps, Checkbox, Radio, Tooltip } from 'antd';
|
||||
import { Play, Download, RotateCw, FileText, CheckCircle2, Zap } from 'lucide-react';
|
||||
import type { KnowledgeBase, Document } from '../../api/knowledgeBaseApi';
|
||||
import { getAccessToken } from '../../../../framework/auth/api';
|
||||
|
||||
interface BatchModeCompleteProps {
|
||||
kbId: string;
|
||||
@@ -137,9 +138,13 @@ export const BatchModeComplete: React.FC<BatchModeCompleteProps> = ({
|
||||
// 调用批处理API(使用v2新版API)
|
||||
// 完整路径: /api/v2/pkb/batch-tasks/batch/execute
|
||||
// 请求体格式必须匹配后端 ExecuteBatchBody 接口
|
||||
const token = getAccessToken();
|
||||
const response = await fetch('/api/v2/pkb/batch-tasks/batch/execute', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...(token ? { 'Authorization': `Bearer ${token}` } : {}),
|
||||
},
|
||||
body: JSON.stringify({
|
||||
kb_id: kbId, // 后端期望 kb_id
|
||||
document_ids: selectedDocs, // 后端期望 document_ids
|
||||
@@ -174,7 +179,10 @@ export const BatchModeComplete: React.FC<BatchModeCompleteProps> = ({
|
||||
// 轮询任务状态
|
||||
const pollInterval = setInterval(async () => {
|
||||
try {
|
||||
const statusRes = await fetch(`/api/v2/pkb/batch-tasks/batch/tasks/${taskId}`);
|
||||
const pollToken = getAccessToken();
|
||||
const statusRes = await fetch(`/api/v2/pkb/batch-tasks/batch/tasks/${taskId}`, {
|
||||
headers: pollToken ? { 'Authorization': `Bearer ${pollToken}` } : {},
|
||||
});
|
||||
if (!statusRes.ok) {
|
||||
console.error('[BatchMode] 获取任务状态失败:', statusRes.status);
|
||||
return;
|
||||
@@ -211,7 +219,10 @@ export const BatchModeComplete: React.FC<BatchModeCompleteProps> = ({
|
||||
|
||||
// 获取最终结果
|
||||
try {
|
||||
const resultsRes = await fetch(`/api/v2/pkb/batch-tasks/batch/tasks/${taskId}/results`);
|
||||
const resultToken = getAccessToken();
|
||||
const resultsRes = await fetch(`/api/v2/pkb/batch-tasks/batch/tasks/${taskId}/results`, {
|
||||
headers: resultToken ? { 'Authorization': `Bearer ${resultToken}` } : {},
|
||||
});
|
||||
console.log('[BatchMode] 获取结果响应状态:', resultsRes.status);
|
||||
|
||||
if (resultsRes.ok) {
|
||||
|
||||
@@ -7,6 +7,7 @@ import React, { useMemo } from 'react';
|
||||
import { FileText, BookOpen, ExternalLink } from 'lucide-react';
|
||||
import { ChatContainer } from '@/shared/components/Chat';
|
||||
import type { KnowledgeBase, Document } from '../../api/knowledgeBaseApi';
|
||||
import { getAccessToken } from '../../../../framework/auth/api';
|
||||
|
||||
interface DeepReadModeProps {
|
||||
kbId: string;
|
||||
@@ -144,16 +145,18 @@ export const DeepReadMode: React.FC<DeepReadModeProps> = ({
|
||||
timestamp: Date.now(),
|
||||
}]}
|
||||
providerConfig={{
|
||||
apiEndpoint: '/api/v1/chat/stream',
|
||||
apiEndpoint: '/api/v2/pkb/chat/stream',
|
||||
requestFn: async (message: string) => {
|
||||
// 🔑 关键:传递 fullTextDocumentIds 而不是 documentIds
|
||||
// fullTextDocumentIds 会触发全文加载模式,AI可以看到完整文献
|
||||
// documentIds 只是过滤RAG检索结果,AI只能看到片段
|
||||
const response = await fetch('/api/v1/chat/stream', {
|
||||
const token = getAccessToken();
|
||||
const response = await fetch('/api/v2/pkb/chat/stream', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'text/event-stream',
|
||||
...(token ? { 'Authorization': `Bearer ${token}` } : {}),
|
||||
},
|
||||
body: JSON.stringify({
|
||||
content: message,
|
||||
|
||||
@@ -6,6 +6,7 @@ import React from 'react';
|
||||
import { BookOpen, FileText } from 'lucide-react';
|
||||
import { ChatContainer } from '@/shared/components/Chat';
|
||||
import type { KnowledgeBase, Document } from '../../api/knowledgeBaseApi';
|
||||
import { getAccessToken } from '../../../../framework/auth/api';
|
||||
|
||||
interface FullTextModeProps {
|
||||
kbId: string;
|
||||
@@ -111,13 +112,15 @@ export const FullTextMode: React.FC<FullTextModeProps> = ({ kbId, documents }) =
|
||||
}]}
|
||||
customMessageRenderer={renderMessageContent}
|
||||
providerConfig={{
|
||||
apiEndpoint: '/api/v1/chat/stream',
|
||||
apiEndpoint: '/api/v2/pkb/chat/stream',
|
||||
requestFn: async (message: string) => {
|
||||
const response = await fetch('/api/v1/chat/stream', {
|
||||
const token = getAccessToken();
|
||||
const response = await fetch('/api/v2/pkb/chat/stream', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'text/event-stream',
|
||||
...(token ? { 'Authorization': `Bearer ${token}` } : {}),
|
||||
},
|
||||
body: JSON.stringify({
|
||||
content: message,
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useKnowledgeBaseStore } from '../stores/useKnowledgeBaseStore';
|
||||
import DocumentUpload from '../components/DocumentUpload';
|
||||
import {
|
||||
Plus, BookOpen, Microscope, Stethoscope, Pill,
|
||||
GraduationCap, Wrench, MessageSquare, FileText,
|
||||
@@ -85,6 +86,9 @@ const DashboardPage: React.FC = () => {
|
||||
// 表单数据
|
||||
const [formData, setFormData] = useState({ name: '', department: 'Cardiology' });
|
||||
const [files, setFiles] = useState<any[]>([]);
|
||||
// 新增:创建知识库后保存ID,用于Step3上传文档
|
||||
const [createdKbId, setCreatedKbId] = useState<string | null>(null);
|
||||
const [uploadedCount, setUploadedCount] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
fetchKnowledgeBases();
|
||||
@@ -97,22 +101,34 @@ const DashboardPage: React.FC = () => {
|
||||
setSelectedTypeId(null);
|
||||
setFormData({ name: '', department: 'Cardiology' });
|
||||
setFiles([]);
|
||||
setCreatedKbId(null);
|
||||
setUploadedCount(0);
|
||||
setIsModalOpen(true);
|
||||
};
|
||||
|
||||
const handleCreateSubmit = async () => {
|
||||
// Step 2 完成后,创建知识库并进入Step 3
|
||||
const handleStep2Next = async () => {
|
||||
if (!formData.name) return;
|
||||
|
||||
try {
|
||||
// 后端期望 description 字段,将 department 作为描述的一部分
|
||||
const description = `${formData.department || ''}科 知识库`;
|
||||
const kb = await createKnowledgeBase(formData.name, description);
|
||||
message.success('知识库创建成功!');
|
||||
setIsModalOpen(false);
|
||||
navigate(`/knowledge-base/workspace/${kb.id}`);
|
||||
setCreatedKbId(kb.id);
|
||||
message.success('知识库创建成功!现在可以上传文档');
|
||||
setCreateStep(3);
|
||||
} catch (error: any) {
|
||||
message.error(error.message || '创建失败');
|
||||
}
|
||||
};
|
||||
|
||||
// Step 3 完成后,进入工作台
|
||||
const handleCreateSubmit = async () => {
|
||||
if (createdKbId) {
|
||||
setIsModalOpen(false);
|
||||
navigate(`/knowledge-base/workspace/${createdKbId}`);
|
||||
}
|
||||
};
|
||||
|
||||
// 删除模拟上传功能,Step 3留作后续实现真实上传组件
|
||||
|
||||
const getStatusText = (status: string) => {
|
||||
@@ -324,57 +340,47 @@ const DashboardPage: React.FC = () => {
|
||||
)}
|
||||
|
||||
{/* Step 3: 上传 */}
|
||||
{createStep === 3 && (
|
||||
{createStep === 3 && createdKbId && (
|
||||
<div className="max-w-3xl mx-auto space-y-6 mt-4">
|
||||
<div
|
||||
className="border-2 border-dashed border-gray-300 rounded-xl p-12 flex flex-col items-center justify-center bg-white"
|
||||
>
|
||||
<div className="w-20 h-20 bg-blue-50 rounded-full flex items-center justify-center mb-6">
|
||||
<Upload className="w-10 h-10 text-blue-400" />
|
||||
{/* 成功提示 */}
|
||||
<div className="bg-green-50 border border-green-200 rounded-xl p-4 flex items-center">
|
||||
<CheckCircle2 className="w-6 h-6 text-green-600 mr-3 flex-shrink-0" />
|
||||
<div>
|
||||
<p className="font-bold text-green-800">知识库创建成功!</p>
|
||||
<p className="text-sm text-green-600">现在可以上传文档,或点击"完成"跳过此步骤稍后上传。</p>
|
||||
</div>
|
||||
<p className="text-xl font-bold text-slate-700">上传知识资产</p>
|
||||
<p className="text-sm text-slate-500 mt-2">知识库创建后,可在工作台中上传文档</p>
|
||||
<p className="text-xs text-slate-400 mt-3">💡 提示:点击下方"完成"按钮进入工作台,在"知识资产"Tab中上传PDF文档</p>
|
||||
</div>
|
||||
|
||||
{files.length > 0 && (
|
||||
<div className="bg-white rounded-xl border border-gray-200 shadow-sm overflow-hidden">
|
||||
<div className="px-4 py-3 bg-gray-50 border-b border-gray-100 flex justify-between items-center">
|
||||
<span className="text-xs font-bold text-slate-500 uppercase">上传队列 ({files.length})</span>
|
||||
</div>
|
||||
<div className="max-h-[280px] overflow-y-auto p-2 space-y-2">
|
||||
{files.map(f => (
|
||||
<div key={f.id} className="flex items-center p-3 rounded-lg border border-gray-100 hover:bg-gray-50 transition-colors">
|
||||
<div className="w-10 h-10 bg-red-50 rounded-lg flex items-center justify-center text-red-500 mr-4 flex-shrink-0">
|
||||
<FileText className="w-6 h-6" />
|
||||
</div>
|
||||
<div className="flex-1 min-w-0 mr-4">
|
||||
<div className="flex justify-between mb-1">
|
||||
<span className="font-medium text-slate-800 text-sm truncate">{f.name}</span>
|
||||
<span className="text-xs text-slate-500">{f.size}</span>
|
||||
</div>
|
||||
<div className="w-full bg-gray-100 h-1.5 rounded-full overflow-hidden">
|
||||
<div
|
||||
className={`h-full transition-all duration-300 ${f.status === 'ready' ? 'bg-green-500' : 'bg-blue-600'}`}
|
||||
style={{ width: `${f.progress}%` }}
|
||||
></div>
|
||||
</div>
|
||||
<div className="flex justify-between mt-1">
|
||||
<span className={`text-[10px] font-bold ${f.status === 'ready' ? 'text-green-600' : 'text-blue-600'} flex items-center`}>
|
||||
{f.status !== 'ready' && <Loader2 className="w-3 h-3 mr-1 animate-spin" />}
|
||||
{getStatusText(f.status)}
|
||||
</span>
|
||||
<span className="text-[10px] text-slate-400">{f.progress}%</span>
|
||||
</div>
|
||||
</div>
|
||||
<button className="text-gray-300 hover:text-red-500 transition-colors">
|
||||
<Trash2 className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
{/* 真实的上传组件 */}
|
||||
<DocumentUpload
|
||||
kbId={createdKbId}
|
||||
onUploadSuccess={() => {
|
||||
setUploadedCount(prev => prev + 1);
|
||||
message.success('文档上传成功!');
|
||||
}}
|
||||
maxDocuments={50}
|
||||
currentDocumentCount={uploadedCount}
|
||||
/>
|
||||
|
||||
{/* 上传统计 */}
|
||||
{uploadedCount > 0 && (
|
||||
<div className="bg-blue-50 border border-blue-200 rounded-xl p-4 flex items-center justify-between">
|
||||
<div className="flex items-center">
|
||||
<FileText className="w-5 h-5 text-blue-600 mr-3" />
|
||||
<span className="text-blue-800 font-medium">
|
||||
已上传 {uploadedCount} 个文档
|
||||
</span>
|
||||
</div>
|
||||
<span className="text-xs text-blue-500">
|
||||
系统正在后台处理,可继续上传
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 提示信息 */}
|
||||
<p className="text-xs text-slate-400 text-center">
|
||||
💡 您也可以跳过此步骤,稍后在工作台的"知识资产"Tab中上传文档
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -382,30 +388,47 @@ const DashboardPage: React.FC = () => {
|
||||
{/* Modal Footer */}
|
||||
<div className="px-8 py-5 border-t border-gray-100 bg-white flex justify-between items-center z-10">
|
||||
<button
|
||||
onClick={() => setCreateStep(Math.max(1, createStep - 1))}
|
||||
disabled={createStep === 1}
|
||||
className={`px-6 py-2.5 rounded-lg font-medium transition-colors ${createStep === 1 ? 'text-gray-300 cursor-not-allowed' : 'text-slate-600 hover:bg-gray-100'}`}
|
||||
onClick={() => {
|
||||
// Step 3 不能返回(知识库已创建)
|
||||
if (createStep === 3) return;
|
||||
setCreateStep(Math.max(1, createStep - 1));
|
||||
}}
|
||||
disabled={createStep === 1 || createStep === 3}
|
||||
className={`px-6 py-2.5 rounded-lg font-medium transition-colors ${
|
||||
createStep === 1 || createStep === 3 ? 'text-gray-300 cursor-not-allowed' : 'text-slate-600 hover:bg-gray-100'
|
||||
}`}
|
||||
>
|
||||
上一步
|
||||
</button>
|
||||
|
||||
{createStep < 3 ? (
|
||||
{createStep === 1 && (
|
||||
<button
|
||||
onClick={() => {
|
||||
if (createStep === 1 && !selectedTypeId) return;
|
||||
if (createStep === 2 && !formData.name) return;
|
||||
setCreateStep(createStep + 1);
|
||||
if (!selectedTypeId) return;
|
||||
setCreateStep(2);
|
||||
}}
|
||||
disabled={(createStep === 1 && !selectedTypeId) || (createStep === 2 && !formData.name)}
|
||||
disabled={!selectedTypeId}
|
||||
className={`px-10 py-3 rounded-lg text-white font-bold shadow-md transition-all flex items-center ${
|
||||
(createStep === 1 && !selectedTypeId) || (createStep === 2 && !formData.name)
|
||||
? 'bg-gray-300 cursor-not-allowed'
|
||||
: 'bg-blue-600 hover:bg-blue-700 active:scale-95'
|
||||
!selectedTypeId ? 'bg-gray-300 cursor-not-allowed' : 'bg-blue-600 hover:bg-blue-700 active:scale-95'
|
||||
}`}
|
||||
>
|
||||
下一步 <ChevronRight className="w-4 h-4 ml-1" />
|
||||
</button>
|
||||
) : (
|
||||
)}
|
||||
|
||||
{createStep === 2 && (
|
||||
<button
|
||||
onClick={handleStep2Next}
|
||||
disabled={!formData.name}
|
||||
className={`px-10 py-3 rounded-lg text-white font-bold shadow-md transition-all flex items-center ${
|
||||
!formData.name ? 'bg-gray-300 cursor-not-allowed' : 'bg-blue-600 hover:bg-blue-700 active:scale-95'
|
||||
}`}
|
||||
>
|
||||
创建并上传文档 <ChevronRight className="w-4 h-4 ml-1" />
|
||||
</button>
|
||||
)}
|
||||
|
||||
{createStep === 3 && (
|
||||
<button
|
||||
onClick={handleCreateSubmit}
|
||||
className="px-10 py-3 rounded-lg bg-emerald-600 hover:bg-emerald-700 text-white font-bold shadow-lg hover:shadow-xl transition-all active:scale-95 flex items-center"
|
||||
|
||||
@@ -292,3 +292,5 @@ export default KnowledgePage;
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -62,6 +62,9 @@ export const useKnowledgeBaseStore = create<KnowledgeBaseState>((set, get) => ({
|
||||
fetchKnowledgeBaseById: async (id: string) => {
|
||||
set({ loading: true, error: null });
|
||||
try {
|
||||
// 🔑 切换知识库时先清空文档列表,避免显示其他知识库的文档
|
||||
set({ documents: [] });
|
||||
|
||||
const kb = await knowledgeBaseApi.getById(id);
|
||||
set({ currentKb: kb, loading: false });
|
||||
|
||||
@@ -159,13 +162,41 @@ export const useKnowledgeBaseStore = create<KnowledgeBaseState>((set, get) => ({
|
||||
try {
|
||||
const document = await documentApi.upload(kbId, file, onProgress);
|
||||
|
||||
// 🔑 验证返回的文档对象
|
||||
if (!document || !document.id) {
|
||||
// 如果没有返回有效文档,尝试刷新文档列表
|
||||
console.warn('[PKB Store] 上传响应格式异常,尝试刷新文档列表');
|
||||
await get().fetchDocuments(kbId);
|
||||
set({ loading: false });
|
||||
return document;
|
||||
}
|
||||
|
||||
// 更新文档列表
|
||||
const documents = [document, ...get().documents];
|
||||
set({ documents, loading: false });
|
||||
|
||||
return document;
|
||||
} catch (error: any) {
|
||||
const errorMsg = error.response?.data?.message || '上传文档失败';
|
||||
// 🔑 改进错误处理:检查是否实际上传成功
|
||||
const errorMsg = error.response?.data?.message || error.message || '上传文档失败';
|
||||
console.error('[PKB Store] 上传错误:', error);
|
||||
|
||||
// 尝试刷新文档列表,检查是否实际上传成功
|
||||
try {
|
||||
const docsBefore = get().documents.length;
|
||||
await get().fetchDocuments(kbId);
|
||||
const docsAfter = get().documents.length;
|
||||
|
||||
// 如果文档数增加了,说明实际上传成功
|
||||
if (docsAfter > docsBefore) {
|
||||
console.log('[PKB Store] 检测到上传实际成功(文档数增加)');
|
||||
set({ loading: false, error: null });
|
||||
return get().documents[0]; // 返回最新的文档
|
||||
}
|
||||
} catch (refreshError) {
|
||||
console.error('[PKB Store] 刷新文档列表失败:', refreshError);
|
||||
}
|
||||
|
||||
set({ error: errorMsg, loading: false });
|
||||
throw new Error(errorMsg);
|
||||
}
|
||||
@@ -230,3 +261,5 @@ export const useKnowledgeBaseStore = create<KnowledgeBaseState>((set, get) => ({
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -47,3 +47,5 @@ export interface BatchTemplate {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -125,3 +125,5 @@ export default function AgentModal({ visible, taskCount, onClose, onConfirm }: A
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -45,3 +45,5 @@ export default function BatchToolbar({ selectedCount, onRunBatch, onClearSelecti
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -68,3 +68,5 @@ export default function FilterChips({ filters, counts, onFilterChange }: FilterC
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -58,3 +58,5 @@ export default function Header({ onUpload }: HeaderProps) {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -112,3 +112,5 @@ export default function ReportDetail({ report, onBack }: ReportDetailProps) {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -40,3 +40,5 @@ export default function ScoreRing({ score, size = 'medium', showLabel = true }:
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -75,3 +75,5 @@ export default function Sidebar({ currentView, onViewChange, onSettingsClick }:
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -17,3 +17,5 @@ export { default as TaskDetail } from './TaskDetail';
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -286,3 +286,5 @@ export default function Dashboard() {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -235,3 +235,5 @@
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user