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:
2026-01-13 22:22:10 +08:00
parent 4088275290
commit 4ed67a8846
272 changed files with 1382 additions and 161 deletions

View File

@@ -44,3 +44,5 @@ export default apiClient;

View File

@@ -207,3 +207,5 @@ export { AuthContext };

View File

@@ -243,3 +243,5 @@ export async function logout(): Promise<void> {

View File

@@ -9,3 +9,5 @@ export * from './api';

View File

@@ -33,3 +33,5 @@ export async function fetchUserModules(): Promise<string[]> {
}

View File

@@ -102,3 +102,5 @@ export interface AuthContextType extends AuthState {

View File

@@ -561,6 +561,8 @@ export default FulltextDetailDrawer;

View File

@@ -154,6 +154,8 @@ export const useAssets = (activeTab: AssetTabType) => {

View File

@@ -144,6 +144,8 @@ export const useRecentTasks = () => {

View File

@@ -343,6 +343,8 @@ export default DropnaDialog;

View File

@@ -428,6 +428,8 @@ export default MetricTimePanel;

View File

@@ -314,6 +314,8 @@ export default PivotPanel;

View File

@@ -114,6 +114,8 @@ export function useSessionStatus({

View File

@@ -106,6 +106,8 @@ export interface DataStats {

View File

@@ -102,6 +102,8 @@ export type AssetTabType = 'all' | 'processed' | 'raw';

View File

@@ -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('上传响应格式异常');
},
/**

View File

@@ -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">
PDFDOCDOCXTXTMD | 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;

View File

@@ -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) {

View File

@@ -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,

View File

@@ -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,

View File

@@ -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"

View File

@@ -292,3 +292,5 @@ export default KnowledgePage;

View File

@@ -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) => ({

View File

@@ -47,3 +47,5 @@ export interface BatchTemplate {

View File

@@ -125,3 +125,5 @@ export default function AgentModal({ visible, taskCount, onClose, onConfirm }: A

View File

@@ -45,3 +45,5 @@ export default function BatchToolbar({ selectedCount, onRunBatch, onClearSelecti

View File

@@ -68,3 +68,5 @@ export default function FilterChips({ filters, counts, onFilterChange }: FilterC

View File

@@ -58,3 +58,5 @@ export default function Header({ onUpload }: HeaderProps) {

View File

@@ -112,3 +112,5 @@ export default function ReportDetail({ report, onBack }: ReportDetailProps) {

View File

@@ -40,3 +40,5 @@ export default function ScoreRing({ score, size = 'medium', showLabel = true }:

View File

@@ -75,3 +75,5 @@ export default function Sidebar({ currentView, onViewChange, onSettingsClick }:

View File

@@ -17,3 +17,5 @@ export { default as TaskDetail } from './TaskDetail';

View File

@@ -286,3 +286,5 @@ export default function Dashboard() {

View File

@@ -235,3 +235,5 @@

View File

@@ -368,3 +368,5 @@ export default function LoginPage() {

View File

@@ -336,3 +336,5 @@ const TenantListPage = () => {
export default TenantListPage;

View File

@@ -245,3 +245,5 @@ export async function fetchModuleList(): Promise<ModuleInfo[]> {
}

View File

@@ -18,7 +18,7 @@ import './styles/chat.css';
* ChatContainer 组件(简化实现)
*/
export const ChatContainer: React.FC<ChatContainerProps> = ({
conversationKey: _conversationKey,
conversationKey,
defaultMessages = [],
providerConfig,
customMessageRenderer,
@@ -29,8 +29,8 @@ export const ChatContainer: React.FC<ChatContainerProps> = ({
className = '',
style = {},
}) => {
// 如果没有默认消息,添加欢迎语
const initialMessages = defaultMessages.length > 0 ? defaultMessages : [{
// 默认欢迎语
const defaultWelcome: ChatMessage[] = [{
id: 'welcome',
role: 'assistant' as const,
content: '您好!我是您的 AI 数据分析师。我可以帮您编写代码来清洗数据。试试说:"把年龄大于60的设为老年组"。',
@@ -38,11 +38,27 @@ export const ChatContainer: React.FC<ChatContainerProps> = ({
timestamp: Date.now(),
}];
const [messages, setMessages] = useState<ChatMessage[]>(initialMessages);
const [messages, setMessages] = useState<ChatMessage[]>(
defaultMessages.length > 0 ? defaultMessages : defaultWelcome
);
const [isLoading, setIsLoading] = useState(false);
const [inputValue, setInputValue] = useState(''); // 受控输入框
const messagesEndRef = useRef<HTMLDivElement>(null); // 用于滚动到底部
// 🔑 当 conversationKey 变化时,重置消息状态
// 使用 ref 记录上一次的 key避免初始化时重复设置
const prevKeyRef = useRef(conversationKey);
useEffect(() => {
if (prevKeyRef.current !== conversationKey) {
console.log('[ChatContainer] conversationKey 变化,重置消息:', conversationKey);
const newMessages = defaultMessages.length > 0 ? defaultMessages : defaultWelcome;
setMessages(newMessages);
setInputValue('');
setIsLoading(false);
prevKeyRef.current = conversationKey;
}
}, [conversationKey, defaultMessages]);
// 滚动到底部
const scrollToBottom = useCallback(() => {
setTimeout(() => {

View File

@@ -57,6 +57,8 @@ export { default as Placeholder } from './Placeholder';

View File

@@ -37,6 +37,8 @@ interface ImportMeta {