temp:临时备份,完成一半知识库管理模块
This commit is contained in:
@@ -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<DatasetsResponse> {
|
||||
const params = new URLSearchParams({
|
||||
page: page.toString(),
|
||||
limit: limit.toString(),
|
||||
});
|
||||
|
||||
const response = await axios.get<DatasetsResponse>(
|
||||
`${API_URL}/datasets?${params}`,
|
||||
{ withCredentials: true }
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取单个知识库详情
|
||||
*
|
||||
* @param datasetId - 知识库 ID
|
||||
* @returns 知识库详情
|
||||
*/
|
||||
export async function fetchDataset(datasetId: string): Promise<Dataset> {
|
||||
const response = await axios.get<Dataset>(
|
||||
`${API_URL}/datasets/${datasetId}`,
|
||||
{ withCredentials: true }
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新知识库名称
|
||||
*
|
||||
* 注意:仅允许修改知识库名称,其他字段不开放修改
|
||||
*
|
||||
* @param datasetId - 知识库 ID
|
||||
* @param name - 新的知识库名称
|
||||
* @returns 更新后的知识库详情
|
||||
*/
|
||||
export async function updateDatasetName(
|
||||
datasetId: string,
|
||||
name: string
|
||||
): Promise<Dataset> {
|
||||
console.log('[Dataset Client] 更新知识库名称:', { datasetId, name });
|
||||
|
||||
const response = await axios.patch<Dataset>(
|
||||
`${API_URL}/datasets/${datasetId}`,
|
||||
{ name },
|
||||
{
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
withCredentials: true,
|
||||
}
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
@@ -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<DatasetsResponse> {
|
||||
const params = new URLSearchParams({
|
||||
page: page.toString(),
|
||||
limit: limit.toString(),
|
||||
});
|
||||
|
||||
const response = await axios.get<DatasetsResponse>(
|
||||
`${API_URL}/datasets?${params}`,
|
||||
{ withCredentials: true }
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取单个知识库详情
|
||||
*
|
||||
* @param datasetId - 知识库 ID
|
||||
* @returns 知识库详情
|
||||
*/
|
||||
export async function fetchDataset(datasetId: string): Promise<Dataset> {
|
||||
const response = await axios.get<Dataset>(
|
||||
`${API_URL}/datasets/${datasetId}`,
|
||||
{ withCredentials: true }
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新知识库详情
|
||||
*
|
||||
* @param datasetId - 知识库 ID
|
||||
* @param data - 更新数据
|
||||
* @returns 更新后的知识库详情
|
||||
*/
|
||||
export async function updateDataset(
|
||||
datasetId: string,
|
||||
data: UpdateDatasetRequest
|
||||
): Promise<Dataset> {
|
||||
console.log('[Dataset Client] 更新知识库:', { datasetId, data });
|
||||
|
||||
const response = await axios.patch<Dataset>(
|
||||
`${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<SegmentsResponse> {
|
||||
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<SegmentsResponse>(
|
||||
`${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<OperationResult> {
|
||||
console.log('[Dataset Client] 删除分段:', { datasetId, documentId, segmentId });
|
||||
|
||||
const response = await axios.delete<OperationResult>(
|
||||
`${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<OperationResult> {
|
||||
console.log('[Dataset Client] 切换分段状态:', { datasetId, documentId, segmentId, enabled });
|
||||
|
||||
const response = await axios.post<OperationResult>(
|
||||
`${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<OperationResult> {
|
||||
console.log('[Dataset Client] 更新文档设置:', { datasetId, documentId, settings });
|
||||
|
||||
const response = await axios.post<OperationResult>(
|
||||
`${API_URL}/datasets/${datasetId}/documents/${documentId}/update-settings`,
|
||||
settings,
|
||||
{
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
withCredentials: true,
|
||||
}
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
@@ -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';
|
||||
@@ -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<SegmentsResponse> {
|
||||
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<SegmentsResponse>(
|
||||
`${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<Segment> {
|
||||
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<CreateSegmentsResponse> {
|
||||
console.log('[Dataset Client] 新增分段:', { datasetId, documentId, count: segments.length });
|
||||
|
||||
const response = await axios.post<CreateSegmentsResponse>(
|
||||
`${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<OperationResult> {
|
||||
console.log('[Dataset Client] 删除分段:', { datasetId, documentId, segmentId });
|
||||
|
||||
const response = await axios.delete<OperationResult>(
|
||||
`${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<OperationResult> {
|
||||
console.log('[Dataset Client] 切换分段状态:', { datasetId, documentId, segmentId, enabled });
|
||||
|
||||
const response = await axios.post<OperationResult>(
|
||||
`${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<ChildChunksResponse> {
|
||||
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<ChildChunksResponse>(
|
||||
`${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<CreateChildChunkResponse> {
|
||||
console.log('[Dataset Client] 新增子分段:', { datasetId, documentId, segmentId });
|
||||
|
||||
const response = await axios.post<CreateChildChunkResponse>(
|
||||
`${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<ChildChunk> {
|
||||
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<OperationResult> {
|
||||
console.log('[Dataset Client] 删除子分段:', { datasetId, documentId, segmentId, childChunkId });
|
||||
|
||||
const response = await axios.delete<OperationResult>(
|
||||
`${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<RetrieveResponse> {
|
||||
console.log('[Dataset Client] 检索知识库:', { datasetId, query });
|
||||
|
||||
const requestBody: RetrieveRequest = {
|
||||
query,
|
||||
retrieval_model: retrievalModel,
|
||||
};
|
||||
|
||||
const response = await axios.post<RetrieveResponse>(
|
||||
`${API_URL}/datasets/${datasetId}/retrieve`,
|
||||
requestBody,
|
||||
{
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
withCredentials: true,
|
||||
}
|
||||
);
|
||||
return response.data;
|
||||
}
|
||||
@@ -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';
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
/**
|
||||
* Dify Dataset API 通用类型定义
|
||||
*
|
||||
* @module api/dify-dataset/type/commonTypes
|
||||
*/
|
||||
|
||||
/**
|
||||
* 通用操作结果
|
||||
*/
|
||||
export interface OperationResult {
|
||||
result: 'success' | 'error';
|
||||
message?: string;
|
||||
}
|
||||
@@ -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[];
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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';
|
||||
@@ -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[];
|
||||
}
|
||||
@@ -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[];
|
||||
}
|
||||
@@ -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 (
|
||||
<div className="settings-loading">
|
||||
<Spin size="large" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="dataset-settings-page">
|
||||
{/* 页面标题 */}
|
||||
<div className="page-header">
|
||||
<h1>设置</h1>
|
||||
<p className="page-description">
|
||||
管理知识库的基本信息
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* 设置表单 */}
|
||||
<Card className="settings-card">
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
onValuesChange={handleValuesChange}
|
||||
>
|
||||
<Form.Item
|
||||
name="name"
|
||||
label="知识库名称"
|
||||
rules={[
|
||||
{ required: true, message: '请输入知识库名称' },
|
||||
{ max: 100, message: '名称不能超过100个字符' },
|
||||
]}
|
||||
>
|
||||
<Input placeholder="请输入知识库名称" maxLength={100} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="description"
|
||||
label="知识库描述"
|
||||
extra="描述知识库的用途和内容(仅展示,暂不支持修改)"
|
||||
>
|
||||
<TextArea
|
||||
placeholder="请输入知识库描述"
|
||||
rows={4}
|
||||
maxLength={500}
|
||||
showCount
|
||||
disabled
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
{/* 只读信息 */}
|
||||
<div className="readonly-info">
|
||||
<div className="info-item">
|
||||
<span className="info-label">索引方式:</span>
|
||||
<span className="info-value">
|
||||
{dataset.indexing_technique === 'high_quality' ? '高质量' : '经济'}
|
||||
</span>
|
||||
</div>
|
||||
{/* <div className="info-item">
|
||||
<span className="info-label">Embedding 模型:</span>
|
||||
<span className="info-value">
|
||||
{dataset.embedding_model || '默认模型'}
|
||||
</span>
|
||||
</div> */}
|
||||
<div className="info-item">
|
||||
<span className="info-label">文档数量:</span>
|
||||
<span className="info-value">{dataset.document_count}</span>
|
||||
</div>
|
||||
<div className="info-item">
|
||||
<span className="info-label">总字符数:</span>
|
||||
<span className="info-value">{dataset.word_count?.toLocaleString()}</span>
|
||||
</div>
|
||||
<div className="info-item">
|
||||
<span className="info-label">创建时间:</span>
|
||||
<span className="info-value">
|
||||
{new Date(dataset.created_at * 1000).toLocaleString('zh-CN')}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 操作按钮 */}
|
||||
<div className="form-actions">
|
||||
<Button onClick={handleReset} disabled={!hasChanges}>
|
||||
重置
|
||||
</Button>
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<SaveOutlined />}
|
||||
onClick={handleSave}
|
||||
loading={saving}
|
||||
disabled={!hasChanges}
|
||||
>
|
||||
保存
|
||||
</Button>
|
||||
</div>
|
||||
</Form>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,367 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import {
|
||||
Input,
|
||||
Button,
|
||||
InputNumber,
|
||||
Checkbox,
|
||||
Select,
|
||||
Card,
|
||||
Empty,
|
||||
Spin,
|
||||
message,
|
||||
Divider,
|
||||
Tooltip,
|
||||
} from 'antd';
|
||||
import {
|
||||
QuestionCircleOutlined,
|
||||
ReloadOutlined,
|
||||
EyeOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import type { Document } from '~/api/dify-dataset/type/documentTypes';
|
||||
import type { Segment } from '~/api/dify-dataset/type';
|
||||
import { fetchSegments } from '~/api/dify-dataset/api/segmentApi';
|
||||
import { updateDocumentWithSettings } from '~/api/dify-dataset/api/documentApi';
|
||||
|
||||
interface DocumentDetailProps {
|
||||
datasetId: string;
|
||||
document: Document | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 分段设置配置
|
||||
* 注意:Dify API 支持的参数有限
|
||||
* - separator: ✅ 支持
|
||||
* - maxTokens: ✅ 支持
|
||||
* - removeExtraSpaces: ✅ 支持
|
||||
* - removeUrlsEmails: ✅ 支持
|
||||
* - useQASegment: ⚠️ 需要 doc_form: "qa_model"
|
||||
*/
|
||||
interface SegmentationSettings {
|
||||
separator: string;
|
||||
maxTokens: number;
|
||||
removeExtraSpaces: boolean;
|
||||
removeUrlsEmails: boolean;
|
||||
useQASegment: boolean;
|
||||
qaLanguage: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 默认分段设置
|
||||
*/
|
||||
const DEFAULT_SETTINGS: SegmentationSettings = {
|
||||
separator: '\\n\\n',
|
||||
maxTokens: 500,
|
||||
removeExtraSpaces: true,
|
||||
removeUrlsEmails: false,
|
||||
useQASegment: false,
|
||||
qaLanguage: 'Chinese',
|
||||
};
|
||||
|
||||
/**
|
||||
* 文档详情组件
|
||||
* 显示文档的分段设置,支持修改并重新处理
|
||||
*/
|
||||
export default function DocumentDetail({
|
||||
datasetId,
|
||||
document,
|
||||
}: DocumentDetailProps) {
|
||||
// 分段设置状态
|
||||
const [settings, setSettings] = useState<SegmentationSettings>(DEFAULT_SETTINGS);
|
||||
|
||||
// 预览状态
|
||||
const [previewSegments, setPreviewSegments] = useState<Segment[]>([]);
|
||||
const [previewLoading, setPreviewLoading] = useState(false);
|
||||
const [showPreview, setShowPreview] = useState(false);
|
||||
|
||||
// 保存状态
|
||||
const [saving, setSaving] = useState(false);
|
||||
|
||||
// 当文档变化时重置设置
|
||||
useEffect(() => {
|
||||
if (document) {
|
||||
// 可以从文档中读取已有的设置,这里使用默认值
|
||||
setSettings(DEFAULT_SETTINGS);
|
||||
setPreviewSegments([]);
|
||||
setShowPreview(false);
|
||||
}
|
||||
}, [document?.id]);
|
||||
|
||||
/**
|
||||
* 更新设置
|
||||
*/
|
||||
const updateSettings = (key: keyof SegmentationSettings, value: any) => {
|
||||
setSettings(prev => ({ ...prev, [key]: value }));
|
||||
};
|
||||
|
||||
/**
|
||||
* 重置设置
|
||||
*/
|
||||
const handleReset = () => {
|
||||
setSettings(DEFAULT_SETTINGS);
|
||||
setPreviewSegments([]);
|
||||
setShowPreview(false);
|
||||
};
|
||||
|
||||
/**
|
||||
* 预览分段
|
||||
*/
|
||||
const handlePreview = async () => {
|
||||
if (!document) return;
|
||||
|
||||
setPreviewLoading(true);
|
||||
setShowPreview(true);
|
||||
try {
|
||||
// 获取当前文档的分段作为预览
|
||||
const response = await fetchSegments(datasetId, document.id, 1, 50);
|
||||
setPreviewSegments(response.data || []);
|
||||
if (response.data?.length === 0) {
|
||||
message.info('该文档暂无分段数据');
|
||||
}
|
||||
} catch (err: any) {
|
||||
console.error('预览分段失败:', err);
|
||||
message.error(err.message || '预览失败');
|
||||
} finally {
|
||||
setPreviewLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 保存并处理
|
||||
*/
|
||||
const handleSaveAndProcess = async () => {
|
||||
if (!document) return;
|
||||
|
||||
setSaving(true);
|
||||
try {
|
||||
await updateDocumentWithSettings(datasetId, document.id, {
|
||||
indexing_technique: 'high_quality',
|
||||
process_rule: {
|
||||
mode: 'custom',
|
||||
rules: {
|
||||
pre_processing_rules: [
|
||||
{ id: 'remove_extra_spaces', enabled: settings.removeExtraSpaces },
|
||||
{ id: 'remove_urls_emails', enabled: settings.removeUrlsEmails },
|
||||
],
|
||||
segmentation: {
|
||||
separator: settings.separator.replace(/\\n/g, '\n'),
|
||||
max_tokens: settings.maxTokens,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
message.success('设置已保存,文档正在重新处理...');
|
||||
} catch (err: any) {
|
||||
console.error('保存设置失败:', err);
|
||||
message.error(err.message || '保存失败');
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (!document) {
|
||||
return (
|
||||
<div className="document-detail-empty">
|
||||
<Empty description="请选择一个文档" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="document-detail-page">
|
||||
<div className="document-detail-content">
|
||||
{/* 左侧设置区域 */}
|
||||
<div className="settings-panel">
|
||||
{/* 分段设置 */}
|
||||
<div className="settings-section">
|
||||
<h3 className="section-title">分段设置</h3>
|
||||
|
||||
{/* 分块模式 */}
|
||||
<div className="setting-item mode-selector">
|
||||
<div className="mode-option active">
|
||||
<div className="mode-icon">
|
||||
<i className="ri-text-spacing"></i>
|
||||
</div>
|
||||
<div className="mode-info">
|
||||
<span className="mode-name">通用</span>
|
||||
<span className="mode-desc">通用文本分块模式,检索和召回的块是相同的</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 分段标识符 */}
|
||||
<div className="setting-item">
|
||||
<label className="setting-label">
|
||||
分段标识符
|
||||
<Tooltip title="系统会在遇到指定分隔符时自动分段,默认值为 \n\n(按段落分段)">
|
||||
<QuestionCircleOutlined className="help-icon" />
|
||||
</Tooltip>
|
||||
</label>
|
||||
<Input
|
||||
value={settings.separator}
|
||||
onChange={(e) => updateSettings('separator', e.target.value)}
|
||||
placeholder="\n\n"
|
||||
className="setting-input"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 分段最大长度 */}
|
||||
<div className="setting-item">
|
||||
<label className="setting-label">
|
||||
分段最大长度
|
||||
<Tooltip title="指定每个分段允许的最大字符数,超过此限制系统会强制分段">
|
||||
<QuestionCircleOutlined className="help-icon" />
|
||||
</Tooltip>
|
||||
</label>
|
||||
<div className="setting-input-with-suffix">
|
||||
<InputNumber
|
||||
value={settings.maxTokens}
|
||||
onChange={(value) => updateSettings('maxTokens', value || 500)}
|
||||
min={100}
|
||||
max={4000}
|
||||
className="setting-input-number"
|
||||
/>
|
||||
<span className="input-suffix">characters</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Divider />
|
||||
|
||||
{/* 文本预处理规则 */}
|
||||
<div className="settings-section">
|
||||
<h3 className="section-title">文本预处理规则</h3>
|
||||
|
||||
<div className="checkbox-group">
|
||||
<Checkbox
|
||||
checked={settings.removeExtraSpaces}
|
||||
onChange={(e) => updateSettings('removeExtraSpaces', e.target.checked)}
|
||||
>
|
||||
替换掉连续的空格、换行符和制表符
|
||||
</Checkbox>
|
||||
|
||||
<Checkbox
|
||||
checked={settings.removeUrlsEmails}
|
||||
onChange={(e) => updateSettings('removeUrlsEmails', e.target.checked)}
|
||||
>
|
||||
删除所有 URL 和电子邮件地址
|
||||
</Checkbox>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Divider />
|
||||
|
||||
{/* Q&A 分段 */}
|
||||
<div className="settings-section">
|
||||
<div className="qa-segment-row">
|
||||
<Checkbox
|
||||
checked={settings.useQASegment}
|
||||
onChange={(e) => updateSettings('useQASegment', e.target.checked)}
|
||||
>
|
||||
使用 Q&A 分段,语言
|
||||
</Checkbox>
|
||||
<Select
|
||||
value={settings.qaLanguage}
|
||||
onChange={(value) => updateSettings('qaLanguage', value)}
|
||||
disabled={!settings.useQASegment}
|
||||
style={{ width: 120 }}
|
||||
options={[
|
||||
{ value: 'Chinese', label: 'Chinese' },
|
||||
{ value: 'English', label: 'English' },
|
||||
{ value: 'Japanese', label: 'Japanese' },
|
||||
{ value: 'Korean', label: 'Korean' },
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 操作按钮 */}
|
||||
<div className="settings-actions">
|
||||
<Button
|
||||
icon={<EyeOutlined />}
|
||||
onClick={handlePreview}
|
||||
loading={previewLoading}
|
||||
>
|
||||
预览块
|
||||
</Button>
|
||||
<Button
|
||||
icon={<ReloadOutlined />}
|
||||
onClick={handleReset}
|
||||
>
|
||||
重置
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Divider />
|
||||
|
||||
{/* 保存并处理按钮 */}
|
||||
<div className="save-actions">
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={handleSaveAndProcess}
|
||||
loading={saving}
|
||||
block
|
||||
>
|
||||
保存并处理
|
||||
</Button>
|
||||
<Button block style={{ marginTop: 8 }}>
|
||||
取消
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 右侧预览区域 */}
|
||||
<div className="preview-panel">
|
||||
<Card
|
||||
title={
|
||||
<div className="preview-header">
|
||||
<span>预览</span>
|
||||
<Select
|
||||
value={document.name}
|
||||
style={{ width: 200 }}
|
||||
disabled
|
||||
options={[{ value: document.name, label: document.name }]}
|
||||
/>
|
||||
<span className="segment-count">
|
||||
{showPreview ? `${previewSegments.length} 段块` : '0 段块'}
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
className="preview-card"
|
||||
>
|
||||
{previewLoading ? (
|
||||
<div className="preview-loading">
|
||||
<Spin size="large" />
|
||||
<div className="loading-text">加载中...</div>
|
||||
</div>
|
||||
) : !showPreview ? (
|
||||
<div className="preview-empty">
|
||||
<div className="empty-icon">
|
||||
<EyeOutlined />
|
||||
</div>
|
||||
<p>点击左侧的"预览块"按钮来预览</p>
|
||||
</div>
|
||||
) : previewSegments.length === 0 ? (
|
||||
<Empty description="暂无分段数据" />
|
||||
) : (
|
||||
<div className="preview-segments">
|
||||
{previewSegments.map((segment, index) => (
|
||||
<div key={segment.id} className="segment-item">
|
||||
<div className="segment-header">
|
||||
<span className="segment-index">#{index + 1}</span>
|
||||
<span className="segment-chars">
|
||||
{segment.word_count} 字符
|
||||
</span>
|
||||
</div>
|
||||
<div className="segment-content">
|
||||
{segment.content}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -27,8 +27,8 @@ import {
|
||||
PauseCircleOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import type { ColumnsType } from 'antd/es/table';
|
||||
import type { Document, IndexingStatus } from '~/api/dify-dataset';
|
||||
import { deleteDocument, toggleDocumentStatus, uploadDocument } from '~/api/dify-dataset';
|
||||
import type { Document, IndexingStatus } from '~/api/dify-dataset/type/documentTypes';
|
||||
import { deleteDocument, toggleDocumentStatus, uploadDocument } from '~/api/dify-dataset/api/documentApi';
|
||||
import '../../styles/components/dify-dataset-manager/index.css';
|
||||
|
||||
interface DocumentListProps {
|
||||
@@ -43,6 +43,7 @@ interface DocumentListProps {
|
||||
onDocumentDeleted: (documentId: string) => void;
|
||||
onDocumentStatusChanged: (documentId: string, enabled: boolean) => void;
|
||||
onRefresh: () => void;
|
||||
onViewDocument?: (document: Document) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -50,7 +51,6 @@ interface DocumentListProps {
|
||||
*/
|
||||
export default function DocumentList({
|
||||
datasetId,
|
||||
datasetName,
|
||||
documents,
|
||||
loading,
|
||||
total,
|
||||
@@ -60,6 +60,7 @@ export default function DocumentList({
|
||||
onDocumentDeleted,
|
||||
onDocumentStatusChanged,
|
||||
onRefresh,
|
||||
onViewDocument,
|
||||
}: DocumentListProps) {
|
||||
const [searchValue, setSearchValue] = useState('');
|
||||
const [uploading, setUploading] = useState(false);
|
||||
@@ -238,15 +239,12 @@ export default function DocumentList({
|
||||
width: 120,
|
||||
render: (_, record) => (
|
||||
<Space size="small">
|
||||
<Tooltip title="查看详情">
|
||||
<Tooltip title="查看分段">
|
||||
<Button
|
||||
type="text"
|
||||
size="small"
|
||||
icon={<EyeOutlined />}
|
||||
onClick={() => {
|
||||
// TODO: 查看文档详情/分段
|
||||
message.info('功能开发中');
|
||||
}}
|
||||
onClick={() => onViewDocument?.(record)}
|
||||
/>
|
||||
</Tooltip>
|
||||
<Popconfirm
|
||||
@@ -273,11 +271,16 @@ export default function DocumentList({
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="dataset-content">
|
||||
{/* 头部区域 */}
|
||||
<div className="dataset-header">
|
||||
<h1>{datasetName || '知识库文档'}</h1>
|
||||
<div className="dataset-header-actions">
|
||||
<div className="document-list-page">
|
||||
{/* 页面头部 */}
|
||||
<div className="page-header">
|
||||
<div className="header-left">
|
||||
<h1>文档</h1>
|
||||
{/* <p className="page-description">
|
||||
知识库的所有文件都在这里显示,整个知识库都可以被接到 Dify 引用或通过 Chat 插件进行索引。
|
||||
</p> */}
|
||||
</div>
|
||||
<div className="header-actions">
|
||||
<Tooltip title="刷新">
|
||||
<Button
|
||||
icon={<ReloadOutlined />}
|
||||
@@ -297,36 +300,34 @@ export default function DocumentList({
|
||||
loading={uploading}
|
||||
disabled={!datasetId}
|
||||
>
|
||||
上传文档
|
||||
添加文件
|
||||
</Button>
|
||||
</Upload>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 工具栏 */}
|
||||
<div className="document-list-toolbar">
|
||||
{/* 搜索栏 */}
|
||||
<div className="document-search-bar">
|
||||
<Input
|
||||
className="document-list-search"
|
||||
placeholder="搜索文档..."
|
||||
prefix={<SearchOutlined />}
|
||||
value={searchValue}
|
||||
onChange={(e) => setSearchValue(e.target.value)}
|
||||
allowClear
|
||||
style={{ width: 280 }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 文档表格 - 固定表头和分页 */}
|
||||
<div className="document-table-container">
|
||||
{/* 文档表格 */}
|
||||
<div className="document-table-wrapper">
|
||||
{loading && documents.length === 0 ? (
|
||||
<div className="dataset-loading">
|
||||
<div className="loading-state">
|
||||
<Spin size="large" />
|
||||
<span className="text-gray-500">加载中...</span>
|
||||
<div className="loading-text">加载中...</div>
|
||||
</div>
|
||||
) : filteredDocuments.length === 0 ? (
|
||||
<div className="dataset-empty">
|
||||
<Empty
|
||||
description={searchValue ? '未找到匹配的文档' : '暂无文档'}
|
||||
>
|
||||
<div className="empty-state">
|
||||
<Empty description={searchValue ? '未找到匹配的文档' : '暂无文档'}>
|
||||
{!searchValue && (
|
||||
<Upload
|
||||
beforeUpload={handleUpload}
|
||||
@@ -354,7 +355,7 @@ export default function DocumentList({
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 固定底部分页器 */}
|
||||
{/* 底部分页器 */}
|
||||
{filteredDocuments.length > 0 && (
|
||||
<div className="document-pagination">
|
||||
<span className="pagination-total">共 {total} 条</span>
|
||||
|
||||
@@ -1,13 +1,19 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { message, Spin } from 'antd';
|
||||
import DatasetLayout, { type MenuTab } from './layout';
|
||||
import DocumentList from './document-list';
|
||||
import type { Dataset, Document } from '~/api/dify-dataset';
|
||||
import { fetchDatasets, fetchDocuments } from '~/api/dify-dataset';
|
||||
import DocumentDetail from './document-detail';
|
||||
import RetrieveTest from './retrieve-test';
|
||||
import DatasetSettings from './dataset-settings';
|
||||
import type { Dataset } from '~/api/dify-dataset/type/datasetTypes';
|
||||
import type { Document } from '~/api/dify-dataset/type/documentTypes';
|
||||
import { fetchDatasets } from '~/api/dify-dataset/api/datasetApi';
|
||||
import { fetchDocuments } from '~/api/dify-dataset/api/documentApi';
|
||||
import '../../styles/components/dify-dataset-manager/index.css';
|
||||
|
||||
/**
|
||||
* 知识库管理主组件
|
||||
* 简化版 - 假设只有一个知识库,直接显示文档列表
|
||||
* 带左侧菜单栏的完整布局
|
||||
*/
|
||||
export default function DatasetManager() {
|
||||
// 知识库状态
|
||||
@@ -25,6 +31,12 @@ export default function DatasetManager() {
|
||||
const [inited, setInited] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
// 菜单状态
|
||||
const [activeTab, setActiveTab] = useState<MenuTab>('documents');
|
||||
|
||||
// 选中的文档(用于查看文档详情)
|
||||
const [selectedDocument, setSelectedDocument] = useState<Document | null>(null);
|
||||
|
||||
/**
|
||||
* 加载知识库(获取第一个知识库)
|
||||
*/
|
||||
@@ -123,6 +135,39 @@ export default function DatasetManager() {
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 查看文档详情(分段管理)
|
||||
*/
|
||||
const handleViewDocument = (doc: Document) => {
|
||||
console.log('[DatasetManager] 查看文档详情:', doc);
|
||||
setSelectedDocument(doc);
|
||||
};
|
||||
|
||||
/**
|
||||
* 返回文档列表
|
||||
*/
|
||||
const handleBackToDocuments = () => {
|
||||
setSelectedDocument(null);
|
||||
};
|
||||
|
||||
/**
|
||||
* 处理菜单切换
|
||||
*/
|
||||
const handleTabChange = (tab: MenuTab) => {
|
||||
setActiveTab(tab);
|
||||
// 切换菜单时清除选中的文档
|
||||
if (tab !== 'documents') {
|
||||
setSelectedDocument(null);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 处理知识库更新
|
||||
*/
|
||||
const handleDatasetUpdated = (updatedDataset: Dataset) => {
|
||||
setDataset(updatedDataset);
|
||||
};
|
||||
|
||||
// 初始化
|
||||
useEffect(() => {
|
||||
loadDataset();
|
||||
@@ -132,11 +177,9 @@ export default function DatasetManager() {
|
||||
if (!inited || loadingDataset) {
|
||||
return (
|
||||
<div className="dataset-manager-wrapper">
|
||||
<div className="dataset-manager-card">
|
||||
<div className="dataset-loading-state">
|
||||
<Spin size="large" />
|
||||
<span className="loading-text">正在加载知识库...</span>
|
||||
</div>
|
||||
<div className="dataset-loading-state">
|
||||
<Spin size="large" />
|
||||
<span className="loading-text">正在加载知识库...</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -146,20 +189,32 @@ export default function DatasetManager() {
|
||||
if (error) {
|
||||
return (
|
||||
<div className="dataset-manager-wrapper">
|
||||
<div className="dataset-manager-card">
|
||||
<div className="dataset-error-state">
|
||||
<i className="ri-error-warning-line error-icon"></i>
|
||||
<h3>加载失败</h3>
|
||||
<p>{error}</p>
|
||||
</div>
|
||||
<div className="dataset-error-state">
|
||||
<i className="ri-error-warning-line error-icon"></i>
|
||||
<h3>加载失败</h3>
|
||||
<p>{error}</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="dataset-manager-wrapper">
|
||||
<div className="dataset-manager-card">
|
||||
/**
|
||||
* 渲染右侧内容区
|
||||
*/
|
||||
const renderContent = () => {
|
||||
// 文档菜单
|
||||
if (activeTab === 'documents') {
|
||||
// 如果选中了文档,显示文档详情
|
||||
if (selectedDocument) {
|
||||
return (
|
||||
<DocumentDetail
|
||||
datasetId={dataset?.id || ''}
|
||||
document={selectedDocument}
|
||||
/>
|
||||
);
|
||||
}
|
||||
// 否则显示文档列表
|
||||
return (
|
||||
<DocumentList
|
||||
datasetId={dataset?.id || ''}
|
||||
datasetName={dataset?.name || ''}
|
||||
@@ -172,8 +227,40 @@ export default function DatasetManager() {
|
||||
onDocumentDeleted={handleDocumentDeleted}
|
||||
onDocumentStatusChanged={handleDocumentStatusChanged}
|
||||
onRefresh={handleRefresh}
|
||||
onViewDocument={handleViewDocument}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// 召回测试菜单
|
||||
if (activeTab === 'retrieve') {
|
||||
return <RetrieveTest datasetId={dataset?.id || ''} />;
|
||||
}
|
||||
|
||||
// 设置菜单
|
||||
if (activeTab === 'settings') {
|
||||
return (
|
||||
<DatasetSettings
|
||||
dataset={dataset}
|
||||
onDatasetUpdated={handleDatasetUpdated}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="dataset-manager-wrapper">
|
||||
<DatasetLayout
|
||||
dataset={dataset}
|
||||
activeTab={activeTab}
|
||||
onTabChange={handleTabChange}
|
||||
showBackButton={activeTab === 'documents' && !!selectedDocument}
|
||||
onBack={handleBackToDocuments}
|
||||
>
|
||||
{renderContent()}
|
||||
</DatasetLayout>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
import { ReactNode } from 'react';
|
||||
import { Button, Tooltip } from 'antd';
|
||||
import {
|
||||
FileTextOutlined,
|
||||
SearchOutlined,
|
||||
SettingOutlined,
|
||||
ArrowLeftOutlined,
|
||||
DatabaseOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import type { Dataset } from '~/api/dify-dataset/type/datasetTypes';
|
||||
|
||||
/**
|
||||
* 菜单项类型
|
||||
*/
|
||||
export type MenuTab = 'documents' | 'retrieve' | 'settings';
|
||||
|
||||
interface DatasetLayoutProps {
|
||||
/** 知识库信息 */
|
||||
dataset: Dataset | null;
|
||||
/** 当前激活的菜单 */
|
||||
activeTab: MenuTab;
|
||||
/** 菜单切换回调 */
|
||||
onTabChange: (tab: MenuTab) => void;
|
||||
/** 是否显示返回按钮(在文档详情页时显示) */
|
||||
showBackButton?: boolean;
|
||||
/** 返回按钮点击回调 */
|
||||
onBack?: () => void;
|
||||
/** 子组件 */
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
/**
|
||||
* 知识库布局组件
|
||||
* 包含左侧菜单栏和右侧内容区
|
||||
*/
|
||||
export default function DatasetLayout({
|
||||
dataset,
|
||||
activeTab,
|
||||
onTabChange,
|
||||
showBackButton = false,
|
||||
onBack,
|
||||
children,
|
||||
}: DatasetLayoutProps) {
|
||||
const menuItems: { key: MenuTab; icon: ReactNode; label: string }[] = [
|
||||
{ key: 'documents', icon: <FileTextOutlined />, label: '文档' },
|
||||
{ key: 'retrieve', icon: <SearchOutlined />, label: '召回测试' },
|
||||
{ key: 'settings', icon: <SettingOutlined />, label: '设置' },
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="dataset-layout">
|
||||
{/* 左侧侧边栏 */}
|
||||
<aside className="dataset-sidebar">
|
||||
{/* 返回按钮 */}
|
||||
{showBackButton && onBack && (
|
||||
<div className="sidebar-back">
|
||||
<Button
|
||||
type="text"
|
||||
icon={<ArrowLeftOutlined />}
|
||||
onClick={onBack}
|
||||
className="back-btn"
|
||||
>
|
||||
返回文档列表
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 知识库信息 */}
|
||||
<div className="sidebar-header">
|
||||
<div className="dataset-icon">
|
||||
<DatabaseOutlined />
|
||||
</div>
|
||||
<div className="dataset-info">
|
||||
<Tooltip title={dataset?.name} placement="right">
|
||||
<h2 className="dataset-name">{dataset?.name || '知识库'}</h2>
|
||||
</Tooltip>
|
||||
<span className="dataset-type">本地文档</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 统计信息 */}
|
||||
<div className="sidebar-stats">
|
||||
<span className="stat-item">
|
||||
{dataset?.document_count || 0} 个文档
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* 导航菜单 */}
|
||||
<nav className="sidebar-menu">
|
||||
{menuItems.map((item) => (
|
||||
<button
|
||||
key={item.key}
|
||||
className={`menu-item ${activeTab === item.key ? 'active' : ''}`}
|
||||
onClick={() => onTabChange(item.key)}
|
||||
>
|
||||
<span className="menu-icon">{item.icon}</span>
|
||||
<span className="menu-label">{item.label}</span>
|
||||
</button>
|
||||
))}
|
||||
</nav>
|
||||
</aside>
|
||||
|
||||
{/* 右侧内容区 */}
|
||||
<main className="dataset-main">
|
||||
{children}
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,202 @@
|
||||
import { useState } from 'react';
|
||||
import {
|
||||
Input,
|
||||
Button,
|
||||
Card,
|
||||
Select,
|
||||
Slider,
|
||||
Table,
|
||||
Tag,
|
||||
Empty,
|
||||
Spin,
|
||||
message,
|
||||
} from 'antd';
|
||||
import { FileSearchOutlined } from '@ant-design/icons';
|
||||
import type { ColumnsType } from 'antd/es/table';
|
||||
import type { RetrieveRecord } from '~/api/dify-dataset/type';
|
||||
import { retrieveDataset } from '~/api/dify-dataset/api/segmentApi';
|
||||
|
||||
interface RetrieveTestProps {
|
||||
datasetId: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 召回测试组件
|
||||
* 用于测试知识库的检索效果
|
||||
*/
|
||||
export default function RetrieveTest({ datasetId }: RetrieveTestProps) {
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const [retrieveResults, setRetrieveResults] = useState<RetrieveRecord[]>([]);
|
||||
const [retrieving, setRetrieving] = useState(false);
|
||||
const [searchMethod, setSearchMethod] = useState<string>('hybrid_search');
|
||||
const [topK, setTopK] = useState<number>(5);
|
||||
|
||||
/**
|
||||
* 执行检索
|
||||
*/
|
||||
const handleRetrieve = async () => {
|
||||
if (!searchQuery.trim()) {
|
||||
message.warning('请输入检索关键词');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!datasetId) {
|
||||
message.warning('知识库ID不存在');
|
||||
return;
|
||||
}
|
||||
|
||||
setRetrieving(true);
|
||||
try {
|
||||
const response = await retrieveDataset(datasetId, searchQuery, {
|
||||
search_method: searchMethod as any,
|
||||
top_k: topK,
|
||||
});
|
||||
setRetrieveResults(response.records || []);
|
||||
if (response.records?.length === 0) {
|
||||
message.info('未找到匹配的结果');
|
||||
}
|
||||
} catch (err: any) {
|
||||
console.error('检索失败:', err);
|
||||
message.error(err.message || '检索失败');
|
||||
} finally {
|
||||
setRetrieving(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 检索结果列定义
|
||||
const columns: ColumnsType<RetrieveRecord> = [
|
||||
{
|
||||
title: '相关度',
|
||||
dataIndex: 'score',
|
||||
key: 'score',
|
||||
width: 100,
|
||||
render: (score: number) => (
|
||||
<Tag color={score > 0.8 ? 'green' : score > 0.5 ? 'orange' : 'default'}>
|
||||
{(score * 100).toFixed(1)}%
|
||||
</Tag>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '内容',
|
||||
key: 'content',
|
||||
render: (_, record) => (
|
||||
<div className="retrieve-result-content">
|
||||
<div className="content-text">
|
||||
{record.segment.content.length > 300
|
||||
? record.segment.content.substring(0, 300) + '...'
|
||||
: record.segment.content}
|
||||
</div>
|
||||
{record.segment.answer && (
|
||||
<div className="answer-text">
|
||||
<strong>答案:</strong>
|
||||
{record.segment.answer.length > 150
|
||||
? record.segment.answer.substring(0, 150) + '...'
|
||||
: record.segment.answer}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '字数',
|
||||
key: 'word_count',
|
||||
width: 80,
|
||||
render: (_, record) => record.segment.word_count,
|
||||
},
|
||||
{
|
||||
title: '命中次数',
|
||||
key: 'hit_count',
|
||||
width: 100,
|
||||
render: (_, record) => record.segment.hit_count,
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="retrieve-test-page">
|
||||
{/* 页面标题 */}
|
||||
<div className="page-header">
|
||||
<h1>召回测试</h1>
|
||||
<p className="page-description">
|
||||
输入查询内容,测试知识库的检索效果
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* 检索设置 */}
|
||||
<Card className="retrieve-settings" size="small">
|
||||
<div className="search-row">
|
||||
<Input
|
||||
placeholder="输入检索关键词..."
|
||||
prefix={<FileSearchOutlined />}
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
onPressEnter={handleRetrieve}
|
||||
className="search-input"
|
||||
/>
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={handleRetrieve}
|
||||
loading={retrieving}
|
||||
>
|
||||
检索
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="options-row">
|
||||
<div className="option-item">
|
||||
<span className="option-label">检索方式:</span>
|
||||
<Select
|
||||
value={searchMethod}
|
||||
onChange={setSearchMethod}
|
||||
style={{ width: 140 }}
|
||||
options={[
|
||||
{ value: 'keyword_search', label: '关键词搜索' },
|
||||
{ value: 'semantic_search', label: '语义搜索' },
|
||||
{ value: 'full_text_search', label: '全文搜索' },
|
||||
{ value: 'hybrid_search', label: '混合搜索' },
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
<div className="option-item">
|
||||
<span className="option-label">返回数量 (Top K):</span>
|
||||
<Slider
|
||||
value={topK}
|
||||
onChange={setTopK}
|
||||
min={1}
|
||||
max={20}
|
||||
style={{ width: 120 }}
|
||||
/>
|
||||
<span className="option-value">{topK}</span>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* 检索结果 */}
|
||||
<div className="retrieve-results">
|
||||
{retrieving ? (
|
||||
<div className="loading-state">
|
||||
<Spin size="large" />
|
||||
<div className="loading-text">检索中...</div>
|
||||
</div>
|
||||
) : retrieveResults.length === 0 ? (
|
||||
<Empty
|
||||
description="请输入关键词进行检索"
|
||||
className="empty-state"
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
<div className="results-header">
|
||||
<span>找到 {retrieveResults.length} 条结果</span>
|
||||
</div>
|
||||
<Table
|
||||
columns={columns}
|
||||
dataSource={retrieveResults}
|
||||
rowKey={(record) => record.segment.id}
|
||||
pagination={false}
|
||||
size="small"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
+93
@@ -0,0 +1,93 @@
|
||||
import { type ActionFunctionArgs } from '@remix-run/node';
|
||||
import { API_BASE_URL } from '~/config/api-config';
|
||||
|
||||
/**
|
||||
* PATCH /api/dataset/datasets/:datasetId/documents/:documentId/segments/:segmentId/child_chunks/:childChunkId - 更新子分段
|
||||
* DELETE /api/dataset/datasets/:datasetId/documents/:documentId/segments/:segmentId/child_chunks/:childChunkId - 删除子分段
|
||||
*
|
||||
* Dify API:
|
||||
* - PATCH /datasets/{dataset_id}/documents/{document_id}/segments/{segment_id}/child_chunks/{child_chunk_id}
|
||||
* - DELETE /datasets/{dataset_id}/documents/{document_id}/segments/{segment_id}/child_chunks/{child_chunk_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, childChunkId } = params;
|
||||
if (!datasetId || !documentId || !segmentId || !childChunkId) {
|
||||
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}/child_chunks/${childChunkId}`;
|
||||
|
||||
if (method === 'DELETE') {
|
||||
console.log('[API] Delete Child Chunk:', { datasetId, documentId, segmentId, childChunkId });
|
||||
|
||||
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 === 'PATCH') {
|
||||
const body = await request.json();
|
||||
console.log('[API] Update Child Chunk:', { datasetId, documentId, segmentId, childChunkId, body });
|
||||
|
||||
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] Child Chunk Action - Error:', error.message);
|
||||
return new Response(
|
||||
JSON.stringify({ error: error.message || 'Failed to process child chunk request' }),
|
||||
{ status: 500, headers: { 'Content-Type': 'application/json' } }
|
||||
);
|
||||
}
|
||||
}
|
||||
+126
@@ -0,0 +1,126 @@
|
||||
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/child_chunks - 获取子分段列表
|
||||
* Dify API: GET /datasets/{dataset_id}/documents/{document_id}/segments/{segment_id}/child_chunks
|
||||
*/
|
||||
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' } }
|
||||
);
|
||||
}
|
||||
|
||||
// 获取查询参数
|
||||
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') || '';
|
||||
|
||||
console.log('[API] Child Chunks List:', { datasetId, documentId, segmentId, page, limit, keyword });
|
||||
|
||||
// 构建查询参数
|
||||
const queryParams = new URLSearchParams({ page, limit });
|
||||
if (keyword) queryParams.append('keyword', keyword);
|
||||
|
||||
// 转发请求到 FastAPI -> Dify API
|
||||
const apiUrl = `${API_BASE_URL}/dify_dataset/datasets/${datasetId}/documents/${documentId}/segments/${segmentId}/child_chunks?${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] Child Chunks List - Error:', error.message);
|
||||
return new Response(
|
||||
JSON.stringify({ error: error.message || 'Failed to get child chunks' }),
|
||||
{ status: 500, headers: { 'Content-Type': 'application/json' } }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /api/dataset/datasets/:datasetId/documents/:documentId/segments/:segmentId/child_chunks - 新增子分段
|
||||
* Dify API: POST /datasets/{dataset_id}/documents/{document_id}/segments/{segment_id}/child_chunks
|
||||
* 请求体: { content: string }
|
||||
*/
|
||||
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' } }
|
||||
);
|
||||
}
|
||||
|
||||
if (request.method !== 'POST') {
|
||||
return new Response(
|
||||
JSON.stringify({ error: 'Method not allowed' }),
|
||||
{ status: 405, headers: { 'Content-Type': 'application/json' } }
|
||||
);
|
||||
}
|
||||
|
||||
const body = await request.json();
|
||||
console.log('[API] Create Child Chunk:', { datasetId, documentId, segmentId });
|
||||
|
||||
// 转发请求到 FastAPI -> Dify API
|
||||
const apiUrl = `${API_BASE_URL}/dify_dataset/datasets/${datasetId}/documents/${documentId}/segments/${segmentId}/child_chunks`;
|
||||
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' },
|
||||
});
|
||||
|
||||
} catch (error: any) {
|
||||
console.error('[API] Create Child Chunk - Error:', error.message);
|
||||
return new Response(
|
||||
JSON.stringify({ error: error.message || 'Failed to create child chunk' }),
|
||||
{ status: 500, headers: { 'Content-Type': 'application/json' } }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { type LoaderFunctionArgs } from '@remix-run/node';
|
||||
import { type LoaderFunctionArgs, type ActionFunctionArgs } from '@remix-run/node';
|
||||
import { API_BASE_URL } from '~/config/api-config';
|
||||
|
||||
/**
|
||||
@@ -64,3 +64,65 @@ export async function loader({ request, params }: LoaderFunctionArgs) {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /api/dataset/datasets/:datasetId/documents/:documentId/segments - 新增分段(批量)
|
||||
* Dify API: POST /datasets/{dataset_id}/documents/{document_id}/segments
|
||||
* 请求体: { segments: [{ content, answer?, keywords? }] }
|
||||
*/
|
||||
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 } = params;
|
||||
if (!datasetId || !documentId) {
|
||||
return new Response(
|
||||
JSON.stringify({ error: '缺少必要参数' }),
|
||||
{ status: 400, headers: { 'Content-Type': 'application/json' } }
|
||||
);
|
||||
}
|
||||
|
||||
if (request.method !== 'POST') {
|
||||
return new Response(
|
||||
JSON.stringify({ error: 'Method not allowed' }),
|
||||
{ status: 405, headers: { 'Content-Type': 'application/json' } }
|
||||
);
|
||||
}
|
||||
|
||||
const body = await request.json();
|
||||
console.log('[API] Create Segments:', { datasetId, documentId, segmentsCount: body.segments?.length });
|
||||
|
||||
// 转发请求到 FastAPI -> Dify API
|
||||
const apiUrl = `${API_BASE_URL}/dify_dataset/datasets/${datasetId}/documents/${documentId}/segments`;
|
||||
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' },
|
||||
});
|
||||
|
||||
} catch (error: any) {
|
||||
console.error('[API] Create Segments - Error:', error.message);
|
||||
return new Response(
|
||||
JSON.stringify({ error: error.message || 'Failed to create segments' }),
|
||||
{ status: 500, headers: { 'Content-Type': 'application/json' } }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
import { type ActionFunctionArgs } from '@remix-run/node';
|
||||
import { API_BASE_URL } from '~/config/api-config';
|
||||
|
||||
/**
|
||||
* POST /api/dataset/datasets/:datasetId/retrieve - 检索知识库
|
||||
* Dify API: POST /datasets/{dataset_id}/retrieve
|
||||
*
|
||||
* 请求体:
|
||||
* {
|
||||
* 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?: {
|
||||
* logical_operator: 'and' | 'or',
|
||||
* conditions: Array<{
|
||||
* name: string,
|
||||
* comparison_operator: string,
|
||||
* value?: string | number
|
||||
* }>
|
||||
* }
|
||||
* }
|
||||
*/
|
||||
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: '缺少知识库ID' }),
|
||||
{ status: 400, headers: { 'Content-Type': 'application/json' } }
|
||||
);
|
||||
}
|
||||
|
||||
if (request.method !== 'POST') {
|
||||
return new Response(
|
||||
JSON.stringify({ error: 'Method not allowed' }),
|
||||
{ status: 405, headers: { 'Content-Type': 'application/json' } }
|
||||
);
|
||||
}
|
||||
|
||||
const body = await request.json();
|
||||
console.log('[API] Retrieve Dataset:', { datasetId, query: body.query });
|
||||
|
||||
// 转发请求到 FastAPI -> Dify API
|
||||
const apiUrl = `${API_BASE_URL}/dify_dataset/datasets/${datasetId}/retrieve`;
|
||||
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' },
|
||||
});
|
||||
|
||||
} catch (error: any) {
|
||||
console.error('[API] Retrieve Dataset - Error:', error.message);
|
||||
return new Response(
|
||||
JSON.stringify({ error: error.message || 'Failed to retrieve dataset' }),
|
||||
{ status: 500, headers: { 'Content-Type': 'application/json' } }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -54,27 +54,15 @@ export async function loader({ request, params }: LoaderFunctionArgs) {
|
||||
}
|
||||
|
||||
/**
|
||||
* PATCH /api/dataset/datasets/:datasetId - 修改知识库详情
|
||||
* 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
|
||||
* }
|
||||
* }
|
||||
* 请求体: { "name": "新的知识库名称" }
|
||||
*
|
||||
* 注意:删除知识库功能不对外开放
|
||||
* 注意:
|
||||
* - 仅允许修改知识库名称,其他字段不开放修改
|
||||
* - 删除知识库功能不对外开放
|
||||
*/
|
||||
export async function action({ request, params }: ActionFunctionArgs) {
|
||||
try {
|
||||
@@ -99,9 +87,27 @@ export async function action({ request, params }: ActionFunctionArgs) {
|
||||
const method = request.method;
|
||||
|
||||
if (method === 'PATCH') {
|
||||
// 修改知识库详情
|
||||
const body = await request.json();
|
||||
console.log('[API] Update Dataset:', { datasetId, body });
|
||||
|
||||
// 只允许修改 name 字段
|
||||
if (!body.name || typeof body.name !== 'string') {
|
||||
return new Response(
|
||||
JSON.stringify({ error: '请提供有效的知识库名称 (name)' }),
|
||||
{ status: 400, headers: { 'Content-Type': 'application/json' } }
|
||||
);
|
||||
}
|
||||
|
||||
// 只传递 name 字段,忽略其他字段
|
||||
const allowedBody = { name: body.name.trim() };
|
||||
|
||||
if (allowedBody.name.length === 0) {
|
||||
return new Response(
|
||||
JSON.stringify({ error: '知识库名称不能为空' }),
|
||||
{ status: 400, headers: { 'Content-Type': 'application/json' } }
|
||||
);
|
||||
}
|
||||
|
||||
console.log('[API] Update Dataset Name:', { datasetId, name: allowedBody.name });
|
||||
|
||||
const apiUrl = `${API_BASE_URL}/dify_dataset/datasets/${datasetId}`;
|
||||
const response = await fetch(apiUrl, {
|
||||
@@ -110,7 +116,7 @@ export async function action({ request, params }: ActionFunctionArgs) {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${frontendJWT}`,
|
||||
},
|
||||
body: JSON.stringify(body),
|
||||
body: JSON.stringify(allowedBody),
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
@@ -43,7 +43,7 @@ export const meta: MetaFunction = () => {
|
||||
export default function ChatWithLLMIndex() {
|
||||
return (
|
||||
<div className="chat-container" style={{
|
||||
height: '93vh', // 使用视口高度的90%
|
||||
height: '90vh', // 使用视口高度的90%
|
||||
borderRadius: '0.5rem',
|
||||
marginBottom: '-20px',
|
||||
marginTop: '2vh',
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import DatasetManager from "~/components/dify-dataset-manager";
|
||||
import datasetManagerStyles from "~/styles/components/dify-dataset-manager/index.css?url";
|
||||
import sidebarStyles from "~/styles/components/dify-dataset-manager/sidebar.css?url";
|
||||
import documentListStyles from "~/styles/components/dify-dataset-manager/document-list.css?url";
|
||||
// import sidebarStyles from "~/styles/components/dify-dataset-manager/sidebar.css?url";
|
||||
// import documentListStyles from "~/styles/components/dify-dataset-manager/document-list.css?url";
|
||||
|
||||
/**
|
||||
* 注册样式
|
||||
@@ -9,8 +9,8 @@ import documentListStyles from "~/styles/components/dify-dataset-manager/documen
|
||||
export function links() {
|
||||
return [
|
||||
{ rel: "stylesheet", href: datasetManagerStyles },
|
||||
{ rel: "stylesheet", href: sidebarStyles },
|
||||
{ rel: "stylesheet", href: documentListStyles },
|
||||
// { rel: "stylesheet", href: sidebarStyles },
|
||||
// { rel: "stylesheet", href: documentListStyles },
|
||||
];
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ export function links() {
|
||||
*/
|
||||
export default function DatasetManagerIndex() {
|
||||
return (
|
||||
<div className="dataset-manager-page" style={{ height: '93vh', padding: '16px' }}>
|
||||
<div className="dataset-manager-page" style={{ height: '90vh', padding: '16px' }}>
|
||||
<DatasetManager />
|
||||
</div>
|
||||
);
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user