From 754ec2c7b5bec8943d56f20260e22ae3022f2db0 Mon Sep 17 00:00:00 2001 From: PingChuan <1259732256@qq.com> Date: Sun, 30 Nov 2025 21:28:49 +0800 Subject: [PATCH] =?UTF-8?q?feat:=E5=AE=8C=E6=88=90dify=E7=9F=A5=E8=AF=86?= =?UTF-8?q?=E5=BA=93=E6=96=87=E6=A1=A3=E5=9F=BA=E7=A1=80CRUD=E6=A8=A1?= =?UTF-8?q?=E5=9D=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env | 7 - app/api/dify-chat/client.server.ts | 64 ++--- app/api/dify-dataset/client.server.ts | 84 ++++++ app/api/dify-dataset/client.ts | 93 ++++++- app/api/dify-dataset/index.ts | 13 +- app/api/dify-dataset/types.ts | 83 ++++++ .../dify-dataset-manager/document-list.tsx | 119 +++++---- app/components/dify-dataset-manager/index.tsx | 208 ++++++--------- .../dify-dataset-manager/sidebar.tsx | 160 ------------ ...setId.documents.$batch.indexing-status.tsx | 74 ++++++ ...uments.$documentId.segments.$segmentId.tsx | 143 ++++++++++ ...tasetId.documents.$documentId.segments.tsx | 66 +++++ ...asets.$datasetId.documents.$documentId.tsx | 4 +- ...tId.documents.$documentId.upload-file.tsx} | 39 ++- ...ts.$datasetId.documents.status.$action.tsx | 75 ++++++ ....dataset.datasets.$datasetId.documents.tsx | 6 +- .../api.dataset.datasets.$datasetId.tsx | 135 ++++++++++ app/routes/api.dataset.datasets.tsx | 2 +- .../dify-dataset-manager/document-list.css | 91 ------- .../components/dify-dataset-manager/index.css | 247 ++++++++++++++---- .../dify-dataset-manager/sidebar.css | 135 ---------- 21 files changed, 1142 insertions(+), 706 deletions(-) create mode 100644 app/api/dify-dataset/client.server.ts delete mode 100644 app/components/dify-dataset-manager/sidebar.tsx create mode 100644 app/routes/api.dataset.datasets.$datasetId.documents.$batch.indexing-status.tsx create mode 100644 app/routes/api.dataset.datasets.$datasetId.documents.$documentId.segments.$segmentId.tsx create mode 100644 app/routes/api.dataset.datasets.$datasetId.documents.$documentId.segments.tsx rename app/routes/{api.dataset.datasets.$datasetId.documents.$documentId.status.tsx => api.dataset.datasets.$datasetId.documents.$documentId.upload-file.tsx} (56%) create mode 100644 app/routes/api.dataset.datasets.$datasetId.documents.status.$action.tsx create mode 100644 app/routes/api.dataset.datasets.$datasetId.tsx delete mode 100644 app/styles/components/dify-dataset-manager/document-list.css delete mode 100644 app/styles/components/dify-dataset-manager/sidebar.css diff --git a/.env b/.env index 39f78b3..4ad3f36 100644 --- a/.env +++ b/.env @@ -1,10 +1,3 @@ -# APP ID -NEXT_PUBLIC_APP_ID=http://nas.7bm.co:12980/app/46539478-3281-4e98-a445-6da9dc078e95/configuration -# APP API key -NEXT_PUBLIC_APP_KEY=app-N3su9tKyMMnqxt2EMgOkVof7 -# API url prefix -NEXT_PUBLIC_API_URL=http://localhost:8000/dify - # JWT Secret - 用于签名和验证前端JWT token # 生产环境请务必修改为强随机字符串(至少32个字符) JWT_SECRET=gdyc-super-secrets-jjwtt-key-change-this-in-production-20250721-from-login-callback \ No newline at end of file diff --git a/app/api/dify-chat/client.server.ts b/app/api/dify-chat/client.server.ts index 210d6f3..dc91a33 100644 --- a/app/api/dify-chat/client.server.ts +++ b/app/api/dify-chat/client.server.ts @@ -1,13 +1,13 @@ /** - * Dify 服务端基础 API 模块 + * Dify Chat 服务端 API 模块 * * 提供 Node.js 服务端调用 FastAPI 后端的基础功能 - * 包括配置管理和基础请求函数 + * Dify 的 API_KEY 和 APP_ID 由 FastAPI 后端管理,前端只负责转发请求 * * 调用链路: - * Remix Server → FastAPI /dify/* → Dify + * Remix Server → FastAPI /dify_chat/* → Dify * - * @module api/dify/client.server + * @module api/dify-chat/client.server */ import { API_BASE_URL } from '~/config/api-config'; @@ -17,45 +17,25 @@ import { API_BASE_URL } from '~/config/api-config'; // ============================================================================ /** - * 获取环境变量的服务端函数 + * Dify Chat API 代理地址 + * 通过 FastAPI 后端的 /dify_chat 路由代理访问 Dify + * Dify 的认证(API_KEY)由 FastAPI 后端处理 */ -const getServerEnvVar = (name: string, defaultValue: string = ''): string => { - return process.env[name] || defaultValue; -}; - -/** - * Dify API 客户端配置 - * 通过 FastAPI 后端的 /dify 路由代理访问 Dify - */ -const DIFY_CONFIG = { - API_URL: `${API_BASE_URL}/dify_chat`, - API_KEY: getServerEnvVar('NEXT_PUBLIC_APP_KEY', ''), - APP_ID: (() => { - const rawAppId = getServerEnvVar('NEXT_PUBLIC_APP_ID', ''); - const match = rawAppId.match(/\/app\/([a-f0-9-]{36})/); - return match ? match[1] : rawAppId; - })(), -}; - -console.log('[Dify Server] 配置:', { - apiUrl: DIFY_CONFIG.API_URL, - apiBaseUrl: API_BASE_URL, - appId: DIFY_CONFIG.APP_ID, - configComplete: !!(DIFY_CONFIG.API_URL && DIFY_CONFIG.APP_ID) -}); +const DIFY_CHAT_API_URL = `${API_BASE_URL}/dify_chat`; // ============================================================================ // 基础请求函数 // ============================================================================ /** - * Dify API 基础请求函数 + * Dify Chat API 基础请求函数 * - * 使用 JWT 认证通过 FastAPI 代理访问 Dify + * 使用用户 JWT 认证通过 FastAPI 代理访问 Dify + * FastAPI 后端会验证 JWT 并添加 Dify API_KEY * * @param endpoint - API 端点路径 * @param options - fetch 请求选项 - * @param jwt - JWT 认证令牌 + * @param jwt - 用户 JWT 认证令牌 * @returns Response 对象 */ export async function difyFetch( @@ -63,7 +43,7 @@ export async function difyFetch( options: RequestInit = {}, jwt?: string ): Promise { - const url = `${DIFY_CONFIG.API_URL}/${endpoint.replace(/^\//, '')}`; + const url = `${DIFY_CHAT_API_URL}/${endpoint.replace(/^\//, '')}`; const headers: HeadersInit = { 'Content-Type': 'application/json', @@ -73,7 +53,7 @@ export async function difyFetch( if (jwt) { (headers as Record)['Authorization'] = `Bearer ${jwt}`; } else { - console.warn('[Dify Server] 没有提供 JWT,转发fastapi请求可能失败'); + console.warn('[Dify Chat] 没有提供 JWT,FastAPI 请求可能失败'); } const response = await fetch(url, { @@ -83,7 +63,7 @@ export async function difyFetch( if (!response.ok) { const errorText = await response.text(); - console.error('[Dify Server] API 转发错误:', { + console.error('[Dify Chat] API 转发错误:', { status: response.status, statusText: response.statusText, error: errorText @@ -99,19 +79,5 @@ export async function difyFetch( return response; } -// ============================================================================ -// 工具函数 -// ============================================================================ - -/** - * Dify 工具函数 - */ -export const difyUtils = { - /** - * 获取 Dify 配置 - */ - getConfig: () => DIFY_CONFIG, -}; - // 重新导出 chat 模块的 difyClient export { difyClient } from './chat'; diff --git a/app/api/dify-dataset/client.server.ts b/app/api/dify-dataset/client.server.ts new file mode 100644 index 0000000..f168043 --- /dev/null +++ b/app/api/dify-dataset/client.server.ts @@ -0,0 +1,84 @@ +/** + * Dify Dataset 服务端 API 模块 + * + * 提供 Node.js 服务端调用 FastAPI 后端的基础功能 + * Dify 的 DATASET_API_KEY 由 FastAPI 后端管理,前端只负责转发请求 + * + * 调用链路: + * Remix Server → FastAPI /dify_dataset/* → Dify Knowledge API + * + * @module api/dify-dataset/client.server + */ + +import { API_BASE_URL } from '~/config/api-config'; + +// ============================================================================ +// 配置 +// ============================================================================ + +/** + * Dify Dataset API 代理地址 + * 通过 FastAPI 后端的 /dify_dataset 路由代理访问 Dify Knowledge API + * Dify 的认证(DATASET_API_KEY)由 FastAPI 后端处理 + */ +const DIFY_DATASET_API_URL = `${API_BASE_URL}/dify_dataset`; + +// ============================================================================ +// 基础请求函数 +// ============================================================================ + +/** + * Dify Dataset API 基础请求函数 + * + * 使用用户 JWT 认证通过 FastAPI 代理访问 Dify Knowledge API + * FastAPI 后端会验证 JWT 并添加 Dify DATASET_API_KEY + * + * @param endpoint - API 端点路径 + * @param options - fetch 请求选项 + * @param jwt - 用户 JWT 认证令牌 + * @returns Response 对象 + */ +export async function difyDatasetFetch( + endpoint: string, + options: RequestInit = {}, + jwt?: string +): Promise { + const url = `${DIFY_DATASET_API_URL}/${endpoint.replace(/^\//, '')}`; + + const headers: HeadersInit = { + ...options.headers, + }; + + // 如果不是 FormData,设置 Content-Type 为 JSON + if (!(options.body instanceof FormData)) { + (headers as Record)['Content-Type'] = 'application/json'; + } + + if (jwt) { + (headers as Record)['Authorization'] = `Bearer ${jwt}`; + } else { + console.warn('[Dify Dataset] 没有提供 JWT,FastAPI 请求可能失败'); + } + + const response = await fetch(url, { + ...options, + headers, + }); + + if (!response.ok) { + const errorText = await response.text(); + console.error('[Dify Dataset] API 转发错误:', { + status: response.status, + statusText: response.statusText, + error: errorText + }); + + if (response.status === 401) { + throw new Error('JWT认证失败,请重新登录'); + } + + throw new Error(`Dify Dataset API Error: ${response.status} ${response.statusText}`); + } + + return response; +} diff --git a/app/api/dify-dataset/client.ts b/app/api/dify-dataset/client.ts index bc3eda4..ce0d09e 100644 --- a/app/api/dify-dataset/client.ts +++ b/app/api/dify-dataset/client.ts @@ -9,11 +9,15 @@ import axios from 'axios'; import type { + Dataset, DatasetsResponse, DocumentsResponse, SegmentsResponse, Document, OperationResult, + IndexingStatusResponse, + UploadFileInfo, + UpdateDatasetRequest, } from './types'; // ============================================================================ @@ -59,14 +63,38 @@ export async function fetchDatasets( * @param datasetId - 知识库 ID * @returns 知识库详情 */ -export async function fetchDataset(datasetId: string): Promise { - const response = await axios.get( +export async function fetchDataset(datasetId: string): Promise { + const response = await axios.get( `${API_URL}/datasets/${datasetId}`, { withCredentials: true } ); return response.data; } +/** + * 更新知识库详情 + * + * @param datasetId - 知识库 ID + * @param data - 更新数据 + * @returns 更新后的知识库详情 + */ +export async function updateDataset( + datasetId: string, + data: UpdateDatasetRequest +): Promise { + console.log('[Dataset Client] 更新知识库:', { datasetId, data }); + + const response = await axios.patch( + `${API_URL}/datasets/${datasetId}`, + data, + { + headers: { 'Content-Type': 'application/json' }, + withCredentials: true, + } + ); + return response.data; +} + // ============================================================================ // 文档 API // ============================================================================ @@ -144,6 +172,8 @@ export async function deleteDocument( /** * 启用/禁用文档 + * Dify API: PATCH /datasets/{dataset_id}/documents/status/{action} + * action: enable / disable / archive / un_archive * * @param datasetId - 知识库 ID * @param documentId - 文档 ID @@ -155,11 +185,12 @@ export async function toggleDocumentStatus( documentId: string, enabled: boolean ): Promise { - console.log('[Dataset Client] 切换文档状态:', { datasetId, documentId, enabled }); + const action = enabled ? 'enable' : 'disable'; + console.log('[Dataset Client] 切换文档状态:', { datasetId, documentId, action }); const response = await axios.patch( - `${API_URL}/datasets/${datasetId}/documents/${documentId}/status`, - { enabled }, + `${API_URL}/datasets/${datasetId}/documents/status/${action}`, + { document_ids: [documentId] }, { headers: { 'Content-Type': 'application/json' }, withCredentials: true, @@ -231,6 +262,8 @@ export async function deleteSegment( /** * 启用/禁用分段 + * Dify API: POST /datasets/{dataset_id}/documents/{document_id}/segments/{segment_id} + * 通过更新分段的方式来切换状态 * * @param datasetId - 知识库 ID * @param documentId - 文档 ID @@ -244,9 +277,11 @@ export async function toggleSegmentStatus( segmentId: string, enabled: boolean ): Promise { - const response = await axios.patch( - `${API_URL}/datasets/${datasetId}/documents/${documentId}/segments/${segmentId}/status`, - { enabled }, + console.log('[Dataset Client] 切换分段状态:', { datasetId, documentId, segmentId, enabled }); + + const response = await axios.post( + `${API_URL}/datasets/${datasetId}/documents/${documentId}/segments/${segmentId}`, + { segment: { enabled } }, { headers: { 'Content-Type': 'application/json' }, withCredentials: true, @@ -284,7 +319,7 @@ export async function uploadDocument( console.log('[Dataset Client] 上传文档:', { datasetId, fileName: file.name }); const response = await axios.post( - `${API_URL}/datasets/${datasetId}/documents/create-by-file`, + `${API_URL}/datasets/${datasetId}/documents`, formData, { withCredentials: true, @@ -298,3 +333,43 @@ export async function uploadDocument( ); return response.data; } + +/** + * 获取文档嵌入状态(索引进度) + * + * @param datasetId - 知识库 ID + * @param batch - 上传文档的批次号 + * @returns 索引状态列表 + */ +export async function fetchIndexingStatus( + datasetId: string, + batch: string +): Promise { + console.log('[Dataset Client] 获取索引状态:', { datasetId, batch }); + + const response = await axios.get( + `${API_URL}/datasets/${datasetId}/documents/${batch}/indexing-status`, + { withCredentials: true } + ); + return response.data; +} + +/** + * 获取文档上传文件信息 + * + * @param datasetId - 知识库 ID + * @param documentId - 文档 ID + * @returns 上传文件信息 + */ +export async function fetchUploadFileInfo( + datasetId: string, + documentId: string +): Promise { + console.log('[Dataset Client] 获取上传文件信息:', { datasetId, documentId }); + + const response = await axios.get( + `${API_URL}/datasets/${datasetId}/documents/${documentId}/upload-file`, + { withCredentials: true } + ); + return response.data; +} diff --git a/app/api/dify-dataset/index.ts b/app/api/dify-dataset/index.ts index ade8698..1d1842a 100644 --- a/app/api/dify-dataset/index.ts +++ b/app/api/dify-dataset/index.ts @@ -16,21 +16,32 @@ export type { OperationResult, CreateDocumentResponse, UploadProgress, + DocumentIndexingStatus, + IndexingStatusResponse, + UploadFileInfo, + RetrievalModel, + UpdateDatasetRequest, } from './types'; -// 客户端 API 导出 +// 客户端 API 导出(浏览器端使用 axios) export { // 知识库 fetchDatasets, fetchDataset, + updateDataset, // 文档 fetchDocuments, fetchDocument, deleteDocument, toggleDocumentStatus, uploadDocument, + fetchIndexingStatus, + fetchUploadFileInfo, // 分段 fetchSegments, deleteSegment, toggleSegmentStatus, } from './client'; + +// 服务端 API 请直接从 client.server.ts 导入 +// import { difyDatasetFetch } from '~/api/dify-dataset/client.server'; diff --git a/app/api/dify-dataset/types.ts b/app/api/dify-dataset/types.ts index 037c275..18cbbf9 100644 --- a/app/api/dify-dataset/types.ts +++ b/app/api/dify-dataset/types.ts @@ -165,3 +165,86 @@ export interface UploadProgress { total: number; percent: number; } + +// ============================================================================ +// 索引状态类型 +// ============================================================================ + +/** + * 单个文档的索引状态 + */ +export interface DocumentIndexingStatus { + id: string; + indexing_status: IndexingStatus; + processing_started_at: number | null; + parsing_completed_at: number | null; + cleaning_completed_at: number | null; + splitting_completed_at: number | null; + completed_at: number | null; + paused_at: number | null; + error: string | null; + stopped_at: number | null; + completed_segments: number; + total_segments: number; +} + +/** + * 批量文档索引状态响应 + */ +export interface IndexingStatusResponse { + data: DocumentIndexingStatus[]; +} + +// ============================================================================ +// 上传文件信息类型 +// ============================================================================ + +/** + * 上传文件信息 + */ +export interface UploadFileInfo { + id: string; + name: string; + size: number; + extension: string; + url: string; + download_url: string; + mime_type: string; + created_by: string; + created_at: number; +} + +// ============================================================================ +// 知识库更新类型 +// ============================================================================ + +/** + * 检索模型配置 + */ +export interface RetrievalModel { + search_method: 'keyword_search' | 'semantic_search' | 'full_text_search' | 'hybrid_search'; + reranking_enable?: boolean; + reranking_mode?: string | null; + reranking_model?: { + reranking_provider_name: string; + reranking_model_name: string; + }; + weights?: number | null; + top_k?: number; + score_threshold_enabled?: boolean; + score_threshold?: number | null; +} + +/** + * 更新知识库请求参数 + */ +export interface UpdateDatasetRequest { + name?: string; + description?: string; + indexing_technique?: 'high_quality' | 'economy'; + permission?: 'only_me' | 'all_team_members' | 'partial_members'; + embedding_model_provider?: string; + embedding_model?: string; + retrieval_model?: RetrievalModel; + partial_member_list?: string[]; +} diff --git a/app/components/dify-dataset-manager/document-list.tsx b/app/components/dify-dataset-manager/document-list.tsx index 1fc2bef..57c7015 100644 --- a/app/components/dify-dataset-manager/document-list.tsx +++ b/app/components/dify-dataset-manager/document-list.tsx @@ -29,7 +29,7 @@ import { import type { ColumnsType } from 'antd/es/table'; import type { Document, IndexingStatus } from '~/api/dify-dataset'; import { deleteDocument, toggleDocumentStatus, uploadDocument } from '~/api/dify-dataset'; -import '../../styles/components/dify-dataset-manager/document-list.css'; +import '../../styles/components/dify-dataset-manager/index.css'; interface DocumentListProps { datasetId: string; @@ -275,10 +275,8 @@ export default function DocumentList({ return (
{/* 头部区域 */} -
-

- {datasetName || '请选择知识库'} -

+
+

{datasetName || '知识库文档'}

- {/* 文档表格 */} - {!datasetId ? ( -
- + {/* 文档表格 - 固定表头和分页 */} +
+ {loading && documents.length === 0 ? ( +
+ + 加载中... +
+ ) : filteredDocuments.length === 0 ? ( +
+ + {!searchValue && ( + + + + )} + +
+ ) : ( + + )} + + + {/* 固定底部分页器 */} + {filteredDocuments.length > 0 && ( +
+ 共 {total} 条 +
+ + + 第 {page} 页 / 共 {Math.ceil(total / pageSize)} 页 + + +
- ) : loading && documents.length === 0 ? ( -
- - 加载中... -
- ) : filteredDocuments.length === 0 ? ( -
- - {!searchValue && ( - - - - )} - -
- ) : ( -
`共 ${total} 条`, - }} - size="middle" - /> )} ); diff --git a/app/components/dify-dataset-manager/index.tsx b/app/components/dify-dataset-manager/index.tsx index 4f43d0b..1f67b8c 100644 --- a/app/components/dify-dataset-manager/index.tsx +++ b/app/components/dify-dataset-manager/index.tsx @@ -1,30 +1,18 @@ -import { Layout, theme, message } from 'antd'; import { useEffect, useState } from 'react'; -import DatasetSidebar from './sidebar'; +import { message, Spin } from 'antd'; import DocumentList from './document-list'; import type { Dataset, Document } from '~/api/dify-dataset'; import { fetchDatasets, fetchDocuments } from '~/api/dify-dataset'; import '../../styles/components/dify-dataset-manager/index.css'; -const { Content } = Layout; - /** * 知识库管理主组件 + * 简化版 - 假设只有一个知识库,直接显示文档列表 */ export default function DatasetManager() { - // 主题 - const { - token: { colorBgContainer }, - } = theme.useToken(); - - // 侧边栏状态 - const [sidebarCollapsed, setSidebarCollapsed] = useState(false); - const [isMobile, setIsMobile] = useState(false); - // 知识库状态 - const [datasets, setDatasets] = useState([]); - const [currentDatasetId, setCurrentDatasetId] = useState(''); - const [loadingDatasets, setLoadingDatasets] = useState(true); + const [dataset, setDataset] = useState(null); + const [loadingDataset, setLoadingDataset] = useState(true); // 文档状态 const [documents, setDocuments] = useState([]); @@ -38,29 +26,29 @@ export default function DatasetManager() { const [error, setError] = useState(null); /** - * 加载知识库列表 + * 加载知识库(获取第一个知识库) */ - const loadDatasets = async () => { - setLoadingDatasets(true); + const loadDataset = async () => { + setLoadingDataset(true); try { - console.log('[DatasetManager] 加载知识库列表...'); - const response = await fetchDatasets(1, 100); - console.log('[DatasetManager] 知识库列表响应:', response); + console.log('[DatasetManager] 加载知识库...'); + const response = await fetchDatasets(1, 1); + console.log('[DatasetManager] 知识库响应:', response); - if (response && response.data) { - setDatasets(response.data); - - // 如果有知识库,默认选中第一个 - if (response.data.length > 0 && !currentDatasetId) { - setCurrentDatasetId(response.data[0].id); - } + if (response && response.data && response.data.length > 0) { + const firstDataset = response.data[0]; + setDataset(firstDataset); + // 立即加载文档 + await loadDocuments(firstDataset.id, 1); + } else { + setError('未找到知识库,请先在Dify中创建知识库'); } } catch (err: any) { - console.error('[DatasetManager] 加载知识库列表失败:', err); - setError(err.message || '加载知识库列表失败'); - message.error('加载知识库列表失败'); + console.error('[DatasetManager] 加载知识库失败:', err); + setError(err.message || '加载知识库失败'); + message.error('加载知识库失败'); } finally { - setLoadingDatasets(false); + setLoadingDataset(false); setInited(true); } }; @@ -90,21 +78,13 @@ export default function DatasetManager() { } }; - /** - * 处理知识库选择 - */ - const handleDatasetSelect = (datasetId: string) => { - if (datasetId !== currentDatasetId) { - setCurrentDatasetId(datasetId); - setDocumentPage(1); - } - }; - /** * 处理文档页码变化 */ const handlePageChange = (page: number) => { - loadDocuments(currentDatasetId, page); + if (dataset) { + loadDocuments(dataset.id, page); + } }; /** @@ -115,13 +95,12 @@ export default function DatasetManager() { setDocumentTotal((prev) => prev - 1); // 更新知识库的文档数量 - setDatasets((prev) => - prev.map((ds) => - ds.id === currentDatasetId - ? { ...ds, document_count: ds.document_count - 1 } - : ds - ) - ); + if (dataset) { + setDataset({ + ...dataset, + document_count: dataset.document_count - 1 + }); + } }; /** @@ -139,103 +118,62 @@ export default function DatasetManager() { * 刷新文档列表 */ const handleRefresh = () => { - loadDocuments(currentDatasetId, documentPage); - }; - - /** - * 处理侧边栏切换 - */ - const handleSidebarToggle = () => { - setSidebarCollapsed(!sidebarCollapsed); + if (dataset) { + loadDocuments(dataset.id, documentPage); + } }; // 初始化 useEffect(() => { - loadDatasets(); + loadDataset(); }, []); - // 当选中的知识库变化时,加载文档列表 - useEffect(() => { - if (currentDatasetId) { - loadDocuments(currentDatasetId, 1); - } - }, [currentDatasetId]); - - // 检查屏幕尺寸 - useEffect(() => { - const checkScreenSize = () => { - setIsMobile(window.innerWidth < 992); - }; - - checkScreenSize(); - window.addEventListener('resize', checkScreenSize); - - return () => { - window.removeEventListener('resize', checkScreenSize); - }; - }, []); - - // 获取当前选中的知识库 - const currentDataset = datasets.find((ds) => ds.id === currentDatasetId); - - // 如果有错误,显示错误页面 - if (error && !inited) { + // 加载中状态 + if (!inited || loadingDataset) { return ( -
-
-

加载失败

-

{error}

+
+
+
+ + 正在加载知识库... +
+
+
+ ); + } + + // 错误状态 + if (error) { + return ( +
+
+
+ +

加载失败

+

{error}

+
); } return ( - - {/* 移动端遮罩层 */} - {!sidebarCollapsed && isMobile && ( -
+
+ - )} - - {/* 侧边栏 */} - - - {/* 主内容区域 */} - - - - - - +
+
); } diff --git a/app/components/dify-dataset-manager/sidebar.tsx b/app/components/dify-dataset-manager/sidebar.tsx deleted file mode 100644 index 7ea6f8f..0000000 --- a/app/components/dify-dataset-manager/sidebar.tsx +++ /dev/null @@ -1,160 +0,0 @@ -import { useState } from 'react'; -import { Button, Layout, Menu, theme, Input, Spin } from 'antd'; -import { - MenuFoldOutlined, - MenuUnfoldOutlined, - DatabaseOutlined, - SearchOutlined, - FileTextOutlined, -} from '@ant-design/icons'; -import type { Dataset } from '~/api/dify-dataset'; -import '../../styles/components/dify-dataset-manager/sidebar.css'; - -const { Sider } = Layout; - -interface DatasetSidebarProps { - collapsed: boolean; - onToggle: () => void; - datasets: Dataset[]; - currentDatasetId: string; - onDatasetSelect: (datasetId: string) => void; - loading?: boolean; -} - -/** - * 知识库侧边栏组件 - */ -export default function DatasetSidebar({ - collapsed, - onToggle, - datasets, - currentDatasetId, - onDatasetSelect, - loading = false, -}: DatasetSidebarProps) { - const [searchValue, setSearchValue] = useState(''); - const { - token: { colorBgContainer }, - } = theme.useToken(); - - // 过滤知识库列表 - const filteredDatasets = datasets.filter((ds) => - ds.name.toLowerCase().includes(searchValue.toLowerCase()) - ); - - // 生成菜单项 - const menuItems = filteredDatasets.map((ds) => ({ - key: ds.id, - icon: , - label: ( -
- - {ds.name} - - {!collapsed && ( -
- - - {ds.document_count} - -
- )} -
- ), - })); - - return ( - - {/* 侧边栏头部 */} -
-
-
- - {/* 搜索框 */} - {!collapsed && ( - } - value={searchValue} - onChange={(e) => setSearchValue(e.target.value)} - allowClear - /> - )} -
- - {/* 知识库列表 */} -
- {loading ? ( -
- -
- ) : ( - <> - {!collapsed && filteredDatasets.length === 0 && searchValue && ( -
- -

未找到相关知识库

-
- )} - - {!collapsed && datasets.length === 0 && !searchValue && ( -
- -

暂无知识库

-
- )} - - onDatasetSelect(key)} - style={{ - border: 'none', - background: 'transparent', - }} - className="dataset-sidebar-menu" - /> - - )} -
- - {/* 侧边栏底部 */} - {!collapsed && datasets.length > 0 && ( -
-
- 共 {datasets.length} 个知识库 -
-
- )} -
- ); -} diff --git a/app/routes/api.dataset.datasets.$datasetId.documents.$batch.indexing-status.tsx b/app/routes/api.dataset.datasets.$datasetId.documents.$batch.indexing-status.tsx new file mode 100644 index 0000000..fdd695c --- /dev/null +++ b/app/routes/api.dataset.datasets.$datasetId.documents.$batch.indexing-status.tsx @@ -0,0 +1,74 @@ +import { type LoaderFunctionArgs } from '@remix-run/node'; +import { API_BASE_URL } from '~/config/api-config'; + +/** + * GET /api/dataset/datasets/:datasetId/documents/:batch/indexing-status + * 获取文档嵌入状态(处理进度) + * + * Dify API: GET /datasets/{dataset_id}/documents/{batch}/indexing-status + * + * 返回示例: + * { + * "data": [{ + * "id": "", + * "indexing_status": "indexing", + * "processing_started_at": 1681623462.0, + * "parsing_completed_at": 1681623462.0, + * "cleaning_completed_at": 1681623462.0, + * "splitting_completed_at": 1681623462.0, + * "completed_at": null, + * "paused_at": null, + * "error": null, + * "stopped_at": null, + * "completed_segments": 24, + * "total_segments": 100 + * }] + * } + */ +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 { datasetId, batch } = params; + if (!datasetId || !batch) { + return new Response( + JSON.stringify({ error: '缺少必要参数 (datasetId, batch)' }), + { status: 400, headers: { 'Content-Type': 'application/json' } } + ); + } + + console.log('[API] Indexing Status:', { datasetId, batch }); + + // 转发请求到 FastAPI -> Dify API + const apiUrl = `${API_BASE_URL}/dify_dataset/datasets/${datasetId}/documents/${batch}/indexing-status`; + const response = await fetch(apiUrl, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${frontendJWT}`, + }, + }); + + const data = await response.json(); + + return new Response(JSON.stringify(data), { + status: response.status, + headers: { 'Content-Type': 'application/json' }, + }); + + } catch (error: any) { + console.error('[API] Indexing Status - Error:', error.message); + return new Response( + JSON.stringify({ error: error.message || 'Failed to get indexing status' }), + { status: 500, headers: { 'Content-Type': 'application/json' } } + ); + } +} diff --git a/app/routes/api.dataset.datasets.$datasetId.documents.$documentId.segments.$segmentId.tsx b/app/routes/api.dataset.datasets.$datasetId.documents.$documentId.segments.$segmentId.tsx new file mode 100644 index 0000000..9ebf550 --- /dev/null +++ b/app/routes/api.dataset.datasets.$datasetId.documents.$documentId.segments.$segmentId.tsx @@ -0,0 +1,143 @@ +import { type LoaderFunctionArgs, type ActionFunctionArgs } from '@remix-run/node'; +import { API_BASE_URL } from '~/config/api-config'; + +/** + * GET /api/dataset/datasets/:datasetId/documents/:documentId/segments/:segmentId - 获取分段详情 + * Dify API: GET /datasets/{dataset_id}/documents/{document_id}/segments/{segment_id} + */ +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 { datasetId, documentId, segmentId } = params; + if (!datasetId || !documentId || !segmentId) { + return new Response( + JSON.stringify({ error: '缺少必要参数' }), + { status: 400, headers: { 'Content-Type': 'application/json' } } + ); + } + + console.log('[API] Segment Detail:', { datasetId, documentId, segmentId }); + + // 转发请求到 FastAPI -> Dify API + const apiUrl = `${API_BASE_URL}/dify_dataset/datasets/${datasetId}/documents/${documentId}/segments/${segmentId}`; + const response = await fetch(apiUrl, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${frontendJWT}`, + }, + }); + + const data = await response.json(); + + return new Response(JSON.stringify(data), { + status: response.status, + headers: { 'Content-Type': 'application/json' }, + }); + + } catch (error: any) { + console.error('[API] Segment Detail - Error:', error.message); + return new Response( + JSON.stringify({ error: error.message || 'Failed to get segment' }), + { status: 500, headers: { 'Content-Type': 'application/json' } } + ); + } +} + +/** + * DELETE /api/dataset/datasets/:datasetId/documents/:documentId/segments/:segmentId - 删除分段 + * POST - 更新分段 (Dify用POST更新分段,可传enabled参数切换状态) + * Dify API: DELETE/POST /datasets/{dataset_id}/documents/{document_id}/segments/{segment_id} + */ +export async function action({ request, params }: ActionFunctionArgs) { + 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 { datasetId, documentId, segmentId } = params; + if (!datasetId || !documentId || !segmentId) { + return new Response( + JSON.stringify({ error: '缺少必要参数' }), + { status: 400, headers: { 'Content-Type': 'application/json' } } + ); + } + + const method = request.method; + const apiUrl = `${API_BASE_URL}/dify_dataset/datasets/${datasetId}/documents/${documentId}/segments/${segmentId}`; + + if (method === 'DELETE') { + console.log('[API] Delete Segment:', { datasetId, documentId, segmentId }); + + const response = await fetch(apiUrl, { + method: 'DELETE', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${frontendJWT}`, + }, + }); + + // Dify删除分段返回 204 No Content + if (response.status === 204) { + return new Response( + JSON.stringify({ result: 'success' }), + { status: 200, headers: { 'Content-Type': 'application/json' } } + ); + } + + const data = await response.json(); + return new Response(JSON.stringify(data), { + status: response.status, + headers: { 'Content-Type': 'application/json' }, + }); + } + + if (method === 'POST') { + // POST 用于更新分段(包括启用/禁用) + const body = await request.json(); + console.log('[API] Update Segment:', { datasetId, documentId, segmentId, body }); + + const response = await fetch(apiUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${frontendJWT}`, + }, + body: JSON.stringify(body), + }); + + const data = await response.json(); + return new Response(JSON.stringify(data), { + status: response.status, + headers: { 'Content-Type': 'application/json' }, + }); + } + + return new Response( + JSON.stringify({ error: 'Method not allowed' }), + { status: 405, headers: { 'Content-Type': 'application/json' } } + ); + + } catch (error: any) { + console.error('[API] Segment Action - Error:', error.message); + return new Response( + JSON.stringify({ error: error.message || 'Failed to process segment request' }), + { status: 500, headers: { 'Content-Type': 'application/json' } } + ); + } +} diff --git a/app/routes/api.dataset.datasets.$datasetId.documents.$documentId.segments.tsx b/app/routes/api.dataset.datasets.$datasetId.documents.$documentId.segments.tsx new file mode 100644 index 0000000..ee2d6ac --- /dev/null +++ b/app/routes/api.dataset.datasets.$datasetId.documents.$documentId.segments.tsx @@ -0,0 +1,66 @@ +import { type LoaderFunctionArgs } from '@remix-run/node'; +import { API_BASE_URL } from '~/config/api-config'; + +/** + * GET /api/dataset/datasets/:datasetId/documents/:documentId/segments - 获取分段列表 + * Dify API: GET /datasets/{dataset_id}/documents/{document_id}/segments + */ +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 { datasetId, documentId } = params; + if (!datasetId || !documentId) { + return new Response( + JSON.stringify({ error: '缺少必要参数' }), + { status: 400, headers: { 'Content-Type': 'application/json' } } + ); + } + + // 获取查询参数 + const url = new URL(request.url); + const page = url.searchParams.get('page') || '1'; + const limit = url.searchParams.get('limit') || '20'; + const keyword = url.searchParams.get('keyword') || ''; + const status = url.searchParams.get('status') || ''; + + console.log('[API] Segments List:', { datasetId, documentId, page, limit, keyword, status }); + + // 构建查询参数 + const queryParams = new URLSearchParams({ page, limit }); + if (keyword) queryParams.append('keyword', keyword); + if (status) queryParams.append('status', status); + + // 转发请求到 FastAPI -> Dify API + const apiUrl = `${API_BASE_URL}/dify_dataset/datasets/${datasetId}/documents/${documentId}/segments?${queryParams}`; + const response = await fetch(apiUrl, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${frontendJWT}`, + }, + }); + + const data = await response.json(); + + return new Response(JSON.stringify(data), { + status: response.status, + headers: { 'Content-Type': 'application/json' }, + }); + + } catch (error: any) { + console.error('[API] Segments List - Error:', error.message); + return new Response( + JSON.stringify({ error: error.message || 'Failed to get segments' }), + { status: 500, headers: { 'Content-Type': 'application/json' } } + ); + } +} diff --git a/app/routes/api.dataset.datasets.$datasetId.documents.$documentId.tsx b/app/routes/api.dataset.datasets.$datasetId.documents.$documentId.tsx index 6f25e58..9bd4e37 100644 --- a/app/routes/api.dataset.datasets.$datasetId.documents.$documentId.tsx +++ b/app/routes/api.dataset.datasets.$datasetId.documents.$documentId.tsx @@ -26,7 +26,7 @@ export async function loader({ request, params }: LoaderFunctionArgs) { console.log('[API] Document Detail:', { datasetId, documentId }); - const apiUrl = `${API_BASE_URL}/dify-dataset/datasets/${datasetId}/documents/${documentId}`; + const apiUrl = `${API_BASE_URL}/dify_dataset/datasets/${datasetId}/documents/${documentId}`; const response = await fetch(apiUrl, { method: 'GET', headers: { @@ -79,7 +79,7 @@ export async function action({ request, params }: ActionFunctionArgs) { if (method === 'DELETE') { console.log('[API] Delete Document:', { datasetId, documentId }); - const apiUrl = `${API_BASE_URL}/dify-dataset/datasets/${datasetId}/documents/${documentId}`; + const apiUrl = `${API_BASE_URL}/dify_dataset/datasets/${datasetId}/documents/${documentId}`; const response = await fetch(apiUrl, { method: 'DELETE', headers: { diff --git a/app/routes/api.dataset.datasets.$datasetId.documents.$documentId.status.tsx b/app/routes/api.dataset.datasets.$datasetId.documents.$documentId.upload-file.tsx similarity index 56% rename from app/routes/api.dataset.datasets.$datasetId.documents.$documentId.status.tsx rename to app/routes/api.dataset.datasets.$datasetId.documents.$documentId.upload-file.tsx index 4e2666f..900bfd6 100644 --- a/app/routes/api.dataset.datasets.$datasetId.documents.$documentId.status.tsx +++ b/app/routes/api.dataset.datasets.$datasetId.documents.$documentId.upload-file.tsx @@ -1,10 +1,26 @@ -import { type ActionFunctionArgs } from '@remix-run/node'; +import { type LoaderFunctionArgs } from '@remix-run/node'; import { API_BASE_URL } from '~/config/api-config'; /** - * PATCH /api/dataset/datasets/:datasetId/documents/:documentId/status - 切换文档状态 + * GET /api/dataset/datasets/:datasetId/documents/:documentId/upload-file + * 获取文档上传文件信息 + * + * Dify API: GET /datasets/{dataset_id}/documents/{document_id}/upload-file + * + * 返回示例: + * { + * "id": "file_id", + * "name": "file_name", + * "size": 1024, + * "extension": "txt", + * "url": "preview_url", + * "download_url": "download_url", + * "mime_type": "text/plain", + * "created_by": "user_id", + * "created_at": 1728734540 + * } */ -export async function action({ request, params }: ActionFunctionArgs) { +export async function loader({ request, params }: LoaderFunctionArgs) { try { const { getUserSession } = await import("~/api/login/auth.server"); const { frontendJWT } = await getUserSession(request); @@ -19,24 +35,21 @@ export async function action({ request, params }: ActionFunctionArgs) { const { datasetId, documentId } = params; if (!datasetId || !documentId) { return new Response( - JSON.stringify({ error: '缺少必要参数' }), + JSON.stringify({ error: '缺少必要参数 (datasetId, documentId)' }), { status: 400, headers: { 'Content-Type': 'application/json' } } ); } - const body = await request.json(); - const { enabled } = body; + console.log('[API] Upload File Info:', { datasetId, documentId }); - console.log('[API] Toggle Document Status:', { datasetId, documentId, enabled }); - - const apiUrl = `${API_BASE_URL}/dify-dataset/datasets/${datasetId}/documents/${documentId}/status`; + // 转发请求到 FastAPI -> Dify API + const apiUrl = `${API_BASE_URL}/dify_dataset/datasets/${datasetId}/documents/${documentId}/upload-file`; const response = await fetch(apiUrl, { - method: 'PATCH', + method: 'GET', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${frontendJWT}`, }, - body: JSON.stringify({ enabled }), }); const data = await response.json(); @@ -47,9 +60,9 @@ export async function action({ request, params }: ActionFunctionArgs) { }); } catch (error: any) { - console.error('[API] Toggle Document Status - Error:', error.message); + console.error('[API] Upload File Info - Error:', error.message); return new Response( - JSON.stringify({ error: error.message || 'Failed to toggle status' }), + JSON.stringify({ error: error.message || 'Failed to get upload file info' }), { status: 500, headers: { 'Content-Type': 'application/json' } } ); } diff --git a/app/routes/api.dataset.datasets.$datasetId.documents.status.$action.tsx b/app/routes/api.dataset.datasets.$datasetId.documents.status.$action.tsx new file mode 100644 index 0000000..1cbb8c2 --- /dev/null +++ b/app/routes/api.dataset.datasets.$datasetId.documents.status.$action.tsx @@ -0,0 +1,75 @@ +import { type ActionFunctionArgs } from '@remix-run/node'; +import { API_BASE_URL } from '~/config/api-config'; + +/** + * PATCH /api/dataset/datasets/:datasetId/documents/status/:action - 批量更新文档状态 + * Dify API: PATCH /datasets/{dataset_id}/documents/status/{action} + * action: enable / disable / archive / un_archive + */ +export async function action({ request, params }: ActionFunctionArgs) { + 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 { datasetId, action: statusAction } = params; + if (!datasetId || !statusAction) { + return new Response( + JSON.stringify({ error: '缺少必要参数' }), + { status: 400, headers: { 'Content-Type': 'application/json' } } + ); + } + + // 验证action参数 + const validActions = ['enable', 'disable', 'archive', 'un_archive']; + if (!validActions.includes(statusAction)) { + return new Response( + JSON.stringify({ error: `无效的action参数,有效值: ${validActions.join(', ')}` }), + { status: 400, headers: { 'Content-Type': 'application/json' } } + ); + } + + const body = await request.json(); + const { document_ids } = body; + + if (!document_ids || !Array.isArray(document_ids) || document_ids.length === 0) { + return new Response( + JSON.stringify({ error: '缺少 document_ids 参数' }), + { status: 400, headers: { 'Content-Type': 'application/json' } } + ); + } + + console.log('[API] Update Documents Status:', { datasetId, action: statusAction, document_ids }); + + // 转发请求到 FastAPI -> Dify API + const apiUrl = `${API_BASE_URL}/dify_dataset/datasets/${datasetId}/documents/status/${statusAction}`; + const response = await fetch(apiUrl, { + method: 'PATCH', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${frontendJWT}`, + }, + body: JSON.stringify({ document_ids }), + }); + + const data = await response.json(); + + return new Response(JSON.stringify(data), { + status: response.status, + headers: { 'Content-Type': 'application/json' }, + }); + + } catch (error: any) { + console.error('[API] Update Documents Status - Error:', error.message); + return new Response( + JSON.stringify({ error: error.message || 'Failed to update documents status' }), + { status: 500, headers: { 'Content-Type': 'application/json' } } + ); + } +} diff --git a/app/routes/api.dataset.datasets.$datasetId.documents.tsx b/app/routes/api.dataset.datasets.$datasetId.documents.tsx index 051ca54..117005a 100644 --- a/app/routes/api.dataset.datasets.$datasetId.documents.tsx +++ b/app/routes/api.dataset.datasets.$datasetId.documents.tsx @@ -38,7 +38,7 @@ export async function loader({ request, params }: LoaderFunctionArgs) { if (keyword) queryParams.append('keyword', keyword); // 转发请求到 FastAPI - const apiUrl = `${API_BASE_URL}/dify-dataset/datasets/${datasetId}/documents?${queryParams}`; + const apiUrl = `${API_BASE_URL}/dify_dataset/datasets/${datasetId}/documents?${queryParams}`; const response = await fetch(apiUrl, { method: 'GET', headers: { @@ -92,8 +92,8 @@ export async function action({ request, params }: ActionFunctionArgs) { console.log('[API] Upload Document:', { datasetId }); - // 转发请求到 FastAPI - const apiUrl = `${API_BASE_URL}/dify-dataset/datasets/${datasetId}/documents/create-by-file`; + // 转发请求到 FastAPI (注意:Dify API是 /document/create-by-file,document是单数) + const apiUrl = `${API_BASE_URL}/dify_dataset/datasets/${datasetId}/document/create-by-file`; const response = await fetch(apiUrl, { method: 'POST', headers: { diff --git a/app/routes/api.dataset.datasets.$datasetId.tsx b/app/routes/api.dataset.datasets.$datasetId.tsx new file mode 100644 index 0000000..c03135e --- /dev/null +++ b/app/routes/api.dataset.datasets.$datasetId.tsx @@ -0,0 +1,135 @@ +import { type LoaderFunctionArgs, type ActionFunctionArgs } from '@remix-run/node'; +import { API_BASE_URL } from '~/config/api-config'; + +/** + * GET /api/dataset/datasets/:datasetId - 获取知识库详情 + * Dify API: GET /datasets/{dataset_id} + */ +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 { datasetId } = params; + if (!datasetId) { + return new Response( + JSON.stringify({ error: '缺少 datasetId 参数' }), + { status: 400, headers: { 'Content-Type': 'application/json' } } + ); + } + + console.log('[API] Dataset Detail:', { datasetId }); + + // 转发请求到 FastAPI -> Dify API + const apiUrl = `${API_BASE_URL}/dify_dataset/datasets/${datasetId}`; + const response = await fetch(apiUrl, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${frontendJWT}`, + }, + }); + + const data = await response.json(); + + return new Response(JSON.stringify(data), { + status: response.status, + headers: { 'Content-Type': 'application/json' }, + }); + + } catch (error: any) { + console.error('[API] Dataset Detail - Error:', error.message); + return new Response( + JSON.stringify({ error: error.message || 'Failed to get dataset' }), + { status: 500, headers: { 'Content-Type': 'application/json' } } + ); + } +} + +/** + * PATCH /api/dataset/datasets/:datasetId - 修改知识库详情 + * + * Dify API: PATCH /datasets/{dataset_id} + * + * 请求体示例: + * { + * "name": "知识库名称", + * "description": "描述", + * "indexing_technique": "high_quality", + * "permission": "only_me", + * "embedding_model_provider": "zhipuai", + * "embedding_model": "embedding-3", + * "retrieval_model": { + * "search_method": "semantic_search", + * "reranking_enable": false, + * "top_k": 2, + * "score_threshold_enabled": false + * } + * } + * + * 注意:删除知识库功能不对外开放 + */ +export async function action({ request, params }: ActionFunctionArgs) { + 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 { datasetId } = params; + if (!datasetId) { + return new Response( + JSON.stringify({ error: '缺少 datasetId 参数' }), + { status: 400, headers: { 'Content-Type': 'application/json' } } + ); + } + + const method = request.method; + + if (method === 'PATCH') { + // 修改知识库详情 + const body = await request.json(); + console.log('[API] Update Dataset:', { datasetId, body }); + + const apiUrl = `${API_BASE_URL}/dify_dataset/datasets/${datasetId}`; + const response = await fetch(apiUrl, { + method: 'PATCH', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${frontendJWT}`, + }, + body: JSON.stringify(body), + }); + + const data = await response.json(); + return new Response(JSON.stringify(data), { + status: response.status, + headers: { 'Content-Type': 'application/json' }, + }); + } + + return new Response( + JSON.stringify({ error: 'Method not allowed' }), + { status: 405, headers: { 'Content-Type': 'application/json' } } + ); + + } catch (error: any) { + console.error('[API] Dataset Action - Error:', error.message); + return new Response( + JSON.stringify({ error: error.message || 'Failed to process dataset request' }), + { status: 500, headers: { 'Content-Type': 'application/json' } } + ); + } +} diff --git a/app/routes/api.dataset.datasets.tsx b/app/routes/api.dataset.datasets.tsx index c6b91a6..803fb5b 100644 --- a/app/routes/api.dataset.datasets.tsx +++ b/app/routes/api.dataset.datasets.tsx @@ -30,7 +30,7 @@ export async function loader({ request }: LoaderFunctionArgs) { console.log('[API] Dataset List - 获取知识库列表:', { page, limit }); // 转发请求到 FastAPI - const apiUrl = `${API_BASE_URL}/dify-dataset/datasets?page=${page}&limit=${limit}`; + const apiUrl = `${API_BASE_URL}/dify_dataset/datasets?page=${page}&limit=${limit}`; const response = await fetch(apiUrl, { method: 'GET', headers: { diff --git a/app/styles/components/dify-dataset-manager/document-list.css b/app/styles/components/dify-dataset-manager/document-list.css deleted file mode 100644 index 577b73e..0000000 --- a/app/styles/components/dify-dataset-manager/document-list.css +++ /dev/null @@ -1,91 +0,0 @@ -/** - * Ɠh - ch7 - */ - -/* w */ -.document-list-toolbar { - display: flex; - align-items: center; - justify-content: space-between; - margin-bottom: 16px; - gap: 16px; -} - -.document-list-search { - width: 280px; -} - -.document-list-actions { - display: flex; - align-items: center; - gap: 12px; -} - -/* h<7 */ -.document-table { - flex: 1; -} - -.document-table .ant-table-thead > tr > th { - background-color: #fafafa; - font-weight: 600; -} - -.document-table .ant-table-tbody > tr:hover > td { - background-color: #f5f5f5; -} - -/* c */ -.document-name-cell { - display: flex; - align-items: center; - gap: 8px; -} - -.document-name-cell .anticon { - color: #999; - font-size: 16px; -} - -.document-name-cell span { - font-weight: 500; -} - -/* ~ */ -.document-status-tag { - display: inline-flex; - align-items: center; - gap: 4px; -} - -/* \ */ -.document-actions { - display: flex; - align-items: center; - gap: 4px; -} - -/* z */ -.document-list-empty { - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - padding: 60px 20px; -} - -/* ͔@ */ -@media (max-width: 991px) { - .document-list-toolbar { - flex-direction: column; - align-items: stretch; - } - - .document-list-search { - width: 100%; - } - - .document-list-actions { - justify-content: space-between; - } -} diff --git a/app/styles/components/dify-dataset-manager/index.css b/app/styles/components/dify-dataset-manager/index.css index 20a66ae..1d41ce8 100644 --- a/app/styles/components/dify-dataset-manager/index.css +++ b/app/styles/components/dify-dataset-manager/index.css @@ -1,46 +1,29 @@ /** - * Ɠh - ;@7 + * 知识库管理器 - 白色卡片风格 */ -.dataset-manager-page { +/* 外层容器 - 直接占满,与大模型对话一致 */ +.dataset-manager-wrapper { display: flex; flex-direction: column; - background-color: #f5f7f9; + height: 100%; + background: #fff; + overflow: hidden; + border-radius: 0.5rem; +} + +/* 卡片容器 - 无边框阴影,直接融入 */ +.dataset-manager-card { + display: flex; + flex-direction: column; + flex: 1; + min-height: 0; + background: #fff; overflow: hidden; } -.dataset-manager-container { - display: flex; - flex-direction: row; - height: 100%; - background-color: #f5f7f9; - overflow: hidden; -} - -/* zh */ -.dataset-empty { - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - height: 100%; - padding: 40px; - color: #999; -} - -.dataset-empty h3 { - margin-bottom: 8px; - color: #666; - font-size: 16px; -} - -.dataset-empty p { - color: #999; - font-size: 14px; -} - -/* }h */ -.dataset-loading { +/* 加载状态 */ +.dataset-loading-state { display: flex; flex-direction: column; align-items: center; @@ -49,25 +32,70 @@ gap: 16px; } -/* ;: */ +.dataset-loading-state .loading-text { + color: #666; + font-size: 14px; +} + +/* Spin 组件主题色 */ +.dataset-loading-state .ant-spin .ant-spin-dot-item, +.dataset-loading .ant-spin .ant-spin-dot-item { + background-color: rgb(0 104 74); +} + +/* 错误状态 */ +.dataset-error-state { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + height: 100%; + padding: 40px; + text-align: center; +} + +.dataset-error-state .error-icon { + font-size: 48px; + color: #ff4d4f; + margin-bottom: 16px; +} + +.dataset-error-state h3 { + font-size: 18px; + font-weight: 600; + color: #1a1a1a; + margin: 0 0 8px 0; +} + +.dataset-error-state p { + font-size: 14px; + color: #666; + margin: 0; +} + +/* 内容区域 */ .dataset-content { display: flex; flex-direction: column; flex: 1; - padding: 20px 24px; - overflow: auto; + min-height: 0; + padding: 24px; + overflow: hidden; } -/* 4: */ +/* 头部区域 */ .dataset-header { display: flex; justify-content: space-between; align-items: center; - margin-bottom: 20px; + flex-shrink: 0; + padding-bottom: 16px; + border-bottom: 1px solid #f0f0f0; + margin-bottom: 16px; } .dataset-header h1 { - font-size: 20px; + font-size: 18px; font-weight: 600; color: #1a1a1a; margin: 0; @@ -78,12 +106,115 @@ gap: 12px; } -/* ͔@ */ -@media (max-width: 991px) { - .dataset-manager-container { - flex-direction: column; - } +/* 工具栏 */ +.document-list-toolbar { + display: flex; + justify-content: space-between; + align-items: center; + flex-shrink: 0; + margin-bottom: 16px; +} +.document-list-search { + width: 280px; +} + +.document-list-actions { + display: flex; + align-items: center; + gap: 12px; +} + +/* 表格容器 - 唯一滚动区域 */ +.document-table-container { + flex: 1; + min-height: 0; + overflow-y: auto; + overflow-x: hidden; +} + +.document-table { + background: transparent; +} + +.document-table .ant-table-wrapper, +.document-table .ant-table, +.document-table .ant-table-container { + background: transparent; +} + +/* 固定表头 */ +.document-table .ant-table-thead > tr > th { + background: #fafafa; + font-weight: 600; + font-size: 13px; + color: #1a1a1a; + padding: 8px 12px !important; + position: sticky; + top: 0; + z-index: 10; +} + +/* 压缩行高 */ +.document-table .ant-table-tbody > tr > td { + padding: 6px 12px !important; + font-size: 13px; +} + +.document-table .ant-table-tbody > tr:hover > td { + background: #f5f7f9; +} + +/* 固定底部分页器 */ +.document-pagination { + display: flex; + justify-content: space-between; + align-items: center; + padding: 12px 0; + border-top: 1px solid #f0f0f0; + flex-shrink: 0; + background: #fff; +} + +.pagination-total { + color: #666; + font-size: 14px; +} + +.pagination-controls { + display: flex; + align-items: center; + gap: 12px; +} + +.pagination-info { + color: #666; + font-size: 14px; +} + +/* 空状态 */ +.dataset-empty { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + flex: 1; + padding: 60px 40px; + color: #999; +} + +/* 加载状态 */ +.dataset-loading { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + flex: 1; + gap: 16px; +} + +/* 响应式 */ +@media (max-width: 991px) { .dataset-content { padding: 16px; } @@ -98,4 +229,28 @@ width: 100%; justify-content: flex-end; } + + .document-list-toolbar { + flex-direction: column; + align-items: stretch; + gap: 12px; + } + + .document-list-search { + width: 100%; + } + + .document-list-actions { + justify-content: space-between; + } +} + +@media (max-width: 576px) { + .dataset-content { + padding: 12px; + } + + .dataset-header h1 { + font-size: 16px; + } } diff --git a/app/styles/components/dify-dataset-manager/sidebar.css b/app/styles/components/dify-dataset-manager/sidebar.css deleted file mode 100644 index cbeb741..0000000 --- a/app/styles/components/dify-dataset-manager/sidebar.css +++ /dev/null @@ -1,135 +0,0 @@ -/** - * 知识库管理器 - 侧边栏样式 - */ - -.dataset-sidebar { - display: flex; - flex-direction: column; - height: 100%; - overflow: hidden; -} - -.dataset-sidebar .ant-layout-sider-children { - display: flex; - flex-direction: column; - height: 100%; -} - -/* 侧边栏头部 */ -.dataset-sidebar-header { - display: flex; - flex-direction: column; - padding: 12px; - border-bottom: 1px solid #f0f0f0; - gap: 12px; -} - -.dataset-sidebar-title { - display: flex; - align-items: center; - gap: 8px; -} - -.dataset-sidebar-title h3 { - font-size: 16px; - font-weight: 600; - color: #1a1a1a; - margin: 0; -} - -/* 知识库列表 */ -.dataset-sidebar-list { - flex: 1; - overflow-y: auto; - overflow-x: hidden; -} - -/* 菜单样式覆盖 */ -.dataset-sidebar-menu.ant-menu { - border: none; - background: transparent; -} - -.dataset-sidebar-menu .ant-menu-item { - margin: 4px 8px; - border-radius: 8px; - height: auto; - line-height: 1.5; - padding: 8px 12px !important; -} - -.dataset-sidebar-menu .ant-menu-item-selected { - background-color: rgba(0, 104, 74, 0.1) !important; -} - -.dataset-sidebar-menu .ant-menu-item-selected::after { - display: none; -} - -.dataset-sidebar-menu .ant-menu-item:hover { - background-color: #f5f5f5; -} - -/* 知识库信息 */ -.dataset-info { - display: flex; - flex-direction: column; - gap: 4px; - min-width: 0; -} - -.dataset-info-name { - font-size: 14px; - font-weight: 500; - color: #1a1a1a; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -} - -.dataset-info-meta { - display: flex; - align-items: center; - gap: 12px; -} - -.dataset-info-meta-item { - display: flex; - align-items: center; - gap: 4px; - font-size: 12px; - color: #999; -} - -.dataset-info-meta-item .anticon { - font-size: 12px; -} - -/* 侧边栏底部 */ -.dataset-sidebar-footer { - padding: 12px 16px; - border-top: 1px solid #f0f0f0; - background: #fafafa; -} - -.stats-text { - font-size: 12px; - color: #999; - text-align: center; -} - -/* 响应式布局 */ -@media (max-width: 991px) { - .dataset-sidebar { - position: fixed; - left: 0; - top: 0; - z-index: 1000; - height: 100vh; - box-shadow: 2px 0 8px rgba(0, 0, 0, 0.15); - } - - .dataset-sidebar.ant-layout-sider-collapsed { - transform: translateX(-100%); - } -}