diff --git a/.gitignore b/.gitignore index ac18449..5156210 100644 --- a/.gitignore +++ b/.gitignore @@ -14,6 +14,7 @@ docreview-frontend-deploy.tar.gz .claude/ .doc/ +.messages/ .database/ .auth_doc/ typecheck_result.txt diff --git a/app/api/cross-checking/cross-files-upload.ts b/app/api/cross-checking/cross-files-upload.ts index 129d3d2..d2b674a 100644 --- a/app/api/cross-checking/cross-files-upload.ts +++ b/app/api/cross-checking/cross-files-upload.ts @@ -362,10 +362,81 @@ export function generateFileId(): string { */ export function formatFileSize(bytes: number): string { if (bytes === 0) return '0 Bytes'; - + const k = 1024; const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB']; const i = Math.floor(Math.log(bytes) / Math.log(k)); - + return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; } + +/** + * 向已有任务上传新文档 + * + * POST /api/v2/cross_review/tasks/{task_id}/upload_documents + * + * @param params 上传参数 + * @returns 上传结果 + */ +export async function uploadDocumentToTask(params: { + taskId: number; + file: File; + jwtToken?: string | null; +}): Promise<{ + success: boolean; + data?: unknown; + error?: string; +}> { + const { taskId, file, jwtToken } = params; + + try { + console.log('[上传文档到任务] 开始上传:', { taskId, fileName: file.name }); + + const formData = new FormData(); + // 添加文件(使用 files 字段名) + formData.append('files', file, file.name); + + const uploadEndpoint = `/api/v2/cross_review/tasks/${taskId}/upload_documents`; + const uploadUrl = API_BASE_URL + uploadEndpoint; + + const headers: Record = {}; + if (jwtToken) { + headers['Authorization'] = `Bearer ${jwtToken}`; + } + + const response = await axios.post(uploadUrl, formData, { headers }); + const result = response.data; + + // 新接口响应格式: { code: 0, success: true, message: "...", data: {...} } + if (result && (result.success || result.code === 0)) { + console.log('[上传文档到任务] 上传成功:', result.message); + return { success: true, data: result.data }; + } else { + console.error('[上传文档到任务] 上传失败:', result.detail || result.message); + return { success: false, error: result.detail || result.message || '上传失败' }; + } + } catch (error) { + console.error('[上传文档到任务] 请求失败:', error); + + let errorMessage = '上传文档失败'; + if (axios.isAxiosError(error)) { + // 新接口错误格式: { detail: "错误信息" } + if (error.response?.data?.detail) { + errorMessage = error.response.data.detail; + } else if (error.response?.data?.message) { + errorMessage = error.response.data.message; + } else if (error.response?.status === 403) { + errorMessage = '无权操作:只有任务创建者或主要负责人可以上传文档'; + } else if (error.response?.status === 400) { + errorMessage = '请求参数错误'; + } + } else if (error instanceof Error) { + errorMessage = error.message || errorMessage; + } + + return { + success: false, + error: errorMessage + }; + } +} diff --git a/app/api/cross-checking/cross-files.ts b/app/api/cross-checking/cross-files.ts index 6cc3137..df45688 100644 --- a/app/api/cross-checking/cross-files.ts +++ b/app/api/cross-checking/cross-files.ts @@ -1,6 +1,7 @@ import { API_BASE_URL } from '../../config/api-config'; import { postgrestPut, postgrestGet } from '../postgrest-client'; import axios from 'axios'; +export { uploadDocumentToTask } from './cross-files-upload'; // 交叉评查任务状态枚举 export enum CrossCheckingTaskStatus { diff --git a/app/components/cross-checking/DocumentListModal.tsx b/app/components/cross-checking/DocumentListModal.tsx index bd7005e..effedba 100644 --- a/app/components/cross-checking/DocumentListModal.tsx +++ b/app/components/cross-checking/DocumentListModal.tsx @@ -8,13 +8,16 @@ import { ResultStats } from '../ui/ResultStats'; import { toastService } from '../ui/Toast'; import { AttachmentUploadModal } from '../ui/AttachmentUploadModal'; import { TemplateUploadModal } from '../ui/TemplateUploadModal'; +import { UploadArea, type UploadAreaRef } from '../ui/UploadArea'; import { formatDate } from '~/utils'; import { type CrossReviewDocumentWithVersion, type CrossReviewHistoryVersion, appendTaskDocumentAttachments, uploadCrossReviewDocumentTemplate, + uploadDocumentToTask, } from '~/api/cross-checking/cross-files'; +import { formatFileSize } from '~/api/cross-checking/cross-files-upload'; // 导出样式链接 export const links = () => []; @@ -69,15 +72,6 @@ const crossReviewAuditStatusMapping: Record { - if (bytes === 0) return "0 Bytes"; - const k = 1024; - const sizes = ["Bytes", "KB", "MB", "GB", "TB"]; - const i = Math.floor(Math.log(bytes) / Math.log(k)); - return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i]; -}; - export function DocumentListModal({ isOpen, onClose, @@ -124,6 +118,12 @@ export function DocumentListModal({ const [showTemplateUpload, setShowTemplateUpload] = useState(false); const [templateUploading, setTemplateUploading] = useState(false); + // 上传文件模态框状态 + const [showUploadModal, setShowUploadModal] = useState(false); + const [uploadedFile, setUploadedFile] = useState(null); + const [isFileUploading, setIsFileUploading] = useState(false); + const uploadAreaRef = useRef(null); + // 同步外部文档数据到本地 useEffect(() => { setLocalDocuments(documents.map(doc => ({ ...doc, isExpanded: expandedRows.has(doc.id) }))); @@ -217,6 +217,78 @@ export function DocumentListModal({ setSelectedDocumentPath(null); }; + // 打开上传文件模态框 + const handleOpenUploadModal = () => { + setShowUploadModal(true); + setUploadedFile(null); + }; + + // 关闭上传文件模态框 + const handleCloseUploadModal = () => { + setShowUploadModal(false); + setUploadedFile(null); + uploadAreaRef.current?.resetFileInput(); + }; + + // 处理文件选择 + const handleFileSelected = (files: FileList) => { + if (files.length === 0) return; + const file = files[0]; + const fileName = file.name.toLowerCase(); + const isValidType = file.type === 'application/pdf' || fileName.endsWith('.pdf') || + file.type === 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' || fileName.endsWith('.docx') || + file.type === 'application/zip' || fileName.endsWith('.zip') || + file.type === 'application/x-zip-compressed'; + // || file.type === 'application/x-7z-compressed' || fileName.endsWith('.7z'); + + if (!isValidType) { + // toastService.error('只能上传 PDF、DOCX 文件或 ZIP、7Z 压缩包'); + toastService.error('只能上传 PDF、DOCX 文件或 ZIP 压缩包'); + return; + } + setUploadedFile(file); + }; + + // 删除选中的文件 + const handleRemoveFile = () => { + setUploadedFile(null); + uploadAreaRef.current?.resetFileInput(); + }; + + // 确认上传文件 + const handleUploadFile = async () => { + if (!uploadedFile || !taskId) { + toastService.error('缺少必要参数'); + return; + } + + setIsFileUploading(true); + try { + const result = await uploadDocumentToTask({ + taskId, + file: uploadedFile, + jwtToken: frontendJWT + }); + + if (!result.success || result.error) { + throw new Error(result.error || '上传文件失败'); + } + + toastService.success('文件上传成功!正在后台处理中'); + handleCloseUploadModal(); + + // 刷新文档列表 + if (onSearch) { + onSearch(searchKeyword); + } + } catch (error) { + console.error('上传文件失败:', error); + toastService.error(error instanceof Error ? error.message : '上传文件失败'); + } finally { + setIsFileUploading(false); + } + }; + // 处理追加附件上传 const handleAttachmentUpload = async (files: File[], _mergeMode: 'overwrite' | 'new', remark: string) => { if (!taskId || !selectedDocumentId) { @@ -642,30 +714,43 @@ export function DocumentListModal({ - {/* 右侧:搜索框 */} - {onSearch && ( -
-
- handleSearchChange(e.target.value)} - className="pl-9 pr-4 py-2 border border-gray-300 rounded-md text-sm w-64 focus:outline-none focus:ring-0" - /> - - {searchKeyword && ( - - )} + {/* 右侧:搜索框 + 上传按钮 */} +
+ {/* 上传文件按钮 - 仅负责人可见 */} + {isProposer && ( + + )} + {onSearch && ( +
+
+ handleSearchChange(e.target.value)} + className="pl-9 pr-4 py-2 border border-gray-300 rounded-md text-sm w-64 focus:outline-none focus:ring-0" + /> + + {searchKeyword && ( + + )} +
-
- )} + )} +
{loading ? ( @@ -780,6 +865,150 @@ export function DocumentListModal({ title="上传合同模板" supportedFormatsDesc="支持.pdf、.docx格式,用于与合同文档进行结构对比" /> + + {/* 上传文件模态框 */} + +
+
+ {/* 左侧:上传区域 */} +
+
上传文件
+ +
支持文件类型:
+
+ + PDF + + + DOCX + + + ZIP + +
+
+ } + disabled={isFileUploading || uploadedFile !== null} + /> +
+ + {/* 右侧:文件信息展示 */} +
+
文件信息
+
+ {uploadedFile ? ( +
+ {/* 文件图标和类型 */} +
+ {(() => { + const fileName = uploadedFile.name.toLowerCase(); + if (fileName.endsWith('.pdf')) return ; + if (fileName.endsWith('.docx')) return ; + if (fileName.endsWith('.zip') || fileName.endsWith('.7z')) return ; + return ; + })()} +
+ + {/* 文件详情 */} +
+
+
文件名
+
+ {uploadedFile.name} +
+
+ +
+
文件大小
+
+ {formatFileSize(uploadedFile.size)} +
+
+
+ + {/* 删除按钮 */} +
+ +
+
+ ) : ( +
+ + 暂未选择文件 + 请在左侧上传区域选择文件 +
+ )} +
+
+
+ + {/* 按钮区域 */} +
+ + +
+ + {/* 上传进度提示 */} + {isFileUploading && ( +
+
+
+ + 正在上传文件,请稍候... +
+
+
+ )} + +
); } diff --git a/app/config/api-config.ts b/app/config/api-config.ts index cb635ec..b969a96 100644 --- a/app/config/api-config.ts +++ b/app/config/api-config.ts @@ -220,8 +220,10 @@ const configs: Record = { appId: 'idaasoauth2' // 应用ID,用于登出 }, dify: { - rerankingProviderName: 'langgenius/tongyi/tongyi', - rerankingModelName: 'gte-rerank' + // rerankingProviderName: 'langgenius/tongyi/tongyi', + rerankingProviderName: 'langgenius/xinference/xinference', + // rerankingModelName: 'gte-rerank' + rerankingModelName: 'bge-reranker-v2-m3' } }, diff --git a/app/hooks/dify-dataset-manager/dataset-settings.ts b/app/hooks/dify-dataset-manager/dataset-settings.ts index ea73e89..60841bd 100644 --- a/app/hooks/dify-dataset-manager/dataset-settings.ts +++ b/app/hooks/dify-dataset-manager/dataset-settings.ts @@ -149,6 +149,7 @@ export function useDatasetSettings( // 仅更新检索设置 await updateDatasetSettings(dataset.id, { + name: dataset.name, retrieval_model: formValuesToRetrievalModel(retrievalSettings), }); diff --git a/app/routes/api.dataset.datasets.$datasetId.tsx b/app/routes/api.dataset.datasets.$datasetId.tsx index 2cc4f78..3b1abf1 100644 --- a/app/routes/api.dataset.datasets.$datasetId.tsx +++ b/app/routes/api.dataset.datasets.$datasetId.tsx @@ -25,7 +25,7 @@ export async function loader({ request, params }: LoaderFunctionArgs) { ); } - console.log('[API] Dataset Detail:', { datasetId }); + // console.log('[API] Dataset Detail:', { datasetId }); // 转发请求到 FastAPI -> Dify API const apiUrl = `${API_BASE_URL}/dify_dataset/datasets/${datasetId}`; @@ -96,6 +96,8 @@ export async function action({ request, params }: ActionFunctionArgs) { if (method === 'PATCH') { const body = await request.json(); + // console.log('[API] body:', body); + // name 是必填字段 if (!body.name || typeof body.name !== 'string') { return new Response( @@ -121,6 +123,8 @@ export async function action({ request, params }: ActionFunctionArgs) { if (body.retrieval_model && typeof body.retrieval_model === 'object') { const rm = body.retrieval_model; + const name = body.name || ''; + // 验证 search_method const validSearchMethods = ['keyword_search', 'semantic_search', 'full_text_search', 'hybrid_search']; if (rm.search_method && !validSearchMethods.includes(rm.search_method)) { @@ -130,6 +134,8 @@ export async function action({ request, params }: ActionFunctionArgs) { ); } + allowedBody.name = name; + allowedBody.retrieval_model = { search_method: rm.search_method, reranking_enable: rm.reranking_enable ?? false, @@ -145,7 +151,7 @@ export async function action({ request, params }: ActionFunctionArgs) { }; } - console.log('[API] Update Dataset Settings:', { datasetId, body: allowedBody }); + // console.log('[API] Update Dataset Settings:', { datasetId, body: allowedBody }); const apiUrl = `${API_BASE_URL}/dify_dataset/datasets/${datasetId}`; const response = await fetch(apiUrl, { diff --git a/app/routes/cross-checking._index.tsx b/app/routes/cross-checking._index.tsx index 6f49980..cd5361e 100644 --- a/app/routes/cross-checking._index.tsx +++ b/app/routes/cross-checking._index.tsx @@ -749,11 +749,11 @@ export default function CrossCheckingIndex() { 总任务数: {stats.totalTasks} -
+ {/*
待开始: {stats.pendingTasks} -
+
*/}
进行中: @@ -806,7 +806,7 @@ export default function CrossCheckingIndex() { name="status" value={searchParams.get('status') || ''} options={[ - { value: CrossCheckingTaskStatus.PENDING, label: "未开始" }, + // { value: CrossCheckingTaskStatus.PENDING, label: "未开始" }, { value: CrossCheckingTaskStatus.IN_PROGRESS, label: "进行中" }, { value: CrossCheckingTaskStatus.COMPLETED, label: "已完成" } ]} diff --git a/app/routes/login.tsx b/app/routes/login.tsx index 541a65d..687073c 100644 --- a/app/routes/login.tsx +++ b/app/routes/login.tsx @@ -404,6 +404,7 @@ export default function Login() {
{/* 管理员登录链接 */} + {/*
*/}