From d85010bada1e3de842edcc9ce3cbc84125c4c4b4 Mon Sep 17 00:00:00 2001 From: PingChuan <1259732256@qq.com> Date: Sun, 30 Nov 2025 20:14:51 +0800 Subject: [PATCH 1/3] =?UTF-8?q?feat:=E5=AE=8C=E5=96=84=E7=9F=A5=E8=AF=86?= =?UTF-8?q?=E5=BA=93=E7=AE=A1=E7=90=86=E8=B7=AF=E7=94=B1=E6=9D=83=E9=99=90?= =?UTF-8?q?=E9=85=8D=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/components/layout/Sidebar.tsx | 3 +-- app/routes/_index.tsx | 2 +- ...-llm._index.tsx => chat-with-llm.chat.tsx} | 0 ....tsx => chat-with-llm.dataset-manager.tsx} | 0 app/routes/dataset-manager.tsx | 20 ------------------- 5 files changed, 2 insertions(+), 23 deletions(-) rename app/routes/{chat-with-llm._index.tsx => chat-with-llm.chat.tsx} (100%) rename app/routes/{dataset-manager._index.tsx => chat-with-llm.dataset-manager.tsx} (100%) delete mode 100644 app/routes/dataset-manager.tsx diff --git a/app/components/layout/Sidebar.tsx b/app/components/layout/Sidebar.tsx index 6123aac..78d8004 100644 --- a/app/components/layout/Sidebar.tsx +++ b/app/components/layout/Sidebar.tsx @@ -214,8 +214,7 @@ export function Sidebar({ onToggle, collapsed, userRole, frontendJWT = '' }: Sid // 🔑 如果选择了"智慧法务大模型",显示 /chat-with-llm 和 /dataset-manager 相关菜单 if (selectedModuleName === '智慧法务大模型') { - return item.path === '/chat-with-llm' || item.path?.startsWith('/chat-with-llm/') || - item.path === '/dataset-manager' || item.path?.startsWith('/dataset-manager/'); + return item.path === '/chat-with-llm' || item.path?.startsWith('/chat-with-llm/') } // 🔑 如果选择了包含"合同"的模块 diff --git a/app/routes/_index.tsx b/app/routes/_index.tsx index bf05f6a..3d83f7a 100644 --- a/app/routes/_index.tsx +++ b/app/routes/_index.tsx @@ -209,7 +209,7 @@ export default function Index() { // console.log('📌 [Index] 合同模块,跳转到:', targetPath); } else if (module.name === '智慧法务大模型') { // 智慧法务大模型 → 跳转到 AI 对话 - targetPath = '/chat-with-llm'; + targetPath = '/chat-with-llm/chat'; // console.log('📌 [Index] 智慧法务大模型,跳转到:', targetPath); } else { // console.log('📌 [Index] 其他模块,跳转到:', targetPath); diff --git a/app/routes/chat-with-llm._index.tsx b/app/routes/chat-with-llm.chat.tsx similarity index 100% rename from app/routes/chat-with-llm._index.tsx rename to app/routes/chat-with-llm.chat.tsx diff --git a/app/routes/dataset-manager._index.tsx b/app/routes/chat-with-llm.dataset-manager.tsx similarity index 100% rename from app/routes/dataset-manager._index.tsx rename to app/routes/chat-with-llm.dataset-manager.tsx diff --git a/app/routes/dataset-manager.tsx b/app/routes/dataset-manager.tsx deleted file mode 100644 index f6de4d7..0000000 --- a/app/routes/dataset-manager.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import { Outlet } from "@remix-run/react"; -import type { MetaFunction } from "@remix-run/node"; - -export const meta: MetaFunction = () => { - return [ - { title: "知识库管理 - 智能审核系统" }, - { name: "description", content: "Dify 知识库文档管理" }, - ]; -}; - -export const handle = { - breadcrumb: "知识库管理", -}; - -/** - * 知识库管理布局路由 - */ -export default function DatasetManagerLayout() { - return ; -} 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 2/3] =?UTF-8?q?feat:=E5=AE=8C=E6=88=90dify=E7=9F=A5?= =?UTF-8?q?=E8=AF=86=E5=BA=93=E6=96=87=E6=A1=A3=E5=9F=BA=E7=A1=80CRUD?= =?UTF-8?q?=E6=A8=A1=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%); - } -} From 0c1b81cfb201efe4c37f6d75fcdff18a9ca02c13 Mon Sep 17 00:00:00 2001 From: PingChuan <1259732256@qq.com> Date: Mon, 1 Dec 2025 12:33:53 +0800 Subject: [PATCH 3/3] =?UTF-8?q?temp:=E4=B8=B4=E6=97=B6=E5=A4=87=E4=BB=BD,?= =?UTF-8?q?=E5=AE=8C=E6=88=90=E4=B8=80=E5=8D=8A=E7=9F=A5=E8=AF=86=E5=BA=93?= =?UTF-8?q?=E7=AE=A1=E7=90=86=E6=A8=A1=E5=9D=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/api/dify-dataset/api/datasetApi.ts | 78 ++ .../{client.ts => api/documentApi.ts} | 239 +--- app/api/dify-dataset/api/index.ts | 38 + app/api/dify-dataset/api/segmentApi.ts | 359 +++++ app/api/dify-dataset/index.ts | 51 +- app/api/dify-dataset/type/commonTypes.ts | 13 + app/api/dify-dataset/type/datasetTypes.ts | 66 + app/api/dify-dataset/type/documentTypes.ts | 116 ++ app/api/dify-dataset/type/index.ts | 47 + app/api/dify-dataset/type/segmentTypes.ts | 191 +++ app/api/dify-dataset/types.ts | 250 ---- .../dify-dataset-manager/dataset-settings.tsx | 187 +++ .../dify-dataset-manager/document-detail.tsx | 367 +++++ .../dify-dataset-manager/document-list.tsx | 53 +- app/components/dify-dataset-manager/index.tsx | 123 +- .../dify-dataset-manager/layout.tsx | 109 ++ .../dify-dataset-manager/retrieve-test.tsx | 202 +++ ....$segmentId.child_chunks.$childChunkId.tsx | 93 ++ ...entId.segments.$segmentId.child_chunks.tsx | 126 ++ ...tasetId.documents.$documentId.segments.tsx | 64 +- ...i.dataset.datasets.$datasetId.retrieve.tsx | 87 ++ .../api.dataset.datasets.$datasetId.tsx | 46 +- app/routes/chat-with-llm.chat.tsx | 2 +- app/routes/chat-with-llm.dataset-manager.tsx | 10 +- .../components/dify-dataset-manager/index.css | 1207 ++++++++++++++++- 25 files changed, 3564 insertions(+), 560 deletions(-) create mode 100644 app/api/dify-dataset/api/datasetApi.ts rename app/api/dify-dataset/{client.ts => api/documentApi.ts} (54%) create mode 100644 app/api/dify-dataset/api/index.ts create mode 100644 app/api/dify-dataset/api/segmentApi.ts create mode 100644 app/api/dify-dataset/type/commonTypes.ts create mode 100644 app/api/dify-dataset/type/datasetTypes.ts create mode 100644 app/api/dify-dataset/type/documentTypes.ts create mode 100644 app/api/dify-dataset/type/index.ts create mode 100644 app/api/dify-dataset/type/segmentTypes.ts delete mode 100644 app/api/dify-dataset/types.ts create mode 100644 app/components/dify-dataset-manager/dataset-settings.tsx create mode 100644 app/components/dify-dataset-manager/document-detail.tsx create mode 100644 app/components/dify-dataset-manager/layout.tsx create mode 100644 app/components/dify-dataset-manager/retrieve-test.tsx create mode 100644 app/routes/api.dataset.datasets.$datasetId.documents.$documentId.segments.$segmentId.child_chunks.$childChunkId.tsx create mode 100644 app/routes/api.dataset.datasets.$datasetId.documents.$documentId.segments.$segmentId.child_chunks.tsx create mode 100644 app/routes/api.dataset.datasets.$datasetId.retrieve.tsx diff --git a/app/api/dify-dataset/api/datasetApi.ts b/app/api/dify-dataset/api/datasetApi.ts new file mode 100644 index 0000000..036a788 --- /dev/null +++ b/app/api/dify-dataset/api/datasetApi.ts @@ -0,0 +1,78 @@ +/** + * Dify Dataset 知识库 API 模块 + * + * 提供浏览器端调用 Dify 知识库管理 API 的函数 + * + * @module api/dify-dataset/api/datasetApi + */ + +import axios from 'axios'; +import type { Dataset, DatasetsResponse } from '../type'; + +/** + * API 基础 URL + */ +const API_URL = '/api/dataset'; + +/** + * 获取知识库列表 + * + * @param page - 页码,默认 1 + * @param limit - 每页数量,默认 20 + * @returns 知识库列表响应 + */ +export async function fetchDatasets( + page: number = 1, + limit: number = 20 +): Promise { + const params = new URLSearchParams({ + page: page.toString(), + limit: limit.toString(), + }); + + const response = await axios.get( + `${API_URL}/datasets?${params}`, + { withCredentials: true } + ); + return response.data; +} + +/** + * 获取单个知识库详情 + * + * @param datasetId - 知识库 ID + * @returns 知识库详情 + */ +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 name - 新的知识库名称 + * @returns 更新后的知识库详情 + */ +export async function updateDatasetName( + datasetId: string, + name: string +): Promise { + console.log('[Dataset Client] 更新知识库名称:', { datasetId, name }); + + const response = await axios.patch( + `${API_URL}/datasets/${datasetId}`, + { name }, + { + headers: { 'Content-Type': 'application/json' }, + withCredentials: true, + } + ); + return response.data; +} diff --git a/app/api/dify-dataset/client.ts b/app/api/dify-dataset/api/documentApi.ts similarity index 54% rename from app/api/dify-dataset/client.ts rename to app/api/dify-dataset/api/documentApi.ts index ce0d09e..d147edf 100644 --- a/app/api/dify-dataset/client.ts +++ b/app/api/dify-dataset/api/documentApi.ts @@ -1,104 +1,25 @@ /** - * Dify Dataset 客户端 API 模块 + * Dify Dataset 文档 API 模块 * - * 提供浏览器端调用 Dify 知识库 API 的函数 - * 通过 Remix API Routes 代理请求 + * 提供浏览器端调用 Dify 文档管理 API 的函数 * - * @module api/dify-dataset/client + * @module api/dify-dataset/api/documentApi */ import axios from 'axios'; import type { - Dataset, - DatasetsResponse, - DocumentsResponse, - SegmentsResponse, Document, - OperationResult, + DocumentsResponse, IndexingStatusResponse, UploadFileInfo, - UpdateDatasetRequest, -} from './types'; - -// ============================================================================ -// 基础配置 -// ============================================================================ + OperationResult, +} from '../type'; /** * API 基础 URL - * 指向 Remix API Routes(/api/dataset/*) */ const API_URL = '/api/dataset'; -// ============================================================================ -// 知识库 API -// ============================================================================ - -/** - * 获取知识库列表 - * - * @param page - 页码,默认 1 - * @param limit - 每页数量,默认 20 - * @returns 知识库列表响应 - */ -export async function fetchDatasets( - page: number = 1, - limit: number = 20 -): Promise { - const params = new URLSearchParams({ - page: page.toString(), - limit: limit.toString(), - }); - - const response = await axios.get( - `${API_URL}/datasets?${params}`, - { withCredentials: true } - ); - return response.data; -} - -/** - * 获取单个知识库详情 - * - * @param datasetId - 知识库 ID - * @returns 知识库详情 - */ -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 -// ============================================================================ - /** * 获取知识库文档列表 * @@ -199,101 +120,6 @@ export async function toggleDocumentStatus( return response.data; } -// ============================================================================ -// 文档分段 API -// ============================================================================ - -/** - * 获取文档分段列表 - * - * @param datasetId - 知识库 ID - * @param documentId - 文档 ID - * @param page - 页码,默认 1 - * @param limit - 每页数量,默认 20 - * @param keyword - 搜索关键词 - * @returns 分段列表响应 - */ -export async function fetchSegments( - datasetId: string, - documentId: string, - page: number = 1, - limit: number = 20, - keyword?: string -): Promise { - const params = new URLSearchParams({ - page: page.toString(), - limit: limit.toString(), - }); - - if (keyword) { - params.append('keyword', keyword); - } - - console.log('[Dataset Client] 获取分段列表:', { datasetId, documentId, page, limit }); - - const response = await axios.get( - `${API_URL}/datasets/${datasetId}/documents/${documentId}/segments?${params}`, - { withCredentials: true } - ); - return response.data; -} - -/** - * 删除分段 - * - * @param datasetId - 知识库 ID - * @param documentId - 文档 ID - * @param segmentId - 分段 ID - * @returns 操作结果 - */ -export async function deleteSegment( - datasetId: string, - documentId: string, - segmentId: string -): Promise { - console.log('[Dataset Client] 删除分段:', { datasetId, documentId, segmentId }); - - const response = await axios.delete( - `${API_URL}/datasets/${datasetId}/documents/${documentId}/segments/${segmentId}`, - { withCredentials: true } - ); - return response.data; -} - -/** - * 启用/禁用分段 - * Dify API: POST /datasets/{dataset_id}/documents/{document_id}/segments/{segment_id} - * 通过更新分段的方式来切换状态 - * - * @param datasetId - 知识库 ID - * @param documentId - 文档 ID - * @param segmentId - 分段 ID - * @param enabled - 是否启用 - * @returns 操作结果 - */ -export async function toggleSegmentStatus( - datasetId: string, - documentId: string, - segmentId: string, - enabled: boolean -): Promise { - 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, - } - ); - return response.data; -} - -// ============================================================================ -// 文件上传 API -// ============================================================================ - /** * 上传文件到知识库 * @@ -373,3 +199,56 @@ export async function fetchUploadFileInfo( ); return response.data; } + +/** + * 文档处理规则配置 + */ +export interface ProcessRule { + mode: 'automatic' | 'custom'; + rules?: { + pre_processing_rules?: Array<{ + id: 'remove_extra_spaces' | 'remove_urls_emails'; + enabled: boolean; + }>; + segmentation?: { + separator: string; + max_tokens: number; + }; + }; +} + +/** + * 更新文档设置参数 + */ +export interface UpdateDocumentSettings { + indexing_technique?: 'high_quality' | 'economy'; + process_rule?: ProcessRule; +} + +/** + * 更新文档设置并重新处理 + * 注意:Dify API 不直接支持修改已有文档的分段设置 + * 此函数尝试通过更新接口应用新设置 + * + * @param datasetId - 知识库 ID + * @param documentId - 文档 ID + * @param settings - 更新设置 + * @returns 操作结果 + */ +export async function updateDocumentWithSettings( + datasetId: string, + documentId: string, + settings: UpdateDocumentSettings +): Promise { + console.log('[Dataset Client] 更新文档设置:', { datasetId, documentId, settings }); + + const response = await axios.post( + `${API_URL}/datasets/${datasetId}/documents/${documentId}/update-settings`, + settings, + { + headers: { 'Content-Type': 'application/json' }, + withCredentials: true, + } + ); + return response.data; +} diff --git a/app/api/dify-dataset/api/index.ts b/app/api/dify-dataset/api/index.ts new file mode 100644 index 0000000..3e69d9e --- /dev/null +++ b/app/api/dify-dataset/api/index.ts @@ -0,0 +1,38 @@ +/** + * Dify Dataset API 客户端统一导出 + * + * @module api/dify-dataset/api + */ + +// 知识库 API +export { + fetchDatasets, + fetchDataset, + updateDatasetName, +} from './datasetApi'; + +// 文档 API +export { + fetchDocuments, + fetchDocument, + deleteDocument, + toggleDocumentStatus, + uploadDocument, + fetchIndexingStatus, + fetchUploadFileInfo, +} from './documentApi'; + +// 分段、子分段、检索 API +export { + fetchSegments, + fetchSegment, + createSegments, + updateSegment, + deleteSegment, + toggleSegmentStatus, + fetchChildChunks, + createChildChunk, + updateChildChunk, + deleteChildChunk, + retrieveDataset, +} from './segmentApi'; diff --git a/app/api/dify-dataset/api/segmentApi.ts b/app/api/dify-dataset/api/segmentApi.ts new file mode 100644 index 0000000..0761e06 --- /dev/null +++ b/app/api/dify-dataset/api/segmentApi.ts @@ -0,0 +1,359 @@ +/** + * Dify Dataset 分段、子分段、检索 API 模块 + * + * 提供浏览器端调用 Dify 分段管理和检索 API 的函数 + * + * @module api/dify-dataset/api/segmentApi + */ + +import axios from 'axios'; +import type { + Segment, + SegmentsResponse, + CreateSegmentRequest, + UpdateSegmentRequest, + CreateSegmentsResponse, + ChildChunk, + ChildChunksResponse, + CreateChildChunkResponse, + RetrieveRequest, + RetrieveResponse, + OperationResult, +} from '../type'; + +/** + * API 基础 URL + */ +const API_URL = '/api/dataset'; + +// ============================================================================ +// 分段 API +// ============================================================================ + +/** + * 获取文档分段列表 + * + * @param datasetId - 知识库 ID + * @param documentId - 文档 ID + * @param page - 页码,默认 1 + * @param limit - 每页数量,默认 20 + * @param keyword - 搜索关键词 + * @returns 分段列表响应 + */ +export async function fetchSegments( + datasetId: string, + documentId: string, + page: number = 1, + limit: number = 20, + keyword?: string +): Promise { + const params = new URLSearchParams({ + page: page.toString(), + limit: limit.toString(), + }); + + if (keyword) { + params.append('keyword', keyword); + } + + console.log('[Dataset Client] 获取分段列表:', { datasetId, documentId, page, limit }); + + const response = await axios.get( + `${API_URL}/datasets/${datasetId}/documents/${documentId}/segments?${params}`, + { withCredentials: true } + ); + return response.data; +} + +/** + * 获取单个分段详情 + * + * @param datasetId - 知识库 ID + * @param documentId - 文档 ID + * @param segmentId - 分段 ID + * @returns 分段详情 + */ +export async function fetchSegment( + datasetId: string, + documentId: string, + segmentId: string +): Promise { + console.log('[Dataset Client] 获取分段详情:', { datasetId, documentId, segmentId }); + + const response = await axios.get<{ data: Segment }>( + `${API_URL}/datasets/${datasetId}/documents/${documentId}/segments/${segmentId}`, + { withCredentials: true } + ); + return response.data.data; +} + +/** + * 新增分段(批量) + * Dify API: POST /datasets/{dataset_id}/documents/{document_id}/segments + * + * @param datasetId - 知识库 ID + * @param documentId - 文档 ID + * @param segments - 分段列表 + * @returns 创建的分段列表 + */ +export async function createSegments( + datasetId: string, + documentId: string, + segments: CreateSegmentRequest[] +): Promise { + console.log('[Dataset Client] 新增分段:', { datasetId, documentId, count: segments.length }); + + const response = await axios.post( + `${API_URL}/datasets/${datasetId}/documents/${documentId}/segments`, + { segments }, + { + headers: { 'Content-Type': 'application/json' }, + withCredentials: true, + } + ); + return response.data; +} + +/** + * 更新分段内容 + * Dify API: POST /datasets/{dataset_id}/documents/{document_id}/segments/{segment_id} + * + * @param datasetId - 知识库 ID + * @param documentId - 文档 ID + * @param segmentId - 分段 ID + * @param segment - 更新内容 + * @returns 更新后的分段 + */ +export async function updateSegment( + datasetId: string, + documentId: string, + segmentId: string, + segment: UpdateSegmentRequest +): Promise<{ data: Segment; doc_form: string }> { + console.log('[Dataset Client] 更新分段:', { datasetId, documentId, segmentId, segment }); + + const response = await axios.post<{ data: Segment; doc_form: string }>( + `${API_URL}/datasets/${datasetId}/documents/${documentId}/segments/${segmentId}`, + { segment }, + { + headers: { 'Content-Type': 'application/json' }, + withCredentials: true, + } + ); + return response.data; +} + +/** + * 删除分段 + * + * @param datasetId - 知识库 ID + * @param documentId - 文档 ID + * @param segmentId - 分段 ID + * @returns 操作结果 + */ +export async function deleteSegment( + datasetId: string, + documentId: string, + segmentId: string +): Promise { + console.log('[Dataset Client] 删除分段:', { datasetId, documentId, segmentId }); + + const response = await axios.delete( + `${API_URL}/datasets/${datasetId}/documents/${documentId}/segments/${segmentId}`, + { withCredentials: true } + ); + return response.data; +} + +/** + * 启用/禁用分段 + * Dify API: POST /datasets/{dataset_id}/documents/{document_id}/segments/{segment_id} + * 通过更新分段的方式来切换状态 + * + * @param datasetId - 知识库 ID + * @param documentId - 文档 ID + * @param segmentId - 分段 ID + * @param enabled - 是否启用 + * @returns 操作结果 + */ +export async function toggleSegmentStatus( + datasetId: string, + documentId: string, + segmentId: string, + enabled: boolean +): Promise { + 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, + } + ); + return response.data; +} + +// ============================================================================ +// 子分段 API(父子模式) +// ============================================================================ + +/** + * 获取子分段列表 + * Dify API: GET /datasets/{dataset_id}/documents/{document_id}/segments/{segment_id}/child_chunks + * + * @param datasetId - 知识库 ID + * @param documentId - 文档 ID + * @param segmentId - 分段 ID + * @param page - 页码 + * @param limit - 每页数量 + * @param keyword - 搜索关键词 + * @returns 子分段列表响应 + */ +export async function fetchChildChunks( + datasetId: string, + documentId: string, + segmentId: string, + page: number = 1, + limit: number = 20, + keyword?: string +): Promise { + const params = new URLSearchParams({ + page: page.toString(), + limit: limit.toString(), + }); + + if (keyword) { + params.append('keyword', keyword); + } + + console.log('[Dataset Client] 获取子分段列表:', { datasetId, documentId, segmentId, page, limit }); + + const response = await axios.get( + `${API_URL}/datasets/${datasetId}/documents/${documentId}/segments/${segmentId}/child_chunks?${params}`, + { withCredentials: true } + ); + return response.data; +} + +/** + * 新增子分段 + * Dify API: POST /datasets/{dataset_id}/documents/{document_id}/segments/{segment_id}/child_chunks + * + * @param datasetId - 知识库 ID + * @param documentId - 文档 ID + * @param segmentId - 分段 ID + * @param content - 子分段内容 + * @returns 创建的子分段 + */ +export async function createChildChunk( + datasetId: string, + documentId: string, + segmentId: string, + content: string +): Promise { + console.log('[Dataset Client] 新增子分段:', { datasetId, documentId, segmentId }); + + const response = await axios.post( + `${API_URL}/datasets/${datasetId}/documents/${documentId}/segments/${segmentId}/child_chunks`, + { content }, + { + headers: { 'Content-Type': 'application/json' }, + withCredentials: true, + } + ); + return response.data; +} + +/** + * 更新子分段 + * Dify API: PATCH /datasets/{dataset_id}/documents/{document_id}/segments/{segment_id}/child_chunks/{child_chunk_id} + * + * @param datasetId - 知识库 ID + * @param documentId - 文档 ID + * @param segmentId - 分段 ID + * @param childChunkId - 子分段 ID + * @param content - 更新内容 + * @returns 更新后的子分段 + */ +export async function updateChildChunk( + datasetId: string, + documentId: string, + segmentId: string, + childChunkId: string, + content: string +): Promise { + console.log('[Dataset Client] 更新子分段:', { datasetId, documentId, segmentId, childChunkId }); + + const response = await axios.patch<{ data: ChildChunk }>( + `${API_URL}/datasets/${datasetId}/documents/${documentId}/segments/${segmentId}/child_chunks/${childChunkId}`, + { content }, + { + headers: { 'Content-Type': 'application/json' }, + withCredentials: true, + } + ); + return response.data.data; +} + +/** + * 删除子分段 + * Dify API: DELETE /datasets/{dataset_id}/documents/{document_id}/segments/{segment_id}/child_chunks/{child_chunk_id} + * + * @param datasetId - 知识库 ID + * @param documentId - 文档 ID + * @param segmentId - 分段 ID + * @param childChunkId - 子分段 ID + * @returns 操作结果 + */ +export async function deleteChildChunk( + datasetId: string, + documentId: string, + segmentId: string, + childChunkId: string +): Promise { + console.log('[Dataset Client] 删除子分段:', { datasetId, documentId, segmentId, childChunkId }); + + const response = await axios.delete( + `${API_URL}/datasets/${datasetId}/documents/${documentId}/segments/${segmentId}/child_chunks/${childChunkId}`, + { withCredentials: true } + ); + return response.data; +} + +// ============================================================================ +// 检索 API +// ============================================================================ + +/** + * 检索知识库 + * Dify API: POST /datasets/{dataset_id}/retrieve + * + * @param datasetId - 知识库 ID + * @param query - 检索关键词 + * @param retrievalModel - 检索模型配置 + * @returns 检索结果 + */ +export async function retrieveDataset( + datasetId: string, + query: string, + retrievalModel?: RetrieveRequest['retrieval_model'] +): Promise { + console.log('[Dataset Client] 检索知识库:', { datasetId, query }); + + const requestBody: RetrieveRequest = { + query, + retrieval_model: retrievalModel, + }; + + const response = await axios.post( + `${API_URL}/datasets/${datasetId}/retrieve`, + requestBody, + { + headers: { 'Content-Type': 'application/json' }, + withCredentials: true, + } + ); + return response.data; +} diff --git a/app/api/dify-dataset/index.ts b/app/api/dify-dataset/index.ts index 1d1842a..136fe4e 100644 --- a/app/api/dify-dataset/index.ts +++ b/app/api/dify-dataset/index.ts @@ -1,47 +1,16 @@ /** - * Dify Dataset API 模块统一导出 + * Dify Dataset API 模块 + * + * 推荐直接从子包导入: + * - 类型:import type { ... } from '~/api/dify-dataset/type' + * - API:import { ... } from '~/api/dify-dataset/api' + * - 服务端:import { ... } from '~/api/dify-dataset/client.server' * * @module api/dify-dataset */ -// 类型导出 -export type { - Dataset, - DatasetsResponse, - Document, - DocumentsResponse, - Segment, - SegmentsResponse, - IndexingStatus, - OperationResult, - CreateDocumentResponse, - UploadProgress, - DocumentIndexingStatus, - IndexingStatusResponse, - UploadFileInfo, - RetrievalModel, - UpdateDatasetRequest, -} from './types'; +// 类型子包重新导出 +export * from './type'; -// 客户端 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'; +// API 子包重新导出 +export * from './api'; diff --git a/app/api/dify-dataset/type/commonTypes.ts b/app/api/dify-dataset/type/commonTypes.ts new file mode 100644 index 0000000..18dc2ee --- /dev/null +++ b/app/api/dify-dataset/type/commonTypes.ts @@ -0,0 +1,13 @@ +/** + * Dify Dataset API 通用类型定义 + * + * @module api/dify-dataset/type/commonTypes + */ + +/** + * 通用操作结果 + */ +export interface OperationResult { + result: 'success' | 'error'; + message?: string; +} diff --git a/app/api/dify-dataset/type/datasetTypes.ts b/app/api/dify-dataset/type/datasetTypes.ts new file mode 100644 index 0000000..65dd4ee --- /dev/null +++ b/app/api/dify-dataset/type/datasetTypes.ts @@ -0,0 +1,66 @@ +/** + * Dify Dataset API 知识库类型定义 + * + * @module api/dify-dataset/type/datasetTypes + */ + +/** + * 知识库信息 + */ +export interface Dataset { + id: string; + name: string; + description: string; + permission: 'only_me' | 'all_team_members'; + data_source_type: 'upload_file' | 'notion_import' | 'website_crawl'; + indexing_technique: 'high_quality' | 'economy'; + app_count: number; + document_count: number; + word_count: number; + created_by: string; + created_at: number; + updated_by: string; + updated_at: number; +} + +/** + * 知识库列表响应 + */ +export interface DatasetsResponse { + data: Dataset[]; + has_more: boolean; + limit: number; + total: number; + page: 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/api/dify-dataset/type/documentTypes.ts b/app/api/dify-dataset/type/documentTypes.ts new file mode 100644 index 0000000..39b5e60 --- /dev/null +++ b/app/api/dify-dataset/type/documentTypes.ts @@ -0,0 +1,116 @@ +/** + * Dify Dataset API 文档类型定义 + * + * @module api/dify-dataset/type/documentTypes + */ + +/** + * 文档索引状态 + */ +export type IndexingStatus = + | 'waiting' + | 'parsing' + | 'cleaning' + | 'splitting' + | 'indexing' + | 'paused' + | 'error' + | 'completed'; + +/** + * 文档信息 + */ +export interface Document { + id: string; + position: number; + data_source_type: 'upload_file' | 'notion_import' | 'website_crawl'; + data_source_info: { + upload_file_id?: string; + notion_page_id?: string; + website_url?: string; + }; + dataset_process_rule_id: string; + name: string; + created_from: string; + created_by: string; + created_at: number; + tokens: number; + indexing_status: IndexingStatus; + error?: string; + enabled: boolean; + disabled_at?: number; + disabled_by?: string; + archived: boolean; + display_status: string; + word_count: number; + hit_count: number; + doc_form: string; +} + +/** + * 文档列表响应 + */ +export interface DocumentsResponse { + data: Document[]; + has_more: boolean; + limit: number; + total: number; + page: number; +} + +/** + * 创建文档响应 + */ +export interface CreateDocumentResponse { + document: Document; + batch: string; +} + +/** + * 文档上传进度 + */ +export interface UploadProgress { + loaded: number; + 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; +} diff --git a/app/api/dify-dataset/type/index.ts b/app/api/dify-dataset/type/index.ts new file mode 100644 index 0000000..78d558e --- /dev/null +++ b/app/api/dify-dataset/type/index.ts @@ -0,0 +1,47 @@ +/** + * Dify Dataset API 类型定义统一导出 + * + * @module api/dify-dataset/type + */ + +// 通用类型 +export type { OperationResult } from './commonTypes'; + +// 知识库类型 +export type { + Dataset, + DatasetsResponse, + RetrievalModel, + UpdateDatasetRequest, +} from './datasetTypes'; + +// 文档类型 +export type { + IndexingStatus, + Document, + DocumentsResponse, + CreateDocumentResponse, + UploadProgress, + DocumentIndexingStatus, + IndexingStatusResponse, + UploadFileInfo, +} from './documentTypes'; + +// 分段、子分段、检索类型 +export type { + Segment, + SegmentsResponse, + CreateSegmentRequest, + UpdateSegmentRequest, + CreateSegmentsResponse, + ChildChunk, + ChildChunksResponse, + CreateChildChunkRequest, + UpdateChildChunkRequest, + CreateChildChunkResponse, + MetadataFilterCondition, + MetadataFilteringConditions, + RetrieveRequest, + RetrieveRecord, + RetrieveResponse, +} from './segmentTypes'; diff --git a/app/api/dify-dataset/type/segmentTypes.ts b/app/api/dify-dataset/type/segmentTypes.ts new file mode 100644 index 0000000..a976acd --- /dev/null +++ b/app/api/dify-dataset/type/segmentTypes.ts @@ -0,0 +1,191 @@ +/** + * Dify Dataset API 分段、子分段、检索类型定义 + * + * @module api/dify-dataset/type/segmentTypes + */ + +// ============================================================================ +// 分段类型 +// ============================================================================ + +/** + * 文档分段 + */ +export interface Segment { + id: string; + position: number; + document_id: string; + content: string; + answer?: string; + word_count: number; + tokens: number; + keywords: string[]; + index_node_id: string; + index_node_hash: string; + hit_count: number; + enabled: boolean; + disabled_at?: number; + disabled_by?: string; + status: 'waiting' | 'completed' | 'error' | 'indexing'; + created_by: string; + created_at: number; + indexing_at?: number; + completed_at?: number; + error?: string; + stopped_at?: number; +} + +/** + * 分段列表响应 + */ +export interface SegmentsResponse { + data: Segment[]; + has_more: boolean; + limit: number; + total: number; +} + +/** + * 创建分段请求参数 + */ +export interface CreateSegmentRequest { + content: string; + answer?: string; + keywords?: string[]; +} + +/** + * 更新分段请求参数 + */ +export interface UpdateSegmentRequest { + content?: string; + answer?: string; + keywords?: string[]; + enabled?: boolean; +} + +/** + * 创建分段响应 + */ +export interface CreateSegmentsResponse { + data: Segment[]; + doc_form: string; +} + +// ============================================================================ +// 子分段类型(父子模式) +// ============================================================================ + +/** + * 子分段信息 + */ +export interface ChildChunk { + id: string; + segment_id: string; + content: string; + word_count: number; + tokens: number; + index_node_id: string; + index_node_hash: string; + hit_count: number; + created_by: string; + created_at: number; + updated_by?: string; + updated_at?: number; + status: 'waiting' | 'completed' | 'error' | 'indexing'; + error?: string; +} + +/** + * 子分段列表响应 + */ +export interface ChildChunksResponse { + data: ChildChunk[]; + has_more: boolean; + limit: number; + total: number; + page: number; +} + +/** + * 创建子分段请求参数 + */ +export interface CreateChildChunkRequest { + content: string; +} + +/** + * 更新子分段请求参数 + */ +export interface UpdateChildChunkRequest { + content: string; +} + +/** + * 创建子分段响应 + */ +export interface CreateChildChunkResponse { + data: ChildChunk; +} + +// ============================================================================ +// 检索功能类型 +// ============================================================================ + +/** + * 元数据过滤条件 + */ +export interface MetadataFilterCondition { + name: string; + comparison_operator: 'contains' | 'not contains' | 'start with' | 'end with' | 'is' | 'is not' | 'empty' | 'not empty' | 'before' | 'after' | '=' | '!=' | '>' | '<' | '>=' | '<='; + value?: string | number; +} + +/** + * 元数据过滤条件组 + */ +export interface MetadataFilteringConditions { + logical_operator: 'and' | 'or'; + conditions: MetadataFilterCondition[]; +} + +/** + * 检索请求参数 + */ +export interface RetrieveRequest { + query: string; + retrieval_model?: { + search_method: 'keyword_search' | 'semantic_search' | 'full_text_search' | 'hybrid_search'; + reranking_enable?: boolean; + reranking_model?: { + reranking_provider_name: string; + reranking_model_name: string; + }; + top_k?: number; + score_threshold_enabled?: boolean; + score_threshold?: number; + }; + metadata_filtering_conditions?: MetadataFilteringConditions; +} + +/** + * 检索结果记录 + */ +export interface RetrieveRecord { + segment: Segment; + score: number; + tsne_position?: { + x: number; + y: number; + }; +} + +/** + * 检索响应 + */ +export interface RetrieveResponse { + query: { + content: string; + }; + records: RetrieveRecord[]; +} diff --git a/app/api/dify-dataset/types.ts b/app/api/dify-dataset/types.ts deleted file mode 100644 index 18cbbf9..0000000 --- a/app/api/dify-dataset/types.ts +++ /dev/null @@ -1,250 +0,0 @@ -/** - * Dify Dataset API 类型定义 - * - * @module api/dify-dataset/types - */ - -// ============================================================================ -// 知识库类型 -// ============================================================================ - -/** - * 知识库信息 - */ -export interface Dataset { - id: string; - name: string; - description: string; - permission: 'only_me' | 'all_team_members'; - data_source_type: 'upload_file' | 'notion_import' | 'website_crawl'; - indexing_technique: 'high_quality' | 'economy'; - app_count: number; - document_count: number; - word_count: number; - created_by: string; - created_at: number; - updated_by: string; - updated_at: number; -} - -/** - * 知识库列表响应 - */ -export interface DatasetsResponse { - data: Dataset[]; - has_more: boolean; - limit: number; - total: number; - page: number; -} - -// ============================================================================ -// 文档类型 -// ============================================================================ - -/** - * 文档索引状态 - */ -export type IndexingStatus = - | 'waiting' - | 'parsing' - | 'cleaning' - | 'splitting' - | 'indexing' - | 'paused' - | 'error' - | 'completed'; - -/** - * 文档信息 - */ -export interface Document { - id: string; - position: number; - data_source_type: 'upload_file' | 'notion_import' | 'website_crawl'; - data_source_info: { - upload_file_id?: string; - notion_page_id?: string; - website_url?: string; - }; - dataset_process_rule_id: string; - name: string; - created_from: string; - created_by: string; - created_at: number; - tokens: number; - indexing_status: IndexingStatus; - error?: string; - enabled: boolean; - disabled_at?: number; - disabled_by?: string; - archived: boolean; - display_status: string; - word_count: number; - hit_count: number; - doc_form: string; -} - -/** - * 文档列表响应 - */ -export interface DocumentsResponse { - data: Document[]; - has_more: boolean; - limit: number; - total: number; - page: number; -} - -// ============================================================================ -// 文档分段类型 -// ============================================================================ - -/** - * 文档分段 - */ -export interface Segment { - id: string; - position: number; - document_id: string; - content: string; - answer?: string; - word_count: number; - tokens: number; - keywords: string[]; - index_node_id: string; - index_node_hash: string; - hit_count: number; - enabled: boolean; - disabled_at?: number; - disabled_by?: string; - status: 'waiting' | 'completed' | 'error' | 'indexing'; - created_by: string; - created_at: number; - indexing_at?: number; - completed_at?: number; - error?: string; - stopped_at?: number; -} - -/** - * 分段列表响应 - */ -export interface SegmentsResponse { - data: Segment[]; - has_more: boolean; - limit: number; - total: number; -} - -// ============================================================================ -// 操作响应类型 -// ============================================================================ - -/** - * 通用操作结果 - */ -export interface OperationResult { - result: 'success' | 'error'; - message?: string; -} - -/** - * 创建文档响应 - */ -export interface CreateDocumentResponse { - document: Document; - batch: string; -} - -/** - * 文档上传进度 - */ -export interface UploadProgress { - loaded: number; - 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/dataset-settings.tsx b/app/components/dify-dataset-manager/dataset-settings.tsx new file mode 100644 index 0000000..333dfde --- /dev/null +++ b/app/components/dify-dataset-manager/dataset-settings.tsx @@ -0,0 +1,187 @@ +import { useState, useEffect } from 'react'; +import { Form, Input, Button, Card, message, Spin } from 'antd'; +import { SaveOutlined } from '@ant-design/icons'; +import type { Dataset } from '~/api/dify-dataset/type/datasetTypes'; +import { updateDatasetName } from '~/api/dify-dataset/api/datasetApi'; + +const { TextArea } = Input; + +interface DatasetSettingsProps { + dataset: Dataset | null; + onDatasetUpdated: (dataset: Dataset) => void; +} + +/** + * 知识库设置组件 + * 用于修改知识库名称和描述 + */ +export default function DatasetSettings({ + dataset, + onDatasetUpdated, +}: DatasetSettingsProps) { + const [form] = Form.useForm(); + const [saving, setSaving] = useState(false); + const [hasChanges, setHasChanges] = useState(false); + + // 初始化表单数据 + useEffect(() => { + if (dataset) { + form.setFieldsValue({ + name: dataset.name, + description: dataset.description || '', + }); + setHasChanges(false); + } + }, [dataset, form]); + + /** + * 处理表单值变化 + */ + const handleValuesChange = () => { + const values = form.getFieldsValue(); + const changed = + values.name !== dataset?.name || + values.description !== (dataset?.description || ''); + setHasChanges(changed); + }; + + /** + * 保存设置 + */ + const handleSave = async () => { + if (!dataset) { + message.error('知识库不存在'); + return; + } + + try { + const values = await form.validateFields(); + setSaving(true); + + // 目前只支持修改名称 + const updatedDataset = await updateDatasetName(dataset.id, values.name); + + message.success('保存成功'); + onDatasetUpdated(updatedDataset); + setHasChanges(false); + } catch (err: any) { + console.error('保存设置失败:', err); + message.error(err.message || '保存失败'); + } finally { + setSaving(false); + } + }; + + /** + * 重置表单 + */ + const handleReset = () => { + if (dataset) { + form.setFieldsValue({ + name: dataset.name, + description: dataset.description || '', + }); + setHasChanges(false); + } + }; + + if (!dataset) { + return ( +
+ +
+ ); + } + + return ( +
+ {/* 页面标题 */} +
+

设置

+

+ 管理知识库的基本信息 +

+
+ + {/* 设置表单 */} + +
+ + + + + +