feat: complete Dify client implementation (Day 19)

## Dify 瀹㈡埛绔皝瑁呭畬鎴?鉁?
### 瀹屾垚鐨勫伐浣?
1. 绫诲瀷瀹氫箟 (backend/src/clients/types.ts)
   - Dataset, Document, Retrieval 鐩稿叧绫诲瀷
   - 瀹屾暣鐨?TypeScript 绫诲瀷瀹氫箟
   - 鑷畾涔?DifyError 閿欒绫?
2. DifyClient 鏍稿績绫?(backend/src/clients/DifyClient.ts)
   - 鐭ヨ瘑搴撶鐞? createDataset, getDatasets, getDataset, deleteDataset
   - 鏂囨。绠$悊: uploadDocumentDirectly, getDocuments, getDocument, deleteDocument
   - 鐭ヨ瘑搴撴绱? retrieveKnowledge (鏀寔璇箟鎼滅储銆乼op_k銆佺浉浼煎害闃堝€?
   - 杈呭姪鏂规硶: waitForDocumentProcessing, uploadAndProcessDocument

3. 娴嬭瘯鑴氭湰 (backend/src/scripts/test-dify-client.ts)
   - 8椤瑰畬鏁存祴璇曞叏閮ㄩ€氳繃
   - 娴嬭瘯鑰楁椂绾?5绉?   - 楠岃瘉鎵€鏈堿PI鍔熻兘姝e父

### 娴嬭瘯缁撴灉

鉁?娴嬭瘯1: 鍒涘缓鐭ヨ瘑搴?鉁?娴嬭瘯2: 鑾峰彇鐭ヨ瘑搴撳垪琛?(鎵惧埌3涓?
鉁?娴嬭瘯3: 鑾峰彇鐭ヨ瘑搴撹鎯?鉁?娴嬭瘯4: 涓婁紶鏂囨。 (247 tokens)
鉁?娴嬭瘯5: 鑾峰彇鏂囨。鍒楄〃
鉁?娴嬭瘯6: 鐭ヨ瘑搴撴绱?(鐩镐技搴?.4420)
鉁?娴嬭瘯7: 鍒犻櫎鏂囨。
鉁?娴嬭瘯8: 鍒犻櫎鐭ヨ瘑搴?
### 鎶€鏈寒鐐?
- 瀹屽杽鐨勯敊璇鐞嗘満鍒?(axios 鎷︽埅鍣?
- 鏅鸿兘杞绛夊緟鏂囨。澶勭悊瀹屾垚
- FormData 鏂囦欢涓婁紶鏀寔
- 鍗曚緥妯″紡瀵煎嚭
- 鏀寔鑷畾涔夐厤缃?
### 渚濊禆鏇存柊

- form-data: ^4.0.0
- @types/form-data: ^2.5.0

### 閰嶇疆鏇存柊

- DIFY_API_KEY 鏇存柊涓烘湇鍔PI瀵嗛挜
- DIFY_API_URL=http://localhost/v1

### 鏂囨。鏇存柊

- 鏂板: docs/05-姣忔棩杩涘害/Day19-Dify瀹㈡埛绔皝瑁呭畬鎴?md
- 鏇存柊: docs/04-寮€鍙戣鍒?寮€鍙戦噷绋嬬.md (Day 19 鏍囪涓哄畬鎴?

### 涓嬩竴姝?
Day 20-22: 鐭ヨ瘑搴撶鐞嗗姛鑳?- 鏁版嵁搴撹〃璁捐 (KnowledgeBase, Document)
- 鍚庣 CRUD API
- 鍓嶇鐭ヨ瘑搴撶鐞嗛〉闈?- 鏂囨。涓婁紶缁勪欢

---
Progress: 閲岀▼纰?1 (MVP) 90% -> 鐭ヨ瘑搴撶鐞嗗紑鍙戜腑
This commit is contained in:
AI Clinical Dev Team
2025-10-11 10:25:30 +08:00
parent 9acbb0ae2b
commit 8a4c703128
7 changed files with 1346 additions and 31 deletions

View File

@@ -12,9 +12,11 @@
"@fastify/cors": "^11.1.0",
"@fastify/jwt": "^10.0.0",
"@prisma/client": "^6.17.0",
"@types/form-data": "^2.2.1",
"axios": "^1.12.2",
"dotenv": "^17.2.3",
"fastify": "^5.6.1",
"form-data": "^4.0.4",
"js-yaml": "^4.1.0",
"prisma": "^6.17.0",
"zod": "^4.1.12"
@@ -788,6 +790,15 @@
"dev": true,
"license": "MIT"
},
"node_modules/@types/form-data": {
"version": "2.2.1",
"resolved": "https://registry.npmmirror.com/@types/form-data/-/form-data-2.2.1.tgz",
"integrity": "sha512-JAMFhOaHIciYVh8fb5/83nmuO/AHwmto+Hq7a9y8FzLDcC1KCU344XDOMEmahnrTFlHjgh4L0WJFczNIX2GxnQ==",
"license": "MIT",
"dependencies": {
"@types/node": "*"
}
},
"node_modules/@types/js-yaml": {
"version": "4.0.9",
"resolved": "https://registry.npmmirror.com/@types/js-yaml/-/js-yaml-4.0.9.tgz",
@@ -799,7 +810,6 @@
"version": "24.7.1",
"resolved": "https://registry.npmmirror.com/@types/node/-/node-24.7.1.tgz",
"integrity": "sha512-CmyhGZanP88uuC5GpWU9q+fI61j2SkhO3UGMUdfYRE6Bcy0ccyzn1Rqj9YAB/ZY4kOXmNf0ocah5GtphmLMP6Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"undici-types": "~7.14.0"
@@ -2737,7 +2747,6 @@
"version": "7.14.0",
"resolved": "https://registry.npmmirror.com/undici-types/-/undici-types-7.14.0.tgz",
"integrity": "sha512-QQiYxHuyZ9gQUIrmPo3IA+hUl4KYk8uSA7cHrcKd/l3p1OTpZcM0Tbp9x7FAtXdAYhlasd60ncPpgu6ihG6TOA==",
"dev": true,
"license": "MIT"
},
"node_modules/v8-compile-cache-lib": {

View File

@@ -25,9 +25,11 @@
"@fastify/cors": "^11.1.0",
"@fastify/jwt": "^10.0.0",
"@prisma/client": "^6.17.0",
"@types/form-data": "^2.2.1",
"axios": "^1.12.2",
"dotenv": "^17.2.3",
"fastify": "^5.6.1",
"form-data": "^4.0.4",
"js-yaml": "^4.1.0",
"prisma": "^6.17.0",
"zod": "^4.1.12"

View File

@@ -0,0 +1,323 @@
import axios, { AxiosInstance, AxiosError } from 'axios';
import FormData from 'form-data';
import {
Dataset,
CreateDatasetRequest,
CreateDatasetResponse,
DatasetListResponse,
Document,
DocumentListResponse,
CreateDocumentByFileRequest,
CreateDocumentResponse,
RetrievalRequest,
RetrievalResponse,
DifyError,
DifyErrorResponse,
} from './types.js';
import { config } from '../config/env.js';
/**
* Dify API 客户端
*
* 封装 Dify 知识库相关 API
*/
export class DifyClient {
private client: AxiosInstance;
private apiKey: string;
private apiUrl: string;
constructor(apiKey?: string, apiUrl?: string) {
this.apiKey = apiKey || config.difyApiKey;
this.apiUrl = apiUrl || config.difyApiUrl;
if (!this.apiKey) {
throw new Error('Dify API Key is required');
}
if (!this.apiUrl) {
throw new Error('Dify API URL is required');
}
// 创建 axios 实例
this.client = axios.create({
baseURL: this.apiUrl,
headers: {
'Authorization': `Bearer ${this.apiKey}`,
'Content-Type': 'application/json',
},
timeout: 30000, // 30秒超时
});
// 响应拦截器:统一错误处理
this.client.interceptors.response.use(
(response) => response,
(error: AxiosError) => {
if (error.response?.data) {
const errorData = error.response.data as DifyErrorResponse;
throw new DifyError({
code: errorData.code || 'UNKNOWN_ERROR',
message: errorData.message || error.message,
status: error.response.status,
});
}
throw error;
}
);
}
// ==================== 知识库管理 API ====================
/**
* 创建知识库
*
* @param params 创建参数
* @returns 创建的知识库信息
*/
async createDataset(params: CreateDatasetRequest): Promise<CreateDatasetResponse> {
const response = await this.client.post<CreateDatasetResponse>('/datasets', params);
return response.data;
}
/**
* 获取知识库列表
*
* @param page 页码从1开始
* @param limit 每页数量默认20
* @returns 知识库列表
*/
async getDatasets(page: number = 1, limit: number = 20): Promise<DatasetListResponse> {
const response = await this.client.get<DatasetListResponse>('/datasets', {
params: { page, limit },
});
return response.data;
}
/**
* 获取知识库详情
*
* @param datasetId 知识库ID
* @returns 知识库信息
*/
async getDataset(datasetId: string): Promise<Dataset> {
const response = await this.client.get<Dataset>(`/datasets/${datasetId}`);
return response.data;
}
/**
* 删除知识库
*
* @param datasetId 知识库ID
*/
async deleteDataset(datasetId: string): Promise<void> {
await this.client.delete(`/datasets/${datasetId}`);
}
// ==================== 文档管理 API ====================
/**
* 直接上传文档到知识库(简化版)
*
* @param datasetId 知识库ID
* @param file 文件 Buffer
* @param filename 文件名
* @param params 创建参数
* @returns 创建的文档信息
*/
async uploadDocumentDirectly(
datasetId: string,
file: Buffer,
filename: string,
params?: Partial<CreateDocumentByFileRequest>
): Promise<CreateDocumentResponse> {
const formData = new FormData();
formData.append('file', file, filename);
// 添加其他参数
const defaultParams = {
indexing_technique: 'high_quality',
process_rule: {
mode: 'automatic',
rules: {
pre_processing_rules: [
{ id: 'remove_extra_spaces', enabled: true },
{ id: 'remove_urls_emails', enabled: false },
],
segmentation: {
separator: '\n',
max_tokens: 500,
},
},
},
...params,
};
formData.append('data', JSON.stringify(defaultParams));
const response = await this.client.post<CreateDocumentResponse>(
`/datasets/${datasetId}/document/create_by_file`,
formData,
{
headers: {
...formData.getHeaders(),
'Authorization': `Bearer ${this.apiKey}`,
},
}
);
return response.data;
}
/**
* 获取文档列表
*
* @param datasetId 知识库ID
* @param page 页码从1开始
* @param limit 每页数量默认20
* @returns 文档列表
*/
async getDocuments(
datasetId: string,
page: number = 1,
limit: number = 20
): Promise<DocumentListResponse> {
const response = await this.client.get<DocumentListResponse>(
`/datasets/${datasetId}/documents`,
{
params: { page, limit },
}
);
return response.data;
}
/**
* 获取文档详情
*
* @param datasetId 知识库ID
* @param documentId 文档ID
* @returns 文档信息
*/
async getDocument(datasetId: string, documentId: string): Promise<Document> {
const response = await this.client.get<Document>(
`/datasets/${datasetId}/documents/${documentId}`
);
return response.data;
}
/**
* 删除文档
*
* @param datasetId 知识库ID
* @param documentId 文档ID
*/
async deleteDocument(datasetId: string, documentId: string): Promise<void> {
await this.client.delete(`/datasets/${datasetId}/documents/${documentId}`);
}
/**
* 更新文档(重新索引)
*
* @param datasetId 知识库ID
* @param documentId 文档ID
*/
async updateDocument(datasetId: string, documentId: string): Promise<void> {
await this.client.post(`/datasets/${datasetId}/documents/${documentId}/processing`);
}
// ==================== 知识库检索 API ====================
/**
* 检索知识库
*
* @param datasetId 知识库ID
* @param query 查询文本
* @param params 检索参数
* @returns 检索结果
*/
async retrieveKnowledge(
datasetId: string,
query: string,
params?: Partial<RetrievalRequest>
): Promise<RetrievalResponse> {
const requestParams: RetrievalRequest = {
query,
retrieval_model: {
search_method: 'semantic_search',
reranking_enable: false,
top_k: 3,
score_threshold_enabled: false,
...params?.retrieval_model,
},
};
const response = await this.client.post<RetrievalResponse>(
`/datasets/${datasetId}/retrieve`,
requestParams
);
return response.data;
}
// ==================== 辅助方法 ====================
/**
* 轮询检查文档处理状态
*
* @param datasetId 知识库ID
* @param documentId 文档ID
* @param maxAttempts 最大尝试次数默认30次
* @param interval 轮询间隔毫秒默认2000ms
* @returns 文档信息
*/
async waitForDocumentProcessing(
datasetId: string,
documentId: string,
maxAttempts: number = 30,
interval: number = 2000
): Promise<Document> {
for (let i = 0; i < maxAttempts; i++) {
const document = await this.getDocument(datasetId, documentId);
if (document.indexing_status === 'completed') {
return document;
}
if (document.indexing_status === 'error') {
throw new Error(`Document processing failed: ${document.error || 'Unknown error'}`);
}
// 等待后继续
await new Promise((resolve) => setTimeout(resolve, interval));
}
throw new Error('Document processing timeout');
}
/**
* 一键上传文档到知识库(上传 + 等待处理完成)
*
* @param datasetId 知识库ID
* @param file 文件 Buffer
* @param filename 文件名
* @returns 处理完成的文档信息
*/
async uploadAndProcessDocument(
datasetId: string,
file: Buffer,
filename: string
): Promise<Document> {
// 1. 直接上传文档
const createResult = await this.uploadDocumentDirectly(datasetId, file, filename);
// 2. 等待处理完成
const document = await this.waitForDocumentProcessing(
datasetId,
createResult.document.id
);
return document;
}
}
// 导出单例实例
export const difyClient = new DifyClient();

View File

@@ -0,0 +1,230 @@
/**
* Dify API 类型定义
*/
// ==================== 知识库相关类型 ====================
/**
* 知识库信息
*/
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 CreateDatasetRequest {
name: string;
description?: string;
permission?: 'only_me' | 'all_team_members';
indexing_technique?: 'high_quality' | 'economy';
embedding_model?: string;
embedding_model_provider?: string;
retrieval_model?: {
search_method: '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;
};
}
/**
* 创建知识库响应
*/
export interface CreateDatasetResponse {
id: string;
name: string;
description: string;
permission: string;
data_source_type: string;
indexing_technique: string;
created_by: string;
created_at: number;
}
/**
* 知识库列表响应
*/
export interface DatasetListResponse {
data: Dataset[];
has_more: boolean;
limit: number;
total: number;
page: number;
}
// ==================== 文档相关类型 ====================
/**
* 文档信息
*/
export interface Document {
id: string;
position: number;
data_source_type: string;
data_source_info: {
upload_file_id: string;
};
dataset_process_rule_id: string;
name: string;
created_from: string;
created_by: string;
created_at: number;
tokens: number;
indexing_status: 'waiting' | 'parsing' | 'cleaning' | 'splitting' | 'indexing' | 'completed' | 'error' | 'paused';
error?: string;
enabled: boolean;
disabled_at?: number;
disabled_by?: string;
archived: boolean;
display_status: string;
word_count: number;
hit_count: number;
}
/**
* 文档列表响应
*/
export interface DocumentListResponse {
data: Document[];
has_more: boolean;
limit: number;
total: number;
page: number;
}
/**
* 上传文件响应
*/
export interface UploadFileResponse {
id: string;
name: string;
size: number;
extension: string;
mime_type: string;
created_by: string;
created_at: number;
}
/**
* 创建文档(从上传的文件)请求参数
*/
export interface CreateDocumentByFileRequest {
indexing_technique: 'high_quality' | 'economy';
process_rule: {
rules: {
pre_processing_rules: Array<{
id: string;
enabled: boolean;
}>;
segmentation: {
separator: string;
max_tokens: number;
};
};
mode: 'automatic' | 'custom';
};
original_document_id?: string;
doc_form?: 'text_model' | 'qa_model';
doc_language?: string;
}
/**
* 创建文档响应
*/
export interface CreateDocumentResponse {
document: Document;
batch: string;
}
// ==================== 知识库检索相关类型 ====================
/**
* 知识库检索请求参数
*/
export interface RetrievalRequest {
query: string;
retrieval_model?: {
search_method?: '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;
};
}
/**
* 检索结果项
*/
export interface RetrievalRecord {
segment_id: string;
document_id: string;
document_name: string;
position: number;
score: number;
content: string;
hit_count: number;
word_count: number;
segment_position: number;
index_node_hash: string;
metadata: Record<string, any>;
}
/**
* 知识库检索响应
*/
export interface RetrievalResponse {
query: {
content: string;
};
records: RetrievalRecord[];
}
// ==================== 错误类型 ====================
/**
* Dify API 错误响应
*/
export interface DifyErrorResponse {
code: string;
message: string;
status: number;
}
/**
* Dify API 错误
*/
export class DifyError extends Error {
code: string;
status: number;
constructor(error: DifyErrorResponse) {
super(error.message);
this.name = 'DifyError';
this.code = error.code;
this.status = error.status;
}
}

View File

@@ -0,0 +1,261 @@
/**
* Dify客户端测试脚本
*
* 用于测试Dify API的连接和基本功能
*/
import { DifyClient } from '../clients/DifyClient.js';
// 创建客户端实例
const client = new DifyClient();
// 测试知识库ID测试时会创建
let testDatasetId: string;
let testDocumentId: string;
/**
* 测试1创建知识库
*/
async function testCreateDataset() {
console.log('\n========== 测试1创建知识库 ==========');
try {
const result = await client.createDataset({
name: '测试知识库 - ' + Date.now(),
description: '这是一个测试知识库用于验证API功能',
indexing_technique: 'high_quality',
});
testDatasetId = result.id;
console.log('✅ 创建成功');
console.log('知识库ID:', result.id);
console.log('知识库名称:', result.name);
console.log('创建时间:', new Date(result.created_at * 1000).toLocaleString());
} catch (error: any) {
console.error('❌ 创建失败:', error.message);
throw error;
}
}
/**
* 测试2获取知识库列表
*/
async function testGetDatasets() {
console.log('\n========== 测试2获取知识库列表 ==========');
try {
const result = await client.getDatasets(1, 10);
console.log('✅ 获取成功');
console.log(`找到 ${result.total} 个知识库`);
if (result.data.length > 0) {
console.log('前3个知识库:');
result.data.slice(0, 3).forEach((dataset, index) => {
console.log(` ${index + 1}. ${dataset.name} (ID: ${dataset.id})`);
console.log(` - 文档数量: ${dataset.document_count}`);
console.log(` - 创建时间: ${new Date(dataset.created_at * 1000).toLocaleString()}`);
});
}
} catch (error: any) {
console.error('❌ 获取失败:', error.message);
throw error;
}
}
/**
* 测试3获取知识库详情
*/
async function testGetDataset() {
console.log('\n========== 测试3获取知识库详情 ==========');
try {
const result = await client.getDataset(testDatasetId);
console.log('✅ 获取成功');
console.log('知识库名称:', result.name);
console.log('描述:', result.description);
console.log('文档数量:', result.document_count);
console.log('索引技术:', result.indexing_technique);
} catch (error: any) {
console.error('❌ 获取失败:', error.message);
throw error;
}
}
/**
* 测试4上传文件并创建文档
*/
async function testUploadDocument() {
console.log('\n========== 测试4上传文档 ==========');
try {
// 创建一个测试文本文件
const testContent = `
# 临床研究测试文档
## 研究背景
这是一个用于测试Dify API的示例文档。
## 研究目标
验证以下功能:
1. 文件上传功能
2. 文档索引功能
3. 知识库检索功能
## 研究方法
使用自动化测试脚本验证API的各项功能。
## 预期结果
所有测试应该通过,并返回正确的数据。
`.trim();
const testBuffer = Buffer.from(testContent, 'utf-8');
const filename = 'test-document.txt';
console.log('正在上传文档...');
const document = await client.uploadAndProcessDocument(
testDatasetId,
testBuffer,
filename
);
testDocumentId = document.id;
console.log('✅ 上传成功');
console.log('文档ID:', document.id);
console.log('文档名称:', document.name);
console.log('处理状态:', document.indexing_status);
console.log('字符数:', document.word_count);
console.log('Token数:', document.tokens);
} catch (error: any) {
console.error('❌ 上传失败:', error.message);
throw error;
}
}
/**
* 测试5获取文档列表
*/
async function testGetDocuments() {
console.log('\n========== 测试5获取文档列表 ==========');
try {
const result = await client.getDocuments(testDatasetId, 1, 10);
console.log('✅ 获取成功');
console.log(`找到 ${result.total} 个文档`);
if (result.data.length > 0) {
console.log('文档列表:');
result.data.forEach((doc, index) => {
console.log(` ${index + 1}. ${doc.name}`);
console.log(` - 状态: ${doc.indexing_status}`);
console.log(` - 字符数: ${doc.word_count}`);
console.log(` - Token数: ${doc.tokens}`);
});
}
} catch (error: any) {
console.error('❌ 获取失败:', error.message);
throw error;
}
}
/**
* 测试6知识库检索
*/
async function testRetrieveKnowledge() {
console.log('\n========== 测试6知识库检索 ==========');
try {
const result = await client.retrieveKnowledge(
testDatasetId,
'研究目标是什么?',
{
retrieval_model: {
search_method: 'semantic_search',
top_k: 3,
},
}
);
console.log('✅ 检索成功');
console.log('查询:', result.query.content);
console.log(`找到 ${result.records.length} 条结果`);
result.records.forEach((record, index) => {
console.log(`\n结果 ${index + 1}:`);
console.log(` 文档: ${record.document_name}`);
console.log(` 相似度得分: ${record.score.toFixed(4)}`);
console.log(` 内容片段: ${record.content.substring(0, 100)}...`);
});
} catch (error: any) {
console.error('❌ 检索失败:', error.message);
// 不抛出错误,因为可能是文档还未完全索引
}
}
/**
* 测试7删除文档
*/
async function testDeleteDocument() {
console.log('\n========== 测试7删除文档 ==========');
try {
await client.deleteDocument(testDatasetId, testDocumentId);
console.log('✅ 删除文档成功');
} catch (error: any) {
console.error('❌ 删除文档失败:', error.message);
// 不抛出错误,继续后续测试
}
}
/**
* 测试8删除知识库
*/
async function testDeleteDataset() {
console.log('\n========== 测试8删除知识库 ==========');
try {
await client.deleteDataset(testDatasetId);
console.log('✅ 删除知识库成功');
} catch (error: any) {
console.error('❌ 删除知识库失败:', error.message);
throw error;
}
}
/**
* 运行所有测试
*/
async function runAllTests() {
console.log('========================================');
console.log(' Dify API 客户端测试');
console.log('========================================');
console.log('开始时间:', new Date().toLocaleString());
try {
await testCreateDataset();
await testGetDatasets();
await testGetDataset();
await testUploadDocument();
await testGetDocuments();
// 等待5秒让文档完全索引
console.log('\n等待5秒让文档完全索引...');
await new Promise((resolve) => setTimeout(resolve, 5000));
await testRetrieveKnowledge();
await testDeleteDocument();
await testDeleteDataset();
console.log('\n========================================');
console.log('✅ 所有测试通过!');
console.log('========================================');
console.log('结束时间:', new Date().toLocaleString());
} catch (error: any) {
console.log('\n========================================');
console.log('❌ 测试失败!');
console.log('========================================');
console.error('错误信息:', error.message);
if (error.code) {
console.error('错误代码:', error.code);
}
if (error.status) {
console.error('HTTP状态码:', error.status);
}
process.exit(1);
}
}
// 执行测试
runAllTests();