style: format with vp fmt (#38803)

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
This commit is contained in:
Stephen Zhou
2026-07-12 15:57:46 +00:00
committed by GitHub
co-authored by autofix-ci[bot]
parent fde08d24fe
commit a84c2d36a3
6213 changed files with 227959 additions and 187183 deletions
+6 -7
View File
@@ -19,7 +19,7 @@ import {
CompletionClient,
WorkflowClient,
KnowledgeBaseClient,
WorkspaceClient
WorkspaceClient,
} from 'dify-client'
const API_KEY = 'your-app-api-key'
@@ -42,7 +42,7 @@ await client.messageFeedback('message-id', 'like', user)
await completionClient.createCompletionMessage({
inputs: { query },
user,
response_mode: 'blocking'
response_mode: 'blocking',
})
// Chat (streaming)
@@ -50,7 +50,7 @@ const stream = await chatClient.createChatMessage({
inputs: {},
query,
user,
response_mode: 'streaming'
response_mode: 'streaming',
})
for await (const event of stream) {
console.log(event.event, event.data)
@@ -62,14 +62,14 @@ await chatClient.createChatMessage({
query,
user,
workflow_id: 'workflow-id',
response_mode: 'blocking'
response_mode: 'blocking',
})
// Workflow run (blocking or streaming)
await workflowClient.run({
inputs: { query },
user,
response_mode: 'blocking'
response_mode: 'blocking',
})
// Knowledge base (dataset token required)
@@ -83,7 +83,7 @@ const pipelineStream = await kbClient.runPipeline('dataset-id', {
datasource_info_list: [],
start_node_id: 'start-node-id',
is_published: true,
response_mode: 'streaming'
response_mode: 'streaming',
})
for await (const event of pipelineStream) {
console.log(event.data)
@@ -91,7 +91,6 @@ for await (const event of pipelineStream) {
// Workspace models (dataset token required)
await workspaceClient.getModelsByType('text-embedding')
```
Notes:
+25 -25
View File
@@ -2,32 +2,19 @@
"name": "dify-client",
"version": "3.1.0",
"description": "This is the Node.js SDK for the Dify.AI API, which allows you to easily integrate Dify.AI into your Node.js applications.",
"type": "module",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js"
}
},
"engines": {
"node": ">=18.0.0"
},
"files": [
"dist/index.js",
"dist/index.d.ts",
"README.md",
"LICENSE"
],
"keywords": [
"AI",
"API",
"Dify",
"Dify.AI",
"LLM",
"AI",
"SDK",
"API"
"SDK"
],
"homepage": "https://dify.ai",
"bugs": {
"url": "https://github.com/langgenius/dify/issues"
},
"license": "MIT",
"author": "LangGenius",
"contributors": [
"Joel <iamjoel007@gmail.com> (https://github.com/iamjoel)",
@@ -39,11 +26,21 @@
"url": "https://github.com/langgenius/dify.git",
"directory": "sdks/nodejs-client"
},
"bugs": {
"url": "https://github.com/langgenius/dify/issues"
"files": [
"dist/index.js",
"dist/index.d.ts",
"README.md",
"LICENSE"
],
"type": "module",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js"
}
},
"homepage": "https://dify.ai",
"license": "MIT",
"scripts": {
"build": "vp pack",
"lint": "eslint",
@@ -67,5 +64,8 @@
"vite": "catalog:",
"vite-plus": "catalog:",
"vitest": "catalog:"
},
"engines": {
"node": ">=18.0.0"
}
}
+119 -119
View File
@@ -1,175 +1,175 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import { ValidationError } from "../errors/dify-error";
import { DifyClient } from "./base";
import { createHttpClientWithSpies } from "../../tests/test-utils";
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { createHttpClientWithSpies } from '../../tests/test-utils'
import { ValidationError } from '../errors/dify-error'
import { DifyClient } from './base'
describe("DifyClient base", () => {
describe('DifyClient base', () => {
beforeEach(() => {
vi.restoreAllMocks();
});
vi.restoreAllMocks()
})
it("getRoot calls root endpoint", async () => {
const { client, request } = createHttpClientWithSpies();
const dify = new DifyClient(client);
it('getRoot calls root endpoint', async () => {
const { client, request } = createHttpClientWithSpies()
const dify = new DifyClient(client)
await dify.getRoot();
await dify.getRoot()
expect(request).toHaveBeenCalledWith({
method: "GET",
path: "/",
});
});
method: 'GET',
path: '/',
})
})
it("getApplicationParameters includes optional user", async () => {
const { client, request } = createHttpClientWithSpies();
const dify = new DifyClient(client);
it('getApplicationParameters includes optional user', async () => {
const { client, request } = createHttpClientWithSpies()
const dify = new DifyClient(client)
await dify.getApplicationParameters();
await dify.getApplicationParameters()
expect(request).toHaveBeenCalledWith({
method: "GET",
path: "/parameters",
method: 'GET',
path: '/parameters',
query: undefined,
});
})
await dify.getApplicationParameters("user-1");
await dify.getApplicationParameters('user-1')
expect(request).toHaveBeenCalledWith({
method: "GET",
path: "/parameters",
query: { user: "user-1" },
});
});
method: 'GET',
path: '/parameters',
query: { user: 'user-1' },
})
})
it("getMeta includes optional user", async () => {
const { client, request } = createHttpClientWithSpies();
const dify = new DifyClient(client);
it('getMeta includes optional user', async () => {
const { client, request } = createHttpClientWithSpies()
const dify = new DifyClient(client)
await dify.getMeta("user-1");
await dify.getMeta('user-1')
expect(request).toHaveBeenCalledWith({
method: "GET",
path: "/meta",
query: { user: "user-1" },
});
});
method: 'GET',
path: '/meta',
query: { user: 'user-1' },
})
})
it("getInfo and getSite support optional user", async () => {
const { client, request } = createHttpClientWithSpies();
const dify = new DifyClient(client);
it('getInfo and getSite support optional user', async () => {
const { client, request } = createHttpClientWithSpies()
const dify = new DifyClient(client)
await dify.getInfo();
await dify.getSite("user");
await dify.getInfo()
await dify.getSite('user')
expect(request).toHaveBeenCalledWith({
method: "GET",
path: "/info",
method: 'GET',
path: '/info',
query: undefined,
});
})
expect(request).toHaveBeenCalledWith({
method: "GET",
path: "/site",
query: { user: "user" },
});
});
method: 'GET',
path: '/site',
query: { user: 'user' },
})
})
it("messageFeedback builds payload from request object", async () => {
const { client, request } = createHttpClientWithSpies();
const dify = new DifyClient(client);
it('messageFeedback builds payload from request object', async () => {
const { client, request } = createHttpClientWithSpies()
const dify = new DifyClient(client)
await dify.messageFeedback({
messageId: "msg",
user: "user",
rating: "like",
content: "good",
});
messageId: 'msg',
user: 'user',
rating: 'like',
content: 'good',
})
expect(request).toHaveBeenCalledWith({
method: "POST",
path: "/messages/msg/feedbacks",
data: { user: "user", rating: "like", content: "good" },
});
});
method: 'POST',
path: '/messages/msg/feedbacks',
data: { user: 'user', rating: 'like', content: 'good' },
})
})
it("fileUpload appends user to form data", async () => {
const { client, request } = createHttpClientWithSpies();
const dify = new DifyClient(client);
const form = { append: vi.fn(), getHeaders: () => ({}) };
it('fileUpload appends user to form data', async () => {
const { client, request } = createHttpClientWithSpies()
const dify = new DifyClient(client)
const form = { append: vi.fn(), getHeaders: () => ({}) }
await dify.fileUpload(form, "user");
await dify.fileUpload(form, 'user')
expect(form.append).toHaveBeenCalledWith("user", "user");
expect(form.append).toHaveBeenCalledWith('user', 'user')
expect(request).toHaveBeenCalledWith({
method: "POST",
path: "/files/upload",
method: 'POST',
path: '/files/upload',
data: form,
});
});
})
})
it("filePreview uses bytes response", async () => {
const { client, request } = createHttpClientWithSpies();
const dify = new DifyClient(client);
it('filePreview uses bytes response', async () => {
const { client, request } = createHttpClientWithSpies()
const dify = new DifyClient(client)
await dify.filePreview("file", "user", true);
await dify.filePreview('file', 'user', true)
expect(request).toHaveBeenCalledWith({
method: "GET",
path: "/files/file/preview",
query: { user: "user", as_attachment: "true" },
responseType: "bytes",
});
});
method: 'GET',
path: '/files/file/preview',
query: { user: 'user', as_attachment: 'true' },
responseType: 'bytes',
})
})
it("audioToText appends user and sends form", async () => {
const { client, request } = createHttpClientWithSpies();
const dify = new DifyClient(client);
const form = { append: vi.fn(), getHeaders: () => ({}) };
it('audioToText appends user and sends form', async () => {
const { client, request } = createHttpClientWithSpies()
const dify = new DifyClient(client)
const form = { append: vi.fn(), getHeaders: () => ({}) }
await dify.audioToText(form, "user");
await dify.audioToText(form, 'user')
expect(form.append).toHaveBeenCalledWith("user", "user");
expect(form.append).toHaveBeenCalledWith('user', 'user')
expect(request).toHaveBeenCalledWith({
method: "POST",
path: "/audio-to-text",
method: 'POST',
path: '/audio-to-text',
data: form,
});
});
})
})
it("textToAudio supports streaming and message id", async () => {
const { client, request, requestBinaryStream } = createHttpClientWithSpies();
const dify = new DifyClient(client);
it('textToAudio supports streaming and message id', async () => {
const { client, request, requestBinaryStream } = createHttpClientWithSpies()
const dify = new DifyClient(client)
await dify.textToAudio({
user: "user",
message_id: "msg",
user: 'user',
message_id: 'msg',
streaming: true,
});
})
expect(requestBinaryStream).toHaveBeenCalledWith({
method: "POST",
path: "/text-to-audio",
method: 'POST',
path: '/text-to-audio',
data: {
user: "user",
message_id: "msg",
user: 'user',
message_id: 'msg',
streaming: true,
},
});
})
await dify.textToAudio("hello", "user", false, "voice");
await dify.textToAudio('hello', 'user', false, 'voice')
expect(request).toHaveBeenCalledWith({
method: "POST",
path: "/text-to-audio",
method: 'POST',
path: '/text-to-audio',
data: {
text: "hello",
user: "user",
text: 'hello',
user: 'user',
streaming: false,
voice: "voice",
voice: 'voice',
},
responseType: "bytes",
});
});
responseType: 'bytes',
})
})
it("textToAudio requires text or message id", () => {
const { client } = createHttpClientWithSpies();
const dify = new DifyClient(client);
it('textToAudio requires text or message id', () => {
const { client } = createHttpClientWithSpies()
const dify = new DifyClient(client)
expect(() => dify.textToAudio({ user: "user" })).toThrow(ValidationError);
});
});
expect(() => dify.textToAudio({ user: 'user' })).toThrow(ValidationError)
})
})
+114 -125
View File
@@ -1,3 +1,5 @@
import type { HttpRequestBody } from '../http/client'
import type { SdkFormData } from '../http/form-data'
import type {
BinaryStream,
DifyClientConfig,
@@ -8,49 +10,44 @@ import type {
RequestMethod,
SuccessResponse,
TextToAudioRequest,
} from "../types/common";
import type { HttpRequestBody } from "../http/client";
import { HttpClient } from "../http/client";
import { ensureNonEmptyString, ensureRating } from "./validation";
import { FileUploadError, ValidationError } from "../errors/dify-error";
import type { SdkFormData } from "../http/form-data";
import { isFormData } from "../http/form-data";
} from '../types/common'
import { FileUploadError, ValidationError } from '../errors/dify-error'
import { HttpClient } from '../http/client'
import { isFormData } from '../http/form-data'
import { ensureNonEmptyString, ensureRating } from './validation'
const toConfig = (
init: string | DifyClientConfig,
baseUrl?: string
): DifyClientConfig => {
if (typeof init === "string") {
const toConfig = (init: string | DifyClientConfig, baseUrl?: string): DifyClientConfig => {
if (typeof init === 'string') {
return {
apiKey: init,
baseUrl,
};
}
}
return init;
};
return init
}
const appendUserToFormData = (form: SdkFormData, user: string): void => {
form.append("user", user);
};
form.append('user', user)
}
export class DifyClient {
protected http: HttpClient;
protected http: HttpClient
constructor(config: string | DifyClientConfig | HttpClient, baseUrl?: string) {
if (config instanceof HttpClient) {
this.http = config;
this.http = config
} else {
this.http = new HttpClient(toConfig(config, baseUrl));
this.http = new HttpClient(toConfig(config, baseUrl))
}
}
updateApiKey(apiKey: string): void {
ensureNonEmptyString(apiKey, "apiKey");
this.http.updateApiKey(apiKey);
ensureNonEmptyString(apiKey, 'apiKey')
this.http.updateApiKey(apiKey)
}
getHttpClient(): HttpClient {
return this.http;
return this.http
}
sendRequest(
@@ -59,225 +56,217 @@ export class DifyClient {
data: HttpRequestBody = null,
params: QueryParams | null = null,
stream = false,
headerParams: Record<string, string> = {}
): ReturnType<HttpClient["requestRaw"]> {
headerParams: Record<string, string> = {},
): ReturnType<HttpClient['requestRaw']> {
return this.http.requestRaw({
method,
path: endpoint,
data,
query: params ?? undefined,
headers: headerParams,
responseType: stream ? "stream" : "json",
});
responseType: stream ? 'stream' : 'json',
})
}
getRoot(): Promise<DifyResponse<JsonObject>> {
return this.http.request({
method: "GET",
path: "/",
});
method: 'GET',
path: '/',
})
}
getApplicationParameters(user?: string): Promise<DifyResponse<JsonObject>> {
if (user) {
ensureNonEmptyString(user, "user");
ensureNonEmptyString(user, 'user')
}
return this.http.request({
method: "GET",
path: "/parameters",
method: 'GET',
path: '/parameters',
query: user ? { user } : undefined,
});
})
}
async getParameters(user?: string): Promise<DifyResponse<JsonObject>> {
return this.getApplicationParameters(user);
return this.getApplicationParameters(user)
}
getMeta(user?: string): Promise<DifyResponse<JsonObject>> {
if (user) {
ensureNonEmptyString(user, "user");
ensureNonEmptyString(user, 'user')
}
return this.http.request({
method: "GET",
path: "/meta",
method: 'GET',
path: '/meta',
query: user ? { user } : undefined,
});
})
}
messageFeedback(
request: MessageFeedbackRequest
): Promise<DifyResponse<SuccessResponse>>;
messageFeedback(request: MessageFeedbackRequest): Promise<DifyResponse<SuccessResponse>>
messageFeedback(
messageId: string,
rating: "like" | "dislike" | null,
rating: 'like' | 'dislike' | null,
user: string,
content?: string
): Promise<DifyResponse<SuccessResponse>>;
content?: string,
): Promise<DifyResponse<SuccessResponse>>
messageFeedback(
messageIdOrRequest: string | MessageFeedbackRequest,
rating?: "like" | "dislike" | null,
rating?: 'like' | 'dislike' | null,
user?: string,
content?: string
content?: string,
): Promise<DifyResponse<SuccessResponse>> {
let messageId: string;
const payload: JsonObject = {};
let messageId: string
const payload: JsonObject = {}
if (typeof messageIdOrRequest === "string") {
messageId = messageIdOrRequest;
ensureNonEmptyString(messageId, "messageId");
ensureNonEmptyString(user, "user");
payload.user = user;
if (typeof messageIdOrRequest === 'string') {
messageId = messageIdOrRequest
ensureNonEmptyString(messageId, 'messageId')
ensureNonEmptyString(user, 'user')
payload.user = user
if (rating !== undefined && rating !== null) {
ensureRating(rating);
payload.rating = rating;
ensureRating(rating)
payload.rating = rating
}
if (content !== undefined) {
payload.content = content;
payload.content = content
}
} else {
const request = messageIdOrRequest;
messageId = request.messageId;
ensureNonEmptyString(messageId, "messageId");
ensureNonEmptyString(request.user, "user");
payload.user = request.user;
const request = messageIdOrRequest
messageId = request.messageId
ensureNonEmptyString(messageId, 'messageId')
ensureNonEmptyString(request.user, 'user')
payload.user = request.user
if (request.rating !== undefined && request.rating !== null) {
ensureRating(request.rating);
payload.rating = request.rating;
ensureRating(request.rating)
payload.rating = request.rating
}
if (request.content !== undefined) {
payload.content = request.content;
payload.content = request.content
}
}
return this.http.request({
method: "POST",
method: 'POST',
path: `/messages/${messageId}/feedbacks`,
data: payload,
});
})
}
getInfo(user?: string): Promise<DifyResponse<JsonObject>> {
if (user) {
ensureNonEmptyString(user, "user");
ensureNonEmptyString(user, 'user')
}
return this.http.request({
method: "GET",
path: "/info",
method: 'GET',
path: '/info',
query: user ? { user } : undefined,
});
})
}
getSite(user?: string): Promise<DifyResponse<JsonObject>> {
if (user) {
ensureNonEmptyString(user, "user");
ensureNonEmptyString(user, 'user')
}
return this.http.request({
method: "GET",
path: "/site",
method: 'GET',
path: '/site',
query: user ? { user } : undefined,
});
})
}
fileUpload(form: unknown, user: string): Promise<DifyResponse<JsonObject>> {
if (!isFormData(form)) {
throw new FileUploadError("FormData is required for file uploads");
throw new FileUploadError('FormData is required for file uploads')
}
ensureNonEmptyString(user, "user");
appendUserToFormData(form, user);
ensureNonEmptyString(user, 'user')
appendUserToFormData(form, user)
return this.http.request({
method: "POST",
path: "/files/upload",
method: 'POST',
path: '/files/upload',
data: form,
});
})
}
filePreview(
fileId: string,
user: string,
asAttachment?: boolean
): Promise<DifyResponse<Buffer>> {
ensureNonEmptyString(fileId, "fileId");
ensureNonEmptyString(user, "user");
return this.http.request<Buffer, "bytes">({
method: "GET",
filePreview(fileId: string, user: string, asAttachment?: boolean): Promise<DifyResponse<Buffer>> {
ensureNonEmptyString(fileId, 'fileId')
ensureNonEmptyString(user, 'user')
return this.http.request<Buffer, 'bytes'>({
method: 'GET',
path: `/files/${fileId}/preview`,
query: {
user,
as_attachment: asAttachment ? "true" : undefined,
as_attachment: asAttachment ? 'true' : undefined,
},
responseType: "bytes",
});
responseType: 'bytes',
})
}
audioToText(form: unknown, user: string): Promise<DifyResponse<JsonObject>> {
if (!isFormData(form)) {
throw new FileUploadError("FormData is required for audio uploads");
throw new FileUploadError('FormData is required for audio uploads')
}
ensureNonEmptyString(user, "user");
appendUserToFormData(form, user);
ensureNonEmptyString(user, 'user')
appendUserToFormData(form, user)
return this.http.request({
method: "POST",
path: "/audio-to-text",
method: 'POST',
path: '/audio-to-text',
data: form,
});
})
}
textToAudio(
request: TextToAudioRequest
): Promise<DifyResponse<Buffer> | BinaryStream>;
textToAudio(request: TextToAudioRequest): Promise<DifyResponse<Buffer> | BinaryStream>
textToAudio(
text: string,
user: string,
streaming?: boolean,
voice?: string
): Promise<DifyResponse<Buffer> | BinaryStream>;
voice?: string,
): Promise<DifyResponse<Buffer> | BinaryStream>
textToAudio(
textOrRequest: string | TextToAudioRequest,
user?: string,
streaming = false,
voice?: string
voice?: string,
): Promise<DifyResponse<Buffer> | BinaryStream> {
let payload: TextToAudioRequest;
let payload: TextToAudioRequest
if (typeof textOrRequest === "string") {
ensureNonEmptyString(textOrRequest, "text");
ensureNonEmptyString(user, "user");
if (typeof textOrRequest === 'string') {
ensureNonEmptyString(textOrRequest, 'text')
ensureNonEmptyString(user, 'user')
payload = {
text: textOrRequest,
user,
streaming,
};
}
if (voice) {
payload.voice = voice;
payload.voice = voice
}
} else {
payload = { ...textOrRequest };
ensureNonEmptyString(payload.user, "user");
payload = { ...textOrRequest }
ensureNonEmptyString(payload.user, 'user')
if (payload.text !== undefined && payload.text !== null) {
ensureNonEmptyString(payload.text, "text");
ensureNonEmptyString(payload.text, 'text')
}
if (payload.message_id !== undefined && payload.message_id !== null) {
ensureNonEmptyString(payload.message_id, "messageId");
ensureNonEmptyString(payload.message_id, 'messageId')
}
if (!payload.text && !payload.message_id) {
throw new ValidationError("text or message_id is required");
throw new ValidationError('text or message_id is required')
}
payload.streaming = payload.streaming ?? false;
payload.streaming = payload.streaming ?? false
}
if (payload.streaming) {
return this.http.requestBinaryStream({
method: "POST",
path: "/text-to-audio",
method: 'POST',
path: '/text-to-audio',
data: payload,
});
})
}
return this.http.request<Buffer, "bytes">({
method: "POST",
path: "/text-to-audio",
return this.http.request<Buffer, 'bytes'>({
method: 'POST',
path: '/text-to-audio',
data: payload,
responseType: "bytes",
});
responseType: 'bytes',
})
}
}
+163 -165
View File
@@ -1,239 +1,237 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import { ValidationError } from "../errors/dify-error";
import { ChatClient } from "./chat";
import { createHttpClientWithSpies } from "../../tests/test-utils";
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { createHttpClientWithSpies } from '../../tests/test-utils'
import { ValidationError } from '../errors/dify-error'
import { ChatClient } from './chat'
describe("ChatClient", () => {
describe('ChatClient', () => {
beforeEach(() => {
vi.restoreAllMocks();
});
vi.restoreAllMocks()
})
it("creates chat messages in blocking mode", async () => {
const { client, request } = createHttpClientWithSpies();
const chat = new ChatClient(client);
it('creates chat messages in blocking mode', async () => {
const { client, request } = createHttpClientWithSpies()
const chat = new ChatClient(client)
await chat.createChatMessage({ input: "x" }, "hello", "user", false, null);
await chat.createChatMessage({ input: 'x' }, 'hello', 'user', false, null)
expect(request).toHaveBeenCalledWith({
method: "POST",
path: "/chat-messages",
method: 'POST',
path: '/chat-messages',
data: {
inputs: { input: "x" },
query: "hello",
user: "user",
response_mode: "blocking",
inputs: { input: 'x' },
query: 'hello',
user: 'user',
response_mode: 'blocking',
files: undefined,
},
});
});
})
})
it("creates chat messages in streaming mode", async () => {
const { client, requestStream } = createHttpClientWithSpies();
const chat = new ChatClient(client);
it('creates chat messages in streaming mode', async () => {
const { client, requestStream } = createHttpClientWithSpies()
const chat = new ChatClient(client)
await chat.createChatMessage({
inputs: { input: "x" },
query: "hello",
user: "user",
response_mode: "streaming",
});
inputs: { input: 'x' },
query: 'hello',
user: 'user',
response_mode: 'streaming',
})
expect(requestStream).toHaveBeenCalledWith({
method: "POST",
path: "/chat-messages",
method: 'POST',
path: '/chat-messages',
data: {
inputs: { input: "x" },
query: "hello",
user: "user",
response_mode: "streaming",
inputs: { input: 'x' },
query: 'hello',
user: 'user',
response_mode: 'streaming',
},
});
});
})
})
it("stops chat messages", async () => {
const { client, request } = createHttpClientWithSpies();
const chat = new ChatClient(client);
it('stops chat messages', async () => {
const { client, request } = createHttpClientWithSpies()
const chat = new ChatClient(client)
await chat.stopChatMessage("task", "user");
await chat.stopMessage("task", "user");
await chat.stopChatMessage('task', 'user')
await chat.stopMessage('task', 'user')
expect(request).toHaveBeenCalledWith({
method: "POST",
path: "/chat-messages/task/stop",
data: { user: "user" },
});
});
method: 'POST',
path: '/chat-messages/task/stop',
data: { user: 'user' },
})
})
it("gets suggested questions", async () => {
const { client, request } = createHttpClientWithSpies();
const chat = new ChatClient(client);
it('gets suggested questions', async () => {
const { client, request } = createHttpClientWithSpies()
const chat = new ChatClient(client)
await chat.getSuggested("msg", "user");
await chat.getSuggested('msg', 'user')
expect(request).toHaveBeenCalledWith({
method: "GET",
path: "/messages/msg/suggested",
query: { user: "user" },
});
});
method: 'GET',
path: '/messages/msg/suggested',
query: { user: 'user' },
})
})
it("submits message feedback", async () => {
const { client, request } = createHttpClientWithSpies();
const chat = new ChatClient(client);
it('submits message feedback', async () => {
const { client, request } = createHttpClientWithSpies()
const chat = new ChatClient(client)
await chat.messageFeedback("msg", "like", "user", "good");
await chat.messageFeedback('msg', 'like', 'user', 'good')
await chat.messageFeedback({
messageId: "msg",
user: "user",
rating: "dislike",
});
messageId: 'msg',
user: 'user',
rating: 'dislike',
})
expect(request).toHaveBeenCalledWith({
method: "POST",
path: "/messages/msg/feedbacks",
data: { user: "user", rating: "like", content: "good" },
});
});
method: 'POST',
path: '/messages/msg/feedbacks',
data: { user: 'user', rating: 'like', content: 'good' },
})
})
it("lists app feedbacks", async () => {
const { client, request } = createHttpClientWithSpies();
const chat = new ChatClient(client);
it('lists app feedbacks', async () => {
const { client, request } = createHttpClientWithSpies()
const chat = new ChatClient(client)
await chat.getAppFeedbacks(2, 5);
await chat.getAppFeedbacks(2, 5)
expect(request).toHaveBeenCalledWith({
method: "GET",
path: "/app/feedbacks",
method: 'GET',
path: '/app/feedbacks',
query: { page: 2, limit: 5 },
});
});
})
})
it("lists conversations and messages", async () => {
const { client, request } = createHttpClientWithSpies();
const chat = new ChatClient(client);
it('lists conversations and messages', async () => {
const { client, request } = createHttpClientWithSpies()
const chat = new ChatClient(client)
await chat.getConversations("user", "last", 10, "-updated_at");
await chat.getConversationMessages("user", "conv", "first", 5);
await chat.getConversations('user', 'last', 10, '-updated_at')
await chat.getConversationMessages('user', 'conv', 'first', 5)
expect(request).toHaveBeenCalledWith({
method: "GET",
path: "/conversations",
method: 'GET',
path: '/conversations',
query: {
user: "user",
last_id: "last",
user: 'user',
last_id: 'last',
limit: 10,
sort_by: "-updated_at",
sort_by: '-updated_at',
},
});
})
expect(request).toHaveBeenCalledWith({
method: "GET",
path: "/messages",
method: 'GET',
path: '/messages',
query: {
user: "user",
conversation_id: "conv",
first_id: "first",
user: 'user',
conversation_id: 'conv',
first_id: 'first',
limit: 5,
},
});
});
})
})
it("renames conversations with optional auto-generate", async () => {
const { client, request } = createHttpClientWithSpies();
const chat = new ChatClient(client);
it('renames conversations with optional auto-generate', async () => {
const { client, request } = createHttpClientWithSpies()
const chat = new ChatClient(client)
await chat.renameConversation("conv", "name", "user", false);
await chat.renameConversation("conv", "user", { autoGenerate: true });
await chat.renameConversation('conv', 'name', 'user', false)
await chat.renameConversation('conv', 'user', { autoGenerate: true })
expect(request).toHaveBeenCalledWith({
method: "POST",
path: "/conversations/conv/name",
data: { user: "user", auto_generate: false, name: "name" },
});
method: 'POST',
path: '/conversations/conv/name',
data: { user: 'user', auto_generate: false, name: 'name' },
})
expect(request).toHaveBeenCalledWith({
method: "POST",
path: "/conversations/conv/name",
data: { user: "user", auto_generate: true },
});
});
method: 'POST',
path: '/conversations/conv/name',
data: { user: 'user', auto_generate: true },
})
})
it("requires name when autoGenerate is false", () => {
const { client } = createHttpClientWithSpies();
const chat = new ChatClient(client);
it('requires name when autoGenerate is false', () => {
const { client } = createHttpClientWithSpies()
const chat = new ChatClient(client)
expect(() => chat.renameConversation("conv", "", "user", false)).toThrow(
ValidationError
);
});
expect(() => chat.renameConversation('conv', '', 'user', false)).toThrow(ValidationError)
})
it("deletes conversations", async () => {
const { client, request } = createHttpClientWithSpies();
const chat = new ChatClient(client);
it('deletes conversations', async () => {
const { client, request } = createHttpClientWithSpies()
const chat = new ChatClient(client)
await chat.deleteConversation("conv", "user");
await chat.deleteConversation('conv', 'user')
expect(request).toHaveBeenCalledWith({
method: "DELETE",
path: "/conversations/conv",
data: { user: "user" },
});
});
method: 'DELETE',
path: '/conversations/conv',
data: { user: 'user' },
})
})
it("manages conversation variables", async () => {
const { client, request } = createHttpClientWithSpies();
const chat = new ChatClient(client);
it('manages conversation variables', async () => {
const { client, request } = createHttpClientWithSpies()
const chat = new ChatClient(client)
await chat.getConversationVariables("conv", "user", "last", 10, "name");
await chat.updateConversationVariable("conv", "var", "user", "value");
await chat.getConversationVariables('conv', 'user', 'last', 10, 'name')
await chat.updateConversationVariable('conv', 'var', 'user', 'value')
expect(request).toHaveBeenCalledWith({
method: "GET",
path: "/conversations/conv/variables",
method: 'GET',
path: '/conversations/conv/variables',
query: {
user: "user",
last_id: "last",
user: 'user',
last_id: 'last',
limit: 10,
variable_name: "name",
variable_name: 'name',
},
});
})
expect(request).toHaveBeenCalledWith({
method: "PUT",
path: "/conversations/conv/variables/var",
data: { user: "user", value: "value" },
});
});
method: 'PUT',
path: '/conversations/conv/variables/var',
data: { user: 'user', value: 'value' },
})
})
it("handles annotation APIs", async () => {
const { client, request } = createHttpClientWithSpies();
const chat = new ChatClient(client);
it('handles annotation APIs', async () => {
const { client, request } = createHttpClientWithSpies()
const chat = new ChatClient(client)
await chat.annotationReplyAction("enable", {
await chat.annotationReplyAction('enable', {
score_threshold: 0.5,
embedding_provider_name: "prov",
embedding_model_name: "model",
});
await chat.getAnnotationReplyStatus("enable", "job");
await chat.listAnnotations({ page: 1, limit: 10, keyword: "k" });
await chat.createAnnotation({ question: "q", answer: "a" });
await chat.updateAnnotation("id", { question: "q", answer: "a" });
await chat.deleteAnnotation("id");
embedding_provider_name: 'prov',
embedding_model_name: 'model',
})
await chat.getAnnotationReplyStatus('enable', 'job')
await chat.listAnnotations({ page: 1, limit: 10, keyword: 'k' })
await chat.createAnnotation({ question: 'q', answer: 'a' })
await chat.updateAnnotation('id', { question: 'q', answer: 'a' })
await chat.deleteAnnotation('id')
expect(request).toHaveBeenCalledWith({
method: "POST",
path: "/apps/annotation-reply/enable",
method: 'POST',
path: '/apps/annotation-reply/enable',
data: {
score_threshold: 0.5,
embedding_provider_name: "prov",
embedding_model_name: "model",
embedding_provider_name: 'prov',
embedding_model_name: 'model',
},
});
})
expect(request).toHaveBeenCalledWith({
method: "GET",
path: "/apps/annotation-reply/enable/status/job",
});
method: 'GET',
path: '/apps/annotation-reply/enable/status/job',
})
expect(request).toHaveBeenCalledWith({
method: "GET",
path: "/apps/annotations",
query: { page: 1, limit: 10, keyword: "k" },
});
});
});
method: 'GET',
path: '/apps/annotations',
query: { page: 1, limit: 10, keyword: 'k' },
})
})
})
+148 -177
View File
@@ -1,15 +1,10 @@
import { DifyClient } from "./base";
import type {
ChatMessageRequest,
ChatMessageResponse,
ConversationSortBy,
} from "../types/chat";
import type {
AnnotationCreateRequest,
AnnotationListOptions,
AnnotationReplyActionRequest,
AnnotationResponse,
} from "../types/annotation";
} from '../types/annotation'
import type { ChatMessageRequest, ChatMessageResponse, ConversationSortBy } from '../types/chat'
import type {
DifyResponse,
DifyStream,
@@ -18,242 +13,224 @@ import type {
QueryParams,
SuccessResponse,
SuggestedQuestionsResponse,
} from "../types/common";
import {
ensureNonEmptyString,
ensureOptionalInt,
ensureOptionalString,
} from "./validation";
} from '../types/common'
import { DifyClient } from './base'
import { ensureNonEmptyString, ensureOptionalInt, ensureOptionalString } from './validation'
export class ChatClient extends DifyClient {
createChatMessage(
request: ChatMessageRequest
): Promise<DifyResponse<ChatMessageResponse> | DifyStream<ChatMessageResponse>>;
request: ChatMessageRequest,
): Promise<DifyResponse<ChatMessageResponse> | DifyStream<ChatMessageResponse>>
createChatMessage(
inputs: JsonObject,
query: string,
user: string,
stream?: boolean,
conversationId?: string | null,
files?: ChatMessageRequest["files"]
): Promise<DifyResponse<ChatMessageResponse> | DifyStream<ChatMessageResponse>>;
files?: ChatMessageRequest['files'],
): Promise<DifyResponse<ChatMessageResponse> | DifyStream<ChatMessageResponse>>
createChatMessage(
inputOrRequest: ChatMessageRequest | JsonObject,
query?: string,
user?: string,
stream = false,
conversationId?: string | null,
files?: ChatMessageRequest["files"]
files?: ChatMessageRequest['files'],
): Promise<DifyResponse<ChatMessageResponse> | DifyStream<ChatMessageResponse>> {
let payload: ChatMessageRequest;
let shouldStream = stream;
let payload: ChatMessageRequest
let shouldStream = stream
if (query === undefined && "user" in (inputOrRequest as ChatMessageRequest)) {
payload = inputOrRequest as ChatMessageRequest;
shouldStream = payload.response_mode === "streaming";
if (query === undefined && 'user' in (inputOrRequest as ChatMessageRequest)) {
payload = inputOrRequest as ChatMessageRequest
shouldStream = payload.response_mode === 'streaming'
} else {
ensureNonEmptyString(query, "query");
ensureNonEmptyString(user, "user");
payload = {
ensureNonEmptyString(query, 'query')
ensureNonEmptyString(user, 'user')
payload = {
inputs: inputOrRequest,
query,
user,
response_mode: stream ? "streaming" : "blocking",
response_mode: stream ? 'streaming' : 'blocking',
files,
};
}
if (conversationId) {
payload.conversation_id = conversationId;
payload.conversation_id = conversationId
}
}
ensureNonEmptyString(payload.user, "user");
ensureNonEmptyString(payload.query, "query");
ensureNonEmptyString(payload.user, 'user')
ensureNonEmptyString(payload.query, 'query')
if (shouldStream) {
return this.http.requestStream<ChatMessageResponse>({
method: "POST",
path: "/chat-messages",
method: 'POST',
path: '/chat-messages',
data: payload,
});
})
}
return this.http.request<ChatMessageResponse>({
method: "POST",
path: "/chat-messages",
method: 'POST',
path: '/chat-messages',
data: payload,
});
})
}
stopChatMessage(
taskId: string,
user: string
): Promise<DifyResponse<SuccessResponse>> {
ensureNonEmptyString(taskId, "taskId");
ensureNonEmptyString(user, "user");
stopChatMessage(taskId: string, user: string): Promise<DifyResponse<SuccessResponse>> {
ensureNonEmptyString(taskId, 'taskId')
ensureNonEmptyString(user, 'user')
return this.http.request<SuccessResponse>({
method: "POST",
method: 'POST',
path: `/chat-messages/${taskId}/stop`,
data: { user },
});
})
}
stopMessage(
taskId: string,
user: string
): Promise<DifyResponse<SuccessResponse>> {
return this.stopChatMessage(taskId, user);
stopMessage(taskId: string, user: string): Promise<DifyResponse<SuccessResponse>> {
return this.stopChatMessage(taskId, user)
}
getSuggested(
messageId: string,
user: string
): Promise<DifyResponse<SuggestedQuestionsResponse>> {
ensureNonEmptyString(messageId, "messageId");
ensureNonEmptyString(user, "user");
getSuggested(messageId: string, user: string): Promise<DifyResponse<SuggestedQuestionsResponse>> {
ensureNonEmptyString(messageId, 'messageId')
ensureNonEmptyString(user, 'user')
return this.http.request<SuggestedQuestionsResponse>({
method: "GET",
method: 'GET',
path: `/messages/${messageId}/suggested`,
query: { user },
});
})
}
// Note: messageFeedback is inherited from DifyClient
getAppFeedbacks(
page?: number,
limit?: number
): Promise<DifyResponse<JsonObject>> {
ensureOptionalInt(page, "page");
ensureOptionalInt(limit, "limit");
getAppFeedbacks(page?: number, limit?: number): Promise<DifyResponse<JsonObject>> {
ensureOptionalInt(page, 'page')
ensureOptionalInt(limit, 'limit')
return this.http.request({
method: "GET",
path: "/app/feedbacks",
method: 'GET',
path: '/app/feedbacks',
query: {
page,
limit,
},
});
})
}
getConversations(
user: string,
lastId?: string | null,
limit?: number | null,
sortBy?: ConversationSortBy | null
sortBy?: ConversationSortBy | null,
): Promise<DifyResponse<JsonObject>> {
ensureNonEmptyString(user, "user");
ensureOptionalString(lastId, "lastId");
ensureOptionalInt(limit, "limit");
ensureNonEmptyString(user, 'user')
ensureOptionalString(lastId, 'lastId')
ensureOptionalInt(limit, 'limit')
const params: QueryParams = { user };
const params: QueryParams = { user }
if (lastId) {
params.last_id = lastId;
params.last_id = lastId
}
if (limit) {
params.limit = limit;
params.limit = limit
}
if (sortBy) {
params.sort_by = sortBy;
params.sort_by = sortBy
}
return this.http.request({
method: "GET",
path: "/conversations",
method: 'GET',
path: '/conversations',
query: params,
});
})
}
getConversationMessages(
user: string,
conversationId: string,
firstId?: string | null,
limit?: number | null
limit?: number | null,
): Promise<DifyResponse<JsonObject>> {
ensureNonEmptyString(user, "user");
ensureNonEmptyString(conversationId, "conversationId");
ensureOptionalString(firstId, "firstId");
ensureOptionalInt(limit, "limit");
ensureNonEmptyString(user, 'user')
ensureNonEmptyString(conversationId, 'conversationId')
ensureOptionalString(firstId, 'firstId')
ensureOptionalInt(limit, 'limit')
const params: QueryParams = { user };
params.conversation_id = conversationId;
const params: QueryParams = { user }
params.conversation_id = conversationId
if (firstId) {
params.first_id = firstId;
params.first_id = firstId
}
if (limit) {
params.limit = limit;
params.limit = limit
}
return this.http.request({
method: "GET",
path: "/messages",
method: 'GET',
path: '/messages',
query: params,
});
})
}
renameConversation(
conversationId: string,
name: string,
user: string,
autoGenerate?: boolean
): Promise<DifyResponse<JsonObject>>;
autoGenerate?: boolean,
): Promise<DifyResponse<JsonObject>>
renameConversation(
conversationId: string,
user: string,
options?: { name?: string | null; autoGenerate?: boolean }
): Promise<DifyResponse<JsonObject>>;
options?: { name?: string | null; autoGenerate?: boolean },
): Promise<DifyResponse<JsonObject>>
renameConversation(
conversationId: string,
nameOrUser: string,
userOrOptions?: string | { name?: string | null; autoGenerate?: boolean },
autoGenerate?: boolean
autoGenerate?: boolean,
): Promise<DifyResponse<JsonObject>> {
ensureNonEmptyString(conversationId, "conversationId");
ensureNonEmptyString(conversationId, 'conversationId')
let name: string | null | undefined;
let user: string;
let resolvedAutoGenerate: boolean;
let name: string | null | undefined
let user: string
let resolvedAutoGenerate: boolean
if (typeof userOrOptions === "string" || userOrOptions === undefined) {
name = nameOrUser;
user = userOrOptions ?? "";
resolvedAutoGenerate = autoGenerate ?? false;
if (typeof userOrOptions === 'string' || userOrOptions === undefined) {
name = nameOrUser
user = userOrOptions ?? ''
resolvedAutoGenerate = autoGenerate ?? false
} else {
user = nameOrUser;
name = userOrOptions.name;
resolvedAutoGenerate = userOrOptions.autoGenerate ?? false;
user = nameOrUser
name = userOrOptions.name
resolvedAutoGenerate = userOrOptions.autoGenerate ?? false
}
ensureNonEmptyString(user, "user");
ensureNonEmptyString(user, 'user')
if (!resolvedAutoGenerate) {
ensureNonEmptyString(name, "name");
ensureNonEmptyString(name, 'name')
}
const payload: JsonObject = {
user,
auto_generate: resolvedAutoGenerate,
};
if (typeof name === "string" && name.trim().length > 0) {
payload.name = name;
}
if (typeof name === 'string' && name.trim().length > 0) {
payload.name = name
}
return this.http.request({
method: "POST",
method: 'POST',
path: `/conversations/${conversationId}/name`,
data: payload,
});
})
}
deleteConversation(
conversationId: string,
user: string
): Promise<DifyResponse<SuccessResponse>> {
ensureNonEmptyString(conversationId, "conversationId");
ensureNonEmptyString(user, "user");
deleteConversation(conversationId: string, user: string): Promise<DifyResponse<SuccessResponse>> {
ensureNonEmptyString(conversationId, 'conversationId')
ensureNonEmptyString(user, 'user')
return this.http.request({
method: "DELETE",
method: 'DELETE',
path: `/conversations/${conversationId}`,
data: { user },
});
})
}
getConversationVariables(
@@ -261,16 +238,16 @@ export class ChatClient extends DifyClient {
user: string,
lastId?: string | null,
limit?: number | null,
variableName?: string | null
variableName?: string | null,
): Promise<DifyResponse<JsonObject>> {
ensureNonEmptyString(conversationId, "conversationId");
ensureNonEmptyString(user, "user");
ensureOptionalString(lastId, "lastId");
ensureOptionalInt(limit, "limit");
ensureOptionalString(variableName, "variableName");
ensureNonEmptyString(conversationId, 'conversationId')
ensureNonEmptyString(user, 'user')
ensureOptionalString(lastId, 'lastId')
ensureOptionalInt(limit, 'limit')
ensureOptionalString(variableName, 'variableName')
return this.http.request({
method: "GET",
method: 'GET',
path: `/conversations/${conversationId}/variables`,
query: {
user,
@@ -278,105 +255,99 @@ export class ChatClient extends DifyClient {
limit: limit ?? undefined,
variable_name: variableName ?? undefined,
},
});
})
}
updateConversationVariable(
conversationId: string,
variableId: string,
user: string,
value: JsonValue
value: JsonValue,
): Promise<DifyResponse<JsonObject>> {
ensureNonEmptyString(conversationId, "conversationId");
ensureNonEmptyString(variableId, "variableId");
ensureNonEmptyString(user, "user");
ensureNonEmptyString(conversationId, 'conversationId')
ensureNonEmptyString(variableId, 'variableId')
ensureNonEmptyString(user, 'user')
return this.http.request({
method: "PUT",
method: 'PUT',
path: `/conversations/${conversationId}/variables/${variableId}`,
data: {
user,
value,
},
});
})
}
annotationReplyAction(
action: "enable" | "disable",
request: AnnotationReplyActionRequest
action: 'enable' | 'disable',
request: AnnotationReplyActionRequest,
): Promise<DifyResponse<AnnotationResponse>> {
ensureNonEmptyString(action, "action");
ensureNonEmptyString(request.embedding_provider_name, "embedding_provider_name");
ensureNonEmptyString(request.embedding_model_name, "embedding_model_name");
ensureNonEmptyString(action, 'action')
ensureNonEmptyString(request.embedding_provider_name, 'embedding_provider_name')
ensureNonEmptyString(request.embedding_model_name, 'embedding_model_name')
return this.http.request({
method: "POST",
method: 'POST',
path: `/apps/annotation-reply/${action}`,
data: request,
});
})
}
getAnnotationReplyStatus(
action: "enable" | "disable",
jobId: string
action: 'enable' | 'disable',
jobId: string,
): Promise<DifyResponse<AnnotationResponse>> {
ensureNonEmptyString(action, "action");
ensureNonEmptyString(jobId, "jobId");
ensureNonEmptyString(action, 'action')
ensureNonEmptyString(jobId, 'jobId')
return this.http.request({
method: "GET",
method: 'GET',
path: `/apps/annotation-reply/${action}/status/${jobId}`,
});
})
}
listAnnotations(
options?: AnnotationListOptions
): Promise<DifyResponse<AnnotationResponse>> {
ensureOptionalInt(options?.page, "page");
ensureOptionalInt(options?.limit, "limit");
ensureOptionalString(options?.keyword, "keyword");
listAnnotations(options?: AnnotationListOptions): Promise<DifyResponse<AnnotationResponse>> {
ensureOptionalInt(options?.page, 'page')
ensureOptionalInt(options?.limit, 'limit')
ensureOptionalString(options?.keyword, 'keyword')
return this.http.request({
method: "GET",
path: "/apps/annotations",
method: 'GET',
path: '/apps/annotations',
query: {
page: options?.page,
limit: options?.limit,
keyword: options?.keyword ?? undefined,
},
});
})
}
createAnnotation(
request: AnnotationCreateRequest
): Promise<DifyResponse<AnnotationResponse>> {
ensureNonEmptyString(request.question, "question");
ensureNonEmptyString(request.answer, "answer");
createAnnotation(request: AnnotationCreateRequest): Promise<DifyResponse<AnnotationResponse>> {
ensureNonEmptyString(request.question, 'question')
ensureNonEmptyString(request.answer, 'answer')
return this.http.request({
method: "POST",
path: "/apps/annotations",
method: 'POST',
path: '/apps/annotations',
data: request,
});
})
}
updateAnnotation(
annotationId: string,
request: AnnotationCreateRequest
request: AnnotationCreateRequest,
): Promise<DifyResponse<AnnotationResponse>> {
ensureNonEmptyString(annotationId, "annotationId");
ensureNonEmptyString(request.question, "question");
ensureNonEmptyString(request.answer, "answer");
ensureNonEmptyString(annotationId, 'annotationId')
ensureNonEmptyString(request.question, 'question')
ensureNonEmptyString(request.answer, 'answer')
return this.http.request({
method: "PUT",
method: 'PUT',
path: `/apps/annotations/${annotationId}`,
data: request,
});
})
}
deleteAnnotation(
annotationId: string
): Promise<DifyResponse<AnnotationResponse>> {
ensureNonEmptyString(annotationId, "annotationId");
deleteAnnotation(annotationId: string): Promise<DifyResponse<AnnotationResponse>> {
ensureNonEmptyString(annotationId, 'annotationId')
return this.http.request({
method: "DELETE",
method: 'DELETE',
path: `/apps/annotations/${annotationId}`,
});
})
}
// Note: audioToText is inherited from DifyClient
@@ -1,83 +1,83 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import { CompletionClient } from "./completion";
import { createHttpClientWithSpies } from "../../tests/test-utils";
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { createHttpClientWithSpies } from '../../tests/test-utils'
import { CompletionClient } from './completion'
describe("CompletionClient", () => {
describe('CompletionClient', () => {
beforeEach(() => {
vi.restoreAllMocks();
});
vi.restoreAllMocks()
})
it("creates completion messages in blocking mode", async () => {
const { client, request } = createHttpClientWithSpies();
const completion = new CompletionClient(client);
it('creates completion messages in blocking mode', async () => {
const { client, request } = createHttpClientWithSpies()
const completion = new CompletionClient(client)
await completion.createCompletionMessage({ input: "x" }, "user", false);
await completion.createCompletionMessage({ input: 'x' }, 'user', false)
expect(request).toHaveBeenCalledWith({
method: "POST",
path: "/completion-messages",
method: 'POST',
path: '/completion-messages',
data: {
inputs: { input: "x" },
user: "user",
inputs: { input: 'x' },
user: 'user',
files: undefined,
response_mode: "blocking",
response_mode: 'blocking',
},
});
});
})
})
it("creates completion messages in streaming mode", async () => {
const { client, requestStream } = createHttpClientWithSpies();
const completion = new CompletionClient(client);
it('creates completion messages in streaming mode', async () => {
const { client, requestStream } = createHttpClientWithSpies()
const completion = new CompletionClient(client)
await completion.createCompletionMessage({
inputs: { input: "x" },
user: "user",
response_mode: "streaming",
});
inputs: { input: 'x' },
user: 'user',
response_mode: 'streaming',
})
expect(requestStream).toHaveBeenCalledWith({
method: "POST",
path: "/completion-messages",
method: 'POST',
path: '/completion-messages',
data: {
inputs: { input: "x" },
user: "user",
response_mode: "streaming",
inputs: { input: 'x' },
user: 'user',
response_mode: 'streaming',
},
});
});
})
})
it("stops completion messages", async () => {
const { client, request } = createHttpClientWithSpies();
const completion = new CompletionClient(client);
it('stops completion messages', async () => {
const { client, request } = createHttpClientWithSpies()
const completion = new CompletionClient(client)
await completion.stopCompletionMessage("task", "user");
await completion.stop("task", "user");
await completion.stopCompletionMessage('task', 'user')
await completion.stop('task', 'user')
expect(request).toHaveBeenCalledWith({
method: "POST",
path: "/completion-messages/task/stop",
data: { user: "user" },
});
});
method: 'POST',
path: '/completion-messages/task/stop',
data: { user: 'user' },
})
})
it("supports deprecated runWorkflow", async () => {
const { client, request, requestStream } = createHttpClientWithSpies();
const completion = new CompletionClient(client);
const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
it('supports deprecated runWorkflow', async () => {
const { client, request, requestStream } = createHttpClientWithSpies()
const completion = new CompletionClient(client)
const warn = vi.spyOn(console, 'warn').mockImplementation(() => {})
await completion.runWorkflow({ input: "x" }, "user", false);
await completion.runWorkflow({ input: "x" }, "user", true);
await completion.runWorkflow({ input: 'x' }, 'user', false)
await completion.runWorkflow({ input: 'x' }, 'user', true)
expect(warn).toHaveBeenCalled();
expect(warn).toHaveBeenCalled()
expect(request).toHaveBeenCalledWith({
method: "POST",
path: "/workflows/run",
data: { inputs: { input: "x" }, user: "user", response_mode: "blocking" },
});
method: 'POST',
path: '/workflows/run',
data: { inputs: { input: 'x' }, user: 'user', response_mode: 'blocking' },
})
expect(requestStream).toHaveBeenCalledWith({
method: "POST",
path: "/workflows/run",
data: { inputs: { input: "x" }, user: "user", response_mode: "streaming" },
});
});
});
method: 'POST',
path: '/workflows/run',
data: { inputs: { input: 'x' }, user: 'user', response_mode: 'streaming' },
})
})
})
+47 -60
View File
@@ -1,116 +1,103 @@
import { DifyClient } from "./base";
import type { CompletionRequest, CompletionResponse } from "../types/completion";
import type {
DifyResponse,
DifyStream,
JsonObject,
SuccessResponse,
} from "../types/common";
import { ensureNonEmptyString } from "./validation";
import type { DifyResponse, DifyStream, JsonObject, SuccessResponse } from '../types/common'
import type { CompletionRequest, CompletionResponse } from '../types/completion'
import { DifyClient } from './base'
import { ensureNonEmptyString } from './validation'
const warned = new Set<string>();
const warned = new Set<string>()
const warnOnce = (message: string): void => {
if (warned.has(message)) {
return;
return
}
warned.add(message);
console.warn(message);
};
warned.add(message)
console.warn(message)
}
export class CompletionClient extends DifyClient {
createCompletionMessage(
request: CompletionRequest
): Promise<DifyResponse<CompletionResponse> | DifyStream<CompletionResponse>>;
request: CompletionRequest,
): Promise<DifyResponse<CompletionResponse> | DifyStream<CompletionResponse>>
createCompletionMessage(
inputs: JsonObject,
user: string,
stream?: boolean,
files?: CompletionRequest["files"]
): Promise<DifyResponse<CompletionResponse> | DifyStream<CompletionResponse>>;
files?: CompletionRequest['files'],
): Promise<DifyResponse<CompletionResponse> | DifyStream<CompletionResponse>>
createCompletionMessage(
inputOrRequest: CompletionRequest | JsonObject,
user?: string,
stream = false,
files?: CompletionRequest["files"]
files?: CompletionRequest['files'],
): Promise<DifyResponse<CompletionResponse> | DifyStream<CompletionResponse>> {
let payload: CompletionRequest;
let shouldStream = stream;
let payload: CompletionRequest
let shouldStream = stream
if (user === undefined && "user" in (inputOrRequest as CompletionRequest)) {
payload = inputOrRequest as CompletionRequest;
shouldStream = payload.response_mode === "streaming";
if (user === undefined && 'user' in (inputOrRequest as CompletionRequest)) {
payload = inputOrRequest as CompletionRequest
shouldStream = payload.response_mode === 'streaming'
} else {
ensureNonEmptyString(user, "user");
ensureNonEmptyString(user, 'user')
payload = {
inputs: inputOrRequest,
user,
files,
response_mode: stream ? "streaming" : "blocking",
};
response_mode: stream ? 'streaming' : 'blocking',
}
}
ensureNonEmptyString(payload.user, "user");
ensureNonEmptyString(payload.user, 'user')
if (shouldStream) {
return this.http.requestStream<CompletionResponse>({
method: "POST",
path: "/completion-messages",
method: 'POST',
path: '/completion-messages',
data: payload,
});
})
}
return this.http.request<CompletionResponse>({
method: "POST",
path: "/completion-messages",
method: 'POST',
path: '/completion-messages',
data: payload,
});
})
}
stopCompletionMessage(
taskId: string,
user: string
): Promise<DifyResponse<SuccessResponse>> {
ensureNonEmptyString(taskId, "taskId");
ensureNonEmptyString(user, "user");
stopCompletionMessage(taskId: string, user: string): Promise<DifyResponse<SuccessResponse>> {
ensureNonEmptyString(taskId, 'taskId')
ensureNonEmptyString(user, 'user')
return this.http.request<SuccessResponse>({
method: "POST",
method: 'POST',
path: `/completion-messages/${taskId}/stop`,
data: { user },
});
})
}
stop(
taskId: string,
user: string
): Promise<DifyResponse<SuccessResponse>> {
return this.stopCompletionMessage(taskId, user);
stop(taskId: string, user: string): Promise<DifyResponse<SuccessResponse>> {
return this.stopCompletionMessage(taskId, user)
}
runWorkflow(
inputs: JsonObject,
user: string,
stream = false
stream = false,
): Promise<DifyResponse<JsonObject> | DifyStream<JsonObject>> {
warnOnce(
"CompletionClient.runWorkflow is deprecated. Use WorkflowClient.run instead."
);
ensureNonEmptyString(user, "user");
warnOnce('CompletionClient.runWorkflow is deprecated. Use WorkflowClient.run instead.')
ensureNonEmptyString(user, 'user')
const payload = {
inputs,
user,
response_mode: stream ? "streaming" : "blocking",
};
response_mode: stream ? 'streaming' : 'blocking',
}
if (stream) {
return this.http.requestStream<JsonObject>({
method: "POST",
path: "/workflows/run",
method: 'POST',
path: '/workflows/run',
data: payload,
});
})
}
return this.http.request<JsonObject>({
method: "POST",
path: "/workflows/run",
method: 'POST',
path: '/workflows/run',
data: payload,
});
})
}
}
@@ -1,266 +1,262 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import { FileUploadError, ValidationError } from "../errors/dify-error";
import { KnowledgeBaseClient } from "./knowledge-base";
import { createHttpClientWithSpies } from "../../tests/test-utils";
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { createHttpClientWithSpies } from '../../tests/test-utils'
import { FileUploadError, ValidationError } from '../errors/dify-error'
import { KnowledgeBaseClient } from './knowledge-base'
describe("KnowledgeBaseClient", () => {
describe('KnowledgeBaseClient', () => {
beforeEach(() => {
vi.restoreAllMocks();
});
vi.restoreAllMocks()
})
it("handles dataset and tag operations", async () => {
const { client, request } = createHttpClientWithSpies();
const kb = new KnowledgeBaseClient(client);
it('handles dataset and tag operations', async () => {
const { client, request } = createHttpClientWithSpies()
const kb = new KnowledgeBaseClient(client)
await kb.listDatasets({
page: 1,
limit: 2,
keyword: "k",
keyword: 'k',
includeAll: true,
tagIds: ["t1"],
});
await kb.createDataset({ name: "dataset" });
await kb.getDataset("ds");
await kb.updateDataset("ds", { name: "new" });
await kb.deleteDataset("ds");
await kb.updateDocumentStatus("ds", "enable", ["doc1"]);
tagIds: ['t1'],
})
await kb.createDataset({ name: 'dataset' })
await kb.getDataset('ds')
await kb.updateDataset('ds', { name: 'new' })
await kb.deleteDataset('ds')
await kb.updateDocumentStatus('ds', 'enable', ['doc1'])
await kb.listTags();
await kb.createTag({ name: "tag" });
await kb.updateTag({ tag_id: "tag", name: "name" });
await kb.deleteTag({ tag_id: "tag" });
await kb.bindTags({ tag_ids: ["tag"], target_id: "doc" });
await kb.unbindTags({ tag_id: "tag", target_id: "doc" });
await kb.getDatasetTags("ds");
await kb.listTags()
await kb.createTag({ name: 'tag' })
await kb.updateTag({ tag_id: 'tag', name: 'name' })
await kb.deleteTag({ tag_id: 'tag' })
await kb.bindTags({ tag_ids: ['tag'], target_id: 'doc' })
await kb.unbindTags({ tag_id: 'tag', target_id: 'doc' })
await kb.getDatasetTags('ds')
expect(request).toHaveBeenCalledWith({
method: "GET",
path: "/datasets",
method: 'GET',
path: '/datasets',
query: {
page: 1,
limit: 2,
keyword: "k",
keyword: 'k',
include_all: true,
tag_ids: ["t1"],
tag_ids: ['t1'],
},
});
})
expect(request).toHaveBeenCalledWith({
method: "POST",
path: "/datasets",
data: { name: "dataset" },
});
method: 'POST',
path: '/datasets',
data: { name: 'dataset' },
})
expect(request).toHaveBeenCalledWith({
method: "PATCH",
path: "/datasets/ds",
data: { name: "new" },
});
method: 'PATCH',
path: '/datasets/ds',
data: { name: 'new' },
})
expect(request).toHaveBeenCalledWith({
method: "PATCH",
path: "/datasets/ds/documents/status/enable",
data: { document_ids: ["doc1"] },
});
method: 'PATCH',
path: '/datasets/ds/documents/status/enable',
data: { document_ids: ['doc1'] },
})
expect(request).toHaveBeenCalledWith({
method: "POST",
path: "/datasets/tags/binding",
data: { tag_ids: ["tag"], target_id: "doc" },
});
});
method: 'POST',
path: '/datasets/tags/binding',
data: { tag_ids: ['tag'], target_id: 'doc' },
})
})
it("handles document operations", async () => {
const { client, request } = createHttpClientWithSpies();
const kb = new KnowledgeBaseClient(client);
const form = { append: vi.fn(), getHeaders: () => ({}) };
it('handles document operations', async () => {
const { client, request } = createHttpClientWithSpies()
const kb = new KnowledgeBaseClient(client)
const form = { append: vi.fn(), getHeaders: () => ({}) }
await kb.createDocumentByText("ds", { name: "doc", text: "text" });
await kb.updateDocumentByText("ds", "doc", { name: "doc2" });
await kb.createDocumentByFile("ds", form);
await kb.updateDocumentByFile("ds", "doc", form);
await kb.listDocuments("ds", { page: 1, limit: 20, keyword: "k" });
await kb.getDocument("ds", "doc", { metadata: "all" });
await kb.deleteDocument("ds", "doc");
await kb.getDocumentIndexingStatus("ds", "batch");
await kb.createDocumentByText('ds', { name: 'doc', text: 'text' })
await kb.updateDocumentByText('ds', 'doc', { name: 'doc2' })
await kb.createDocumentByFile('ds', form)
await kb.updateDocumentByFile('ds', 'doc', form)
await kb.listDocuments('ds', { page: 1, limit: 20, keyword: 'k' })
await kb.getDocument('ds', 'doc', { metadata: 'all' })
await kb.deleteDocument('ds', 'doc')
await kb.getDocumentIndexingStatus('ds', 'batch')
expect(request).toHaveBeenCalledWith({
method: "POST",
path: "/datasets/ds/document/create_by_text",
data: { name: "doc", text: "text" },
});
method: 'POST',
path: '/datasets/ds/document/create_by_text',
data: { name: 'doc', text: 'text' },
})
expect(request).toHaveBeenCalledWith({
method: "POST",
path: "/datasets/ds/documents/doc/update_by_text",
data: { name: "doc2" },
});
method: 'POST',
path: '/datasets/ds/documents/doc/update_by_text',
data: { name: 'doc2' },
})
expect(request).toHaveBeenCalledWith({
method: "POST",
path: "/datasets/ds/document/create_by_file",
method: 'POST',
path: '/datasets/ds/document/create_by_file',
data: form,
});
})
expect(request).toHaveBeenCalledWith({
method: "GET",
path: "/datasets/ds/documents",
query: { page: 1, limit: 20, keyword: "k", status: undefined },
});
});
method: 'GET',
path: '/datasets/ds/documents',
query: { page: 1, limit: 20, keyword: 'k', status: undefined },
})
})
it("handles segments and child chunks", async () => {
const { client, request } = createHttpClientWithSpies();
const kb = new KnowledgeBaseClient(client);
it('handles segments and child chunks', async () => {
const { client, request } = createHttpClientWithSpies()
const kb = new KnowledgeBaseClient(client)
await kb.createSegments("ds", "doc", { segments: [{ content: "x" }] });
await kb.listSegments("ds", "doc", { page: 1, limit: 10, keyword: "k" });
await kb.getSegment("ds", "doc", "seg");
await kb.updateSegment("ds", "doc", "seg", {
segment: { content: "y" },
});
await kb.deleteSegment("ds", "doc", "seg");
await kb.createSegments('ds', 'doc', { segments: [{ content: 'x' }] })
await kb.listSegments('ds', 'doc', { page: 1, limit: 10, keyword: 'k' })
await kb.getSegment('ds', 'doc', 'seg')
await kb.updateSegment('ds', 'doc', 'seg', {
segment: { content: 'y' },
})
await kb.deleteSegment('ds', 'doc', 'seg')
await kb.createChildChunk("ds", "doc", "seg", { content: "c" });
await kb.listChildChunks("ds", "doc", "seg", { page: 1, limit: 10 });
await kb.updateChildChunk("ds", "doc", "seg", "child", {
content: "c2",
});
await kb.deleteChildChunk("ds", "doc", "seg", "child");
await kb.createChildChunk('ds', 'doc', 'seg', { content: 'c' })
await kb.listChildChunks('ds', 'doc', 'seg', { page: 1, limit: 10 })
await kb.updateChildChunk('ds', 'doc', 'seg', 'child', {
content: 'c2',
})
await kb.deleteChildChunk('ds', 'doc', 'seg', 'child')
expect(request).toHaveBeenCalledWith({
method: "POST",
path: "/datasets/ds/documents/doc/segments",
data: { segments: [{ content: "x" }] },
});
method: 'POST',
path: '/datasets/ds/documents/doc/segments',
data: { segments: [{ content: 'x' }] },
})
expect(request).toHaveBeenCalledWith({
method: "POST",
path: "/datasets/ds/documents/doc/segments/seg",
data: { segment: { content: "y" } },
});
method: 'POST',
path: '/datasets/ds/documents/doc/segments/seg',
data: { segment: { content: 'y' } },
})
expect(request).toHaveBeenCalledWith({
method: "PATCH",
path: "/datasets/ds/documents/doc/segments/seg/child_chunks/child",
data: { content: "c2" },
});
});
method: 'PATCH',
path: '/datasets/ds/documents/doc/segments/seg/child_chunks/child',
data: { content: 'c2' },
})
})
it("handles metadata and retrieval", async () => {
const { client, request } = createHttpClientWithSpies();
const kb = new KnowledgeBaseClient(client);
it('handles metadata and retrieval', async () => {
const { client, request } = createHttpClientWithSpies()
const kb = new KnowledgeBaseClient(client)
await kb.listMetadata("ds");
await kb.createMetadata("ds", { name: "m", type: "string" });
await kb.updateMetadata("ds", "mid", { name: "m2" });
await kb.deleteMetadata("ds", "mid");
await kb.listBuiltInMetadata("ds");
await kb.updateBuiltInMetadata("ds", "enable");
await kb.updateDocumentsMetadata("ds", {
operation_data: [
{ document_id: "doc", metadata_list: [{ id: "m", name: "n" }] },
],
});
await kb.hitTesting("ds", { query: "q" });
await kb.retrieve("ds", { query: "q" });
await kb.listMetadata('ds')
await kb.createMetadata('ds', { name: 'm', type: 'string' })
await kb.updateMetadata('ds', 'mid', { name: 'm2' })
await kb.deleteMetadata('ds', 'mid')
await kb.listBuiltInMetadata('ds')
await kb.updateBuiltInMetadata('ds', 'enable')
await kb.updateDocumentsMetadata('ds', {
operation_data: [{ document_id: 'doc', metadata_list: [{ id: 'm', name: 'n' }] }],
})
await kb.hitTesting('ds', { query: 'q' })
await kb.retrieve('ds', { query: 'q' })
expect(request).toHaveBeenCalledWith({
method: "GET",
path: "/datasets/ds/metadata",
});
method: 'GET',
path: '/datasets/ds/metadata',
})
expect(request).toHaveBeenCalledWith({
method: "POST",
path: "/datasets/ds/metadata",
data: { name: "m", type: "string" },
});
method: 'POST',
path: '/datasets/ds/metadata',
data: { name: 'm', type: 'string' },
})
expect(request).toHaveBeenCalledWith({
method: "POST",
path: "/datasets/ds/hit-testing",
data: { query: "q" },
});
});
method: 'POST',
path: '/datasets/ds/hit-testing',
data: { query: 'q' },
})
})
it("handles pipeline operations", async () => {
const { client, request, requestStream } = createHttpClientWithSpies();
const kb = new KnowledgeBaseClient(client);
const form = { append: vi.fn(), getHeaders: () => ({}) };
it('handles pipeline operations', async () => {
const { client, request, requestStream } = createHttpClientWithSpies()
const kb = new KnowledgeBaseClient(client)
const form = { append: vi.fn(), getHeaders: () => ({}) }
await kb.listDatasourcePlugins("ds", { isPublished: true });
await kb.runDatasourceNode("ds", "node", {
inputs: { input: "x" },
datasource_type: "custom",
await kb.listDatasourcePlugins('ds', { isPublished: true })
await kb.runDatasourceNode('ds', 'node', {
inputs: { input: 'x' },
datasource_type: 'custom',
is_published: true,
});
await kb.runPipeline("ds", {
inputs: { input: "x" },
datasource_type: "custom",
})
await kb.runPipeline('ds', {
inputs: { input: 'x' },
datasource_type: 'custom',
datasource_info_list: [],
start_node_id: "start",
start_node_id: 'start',
is_published: true,
response_mode: "streaming",
});
await kb.runPipeline("ds", {
inputs: { input: "x" },
datasource_type: "custom",
response_mode: 'streaming',
})
await kb.runPipeline('ds', {
inputs: { input: 'x' },
datasource_type: 'custom',
datasource_info_list: [],
start_node_id: "start",
start_node_id: 'start',
is_published: true,
response_mode: "blocking",
});
await kb.uploadPipelineFile(form);
response_mode: 'blocking',
})
await kb.uploadPipelineFile(form)
expect(request).toHaveBeenCalledWith({
method: "GET",
path: "/datasets/ds/pipeline/datasource-plugins",
method: 'GET',
path: '/datasets/ds/pipeline/datasource-plugins',
query: { is_published: true },
});
})
expect(requestStream).toHaveBeenCalledWith({
method: "POST",
path: "/datasets/ds/pipeline/datasource/nodes/node/run",
method: 'POST',
path: '/datasets/ds/pipeline/datasource/nodes/node/run',
data: {
inputs: { input: "x" },
datasource_type: "custom",
inputs: { input: 'x' },
datasource_type: 'custom',
is_published: true,
},
});
})
expect(requestStream).toHaveBeenCalledWith({
method: "POST",
path: "/datasets/ds/pipeline/run",
method: 'POST',
path: '/datasets/ds/pipeline/run',
data: {
inputs: { input: "x" },
datasource_type: "custom",
inputs: { input: 'x' },
datasource_type: 'custom',
datasource_info_list: [],
start_node_id: "start",
start_node_id: 'start',
is_published: true,
response_mode: "streaming",
response_mode: 'streaming',
},
});
})
expect(request).toHaveBeenCalledWith({
method: "POST",
path: "/datasets/ds/pipeline/run",
method: 'POST',
path: '/datasets/ds/pipeline/run',
data: {
inputs: { input: "x" },
datasource_type: "custom",
inputs: { input: 'x' },
datasource_type: 'custom',
datasource_info_list: [],
start_node_id: "start",
start_node_id: 'start',
is_published: true,
response_mode: "blocking",
response_mode: 'blocking',
},
});
})
expect(request).toHaveBeenCalledWith({
method: "POST",
path: "/datasets/pipeline/file-upload",
method: 'POST',
path: '/datasets/pipeline/file-upload',
data: form,
});
});
})
})
it("validates form-data and optional array filters", async () => {
const { client } = createHttpClientWithSpies();
const kb = new KnowledgeBaseClient(client);
it('validates form-data and optional array filters', async () => {
const { client } = createHttpClientWithSpies()
const kb = new KnowledgeBaseClient(client)
await expect(kb.createDocumentByFile("ds", {})).rejects.toBeInstanceOf(
FileUploadError
);
await expect(kb.createDocumentByFile('ds', {})).rejects.toBeInstanceOf(FileUploadError)
await expect(
kb.listSegments("ds", "doc", { status: ["ok", 1] as unknown as string[] })
).rejects.toBeInstanceOf(ValidationError);
kb.listSegments('ds', 'doc', { status: ['ok', 1] as unknown as string[] }),
).rejects.toBeInstanceOf(ValidationError)
await expect(
kb.hitTesting("ds", {
query: "q",
attachment_ids: ["att-1", 2] as unknown as string[],
})
).rejects.toBeInstanceOf(ValidationError);
});
});
kb.hitTesting('ds', {
query: 'q',
attachment_ids: ['att-1', 2] as unknown as string[],
}),
).rejects.toBeInstanceOf(ValidationError)
})
})
+263 -286
View File
@@ -1,4 +1,5 @@
import { DifyClient } from "./base";
import type { SdkFormData } from '../http/form-data'
import type { DifyResponse, DifyStream, QueryParams } from '../types/common'
import type {
DatasetCreateRequest,
DatasetListOptions,
@@ -28,267 +29,249 @@ import type {
PipelineRunRequest,
KnowledgeBaseResponse,
PipelineStreamEvent,
} from "../types/knowledge-base";
import type { DifyResponse, DifyStream, QueryParams } from "../types/common";
} from '../types/knowledge-base'
import { FileUploadError, ValidationError } from '../errors/dify-error'
import { isFormData } from '../http/form-data'
import { DifyClient } from './base'
import {
ensureNonEmptyString,
ensureOptionalBoolean,
ensureOptionalInt,
ensureOptionalString,
ensureStringArray,
} from "./validation";
import { FileUploadError, ValidationError } from "../errors/dify-error";
import type { SdkFormData } from "../http/form-data";
import { isFormData } from "../http/form-data";
} from './validation'
function ensureFormData(
form: unknown,
context: string
): asserts form is SdkFormData {
function ensureFormData(form: unknown, context: string): asserts form is SdkFormData {
if (!isFormData(form)) {
throw new FileUploadError(`${context} requires FormData`);
throw new FileUploadError(`${context} requires FormData`)
}
}
const ensureNonEmptyArray = (value: unknown, name: string): void => {
if (!Array.isArray(value) || value.length === 0) {
throw new ValidationError(`${name} must be a non-empty array`);
throw new ValidationError(`${name} must be a non-empty array`)
}
};
}
export class KnowledgeBaseClient extends DifyClient {
async listDatasets(
options?: DatasetListOptions
): Promise<DifyResponse<KnowledgeBaseResponse>> {
ensureOptionalInt(options?.page, "page");
ensureOptionalInt(options?.limit, "limit");
ensureOptionalString(options?.keyword, "keyword");
ensureOptionalBoolean(options?.includeAll, "includeAll");
async listDatasets(options?: DatasetListOptions): Promise<DifyResponse<KnowledgeBaseResponse>> {
ensureOptionalInt(options?.page, 'page')
ensureOptionalInt(options?.limit, 'limit')
ensureOptionalString(options?.keyword, 'keyword')
ensureOptionalBoolean(options?.includeAll, 'includeAll')
const query: QueryParams = {
page: options?.page,
limit: options?.limit,
keyword: options?.keyword ?? undefined,
include_all: options?.includeAll ?? undefined,
};
}
if (options?.tagIds && options.tagIds.length > 0) {
ensureStringArray(options.tagIds, "tagIds");
query.tag_ids = options.tagIds;
ensureStringArray(options.tagIds, 'tagIds')
query.tag_ids = options.tagIds
}
return this.http.request({
method: "GET",
path: "/datasets",
method: 'GET',
path: '/datasets',
query,
});
})
}
async createDataset(
request: DatasetCreateRequest
): Promise<DifyResponse<KnowledgeBaseResponse>> {
ensureNonEmptyString(request.name, "name");
async createDataset(request: DatasetCreateRequest): Promise<DifyResponse<KnowledgeBaseResponse>> {
ensureNonEmptyString(request.name, 'name')
return this.http.request({
method: "POST",
path: "/datasets",
method: 'POST',
path: '/datasets',
data: request,
});
})
}
async getDataset(datasetId: string): Promise<DifyResponse<KnowledgeBaseResponse>> {
ensureNonEmptyString(datasetId, "datasetId");
ensureNonEmptyString(datasetId, 'datasetId')
return this.http.request({
method: "GET",
method: 'GET',
path: `/datasets/${datasetId}`,
});
})
}
async updateDataset(
datasetId: string,
request: DatasetUpdateRequest
request: DatasetUpdateRequest,
): Promise<DifyResponse<KnowledgeBaseResponse>> {
ensureNonEmptyString(datasetId, "datasetId");
ensureNonEmptyString(datasetId, 'datasetId')
if (request.name !== undefined && request.name !== null) {
ensureNonEmptyString(request.name, "name");
ensureNonEmptyString(request.name, 'name')
}
return this.http.request({
method: "PATCH",
method: 'PATCH',
path: `/datasets/${datasetId}`,
data: request,
});
})
}
async deleteDataset(datasetId: string): Promise<DifyResponse<KnowledgeBaseResponse>> {
ensureNonEmptyString(datasetId, "datasetId");
ensureNonEmptyString(datasetId, 'datasetId')
return this.http.request({
method: "DELETE",
method: 'DELETE',
path: `/datasets/${datasetId}`,
});
})
}
async updateDocumentStatus(
datasetId: string,
action: DocumentStatusAction,
documentIds: string[]
documentIds: string[],
): Promise<DifyResponse<KnowledgeBaseResponse>> {
ensureNonEmptyString(datasetId, "datasetId");
ensureNonEmptyString(action, "action");
ensureStringArray(documentIds, "documentIds");
ensureNonEmptyString(datasetId, 'datasetId')
ensureNonEmptyString(action, 'action')
ensureStringArray(documentIds, 'documentIds')
return this.http.request({
method: "PATCH",
method: 'PATCH',
path: `/datasets/${datasetId}/documents/status/${action}`,
data: {
document_ids: documentIds,
},
});
})
}
async listTags(): Promise<DifyResponse<KnowledgeBaseResponse>> {
return this.http.request({
method: "GET",
path: "/datasets/tags",
});
method: 'GET',
path: '/datasets/tags',
})
}
async createTag(
request: DatasetTagCreateRequest
): Promise<DifyResponse<KnowledgeBaseResponse>> {
ensureNonEmptyString(request.name, "name");
async createTag(request: DatasetTagCreateRequest): Promise<DifyResponse<KnowledgeBaseResponse>> {
ensureNonEmptyString(request.name, 'name')
return this.http.request({
method: "POST",
path: "/datasets/tags",
method: 'POST',
path: '/datasets/tags',
data: request,
});
})
}
async updateTag(
request: DatasetTagUpdateRequest
): Promise<DifyResponse<KnowledgeBaseResponse>> {
ensureNonEmptyString(request.tag_id, "tag_id");
ensureNonEmptyString(request.name, "name");
async updateTag(request: DatasetTagUpdateRequest): Promise<DifyResponse<KnowledgeBaseResponse>> {
ensureNonEmptyString(request.tag_id, 'tag_id')
ensureNonEmptyString(request.name, 'name')
return this.http.request({
method: "PATCH",
path: "/datasets/tags",
method: 'PATCH',
path: '/datasets/tags',
data: request,
});
})
}
async deleteTag(
request: DatasetTagDeleteRequest
): Promise<DifyResponse<KnowledgeBaseResponse>> {
ensureNonEmptyString(request.tag_id, "tag_id");
async deleteTag(request: DatasetTagDeleteRequest): Promise<DifyResponse<KnowledgeBaseResponse>> {
ensureNonEmptyString(request.tag_id, 'tag_id')
return this.http.request({
method: "DELETE",
path: "/datasets/tags",
method: 'DELETE',
path: '/datasets/tags',
data: request,
});
})
}
async bindTags(
request: DatasetTagBindingRequest
): Promise<DifyResponse<KnowledgeBaseResponse>> {
ensureStringArray(request.tag_ids, "tag_ids");
ensureNonEmptyString(request.target_id, "target_id");
async bindTags(request: DatasetTagBindingRequest): Promise<DifyResponse<KnowledgeBaseResponse>> {
ensureStringArray(request.tag_ids, 'tag_ids')
ensureNonEmptyString(request.target_id, 'target_id')
return this.http.request({
method: "POST",
path: "/datasets/tags/binding",
method: 'POST',
path: '/datasets/tags/binding',
data: request,
});
})
}
async unbindTags(
request: DatasetTagUnbindingRequest
request: DatasetTagUnbindingRequest,
): Promise<DifyResponse<KnowledgeBaseResponse>> {
ensureNonEmptyString(request.tag_id, "tag_id");
ensureNonEmptyString(request.target_id, "target_id");
ensureNonEmptyString(request.tag_id, 'tag_id')
ensureNonEmptyString(request.target_id, 'target_id')
return this.http.request({
method: "POST",
path: "/datasets/tags/unbinding",
method: 'POST',
path: '/datasets/tags/unbinding',
data: request,
});
})
}
async getDatasetTags(
datasetId: string
): Promise<DifyResponse<KnowledgeBaseResponse>> {
ensureNonEmptyString(datasetId, "datasetId");
async getDatasetTags(datasetId: string): Promise<DifyResponse<KnowledgeBaseResponse>> {
ensureNonEmptyString(datasetId, 'datasetId')
return this.http.request({
method: "GET",
method: 'GET',
path: `/datasets/${datasetId}/tags`,
});
})
}
async createDocumentByText(
datasetId: string,
request: DocumentTextCreateRequest
request: DocumentTextCreateRequest,
): Promise<DifyResponse<KnowledgeBaseResponse>> {
ensureNonEmptyString(datasetId, "datasetId");
ensureNonEmptyString(request.name, "name");
ensureNonEmptyString(request.text, "text");
ensureNonEmptyString(datasetId, 'datasetId')
ensureNonEmptyString(request.name, 'name')
ensureNonEmptyString(request.text, 'text')
return this.http.request({
method: "POST",
method: 'POST',
path: `/datasets/${datasetId}/document/create_by_text`,
data: request,
});
})
}
async updateDocumentByText(
datasetId: string,
documentId: string,
request: DocumentTextUpdateRequest
request: DocumentTextUpdateRequest,
): Promise<DifyResponse<KnowledgeBaseResponse>> {
ensureNonEmptyString(datasetId, "datasetId");
ensureNonEmptyString(documentId, "documentId");
ensureNonEmptyString(datasetId, 'datasetId')
ensureNonEmptyString(documentId, 'documentId')
if (request.name !== undefined && request.name !== null) {
ensureNonEmptyString(request.name, "name");
ensureNonEmptyString(request.name, 'name')
}
return this.http.request({
method: "POST",
method: 'POST',
path: `/datasets/${datasetId}/documents/${documentId}/update_by_text`,
data: request,
});
})
}
async createDocumentByFile(
datasetId: string,
form: unknown
form: unknown,
): Promise<DifyResponse<KnowledgeBaseResponse>> {
ensureNonEmptyString(datasetId, "datasetId");
ensureFormData(form, "createDocumentByFile");
ensureNonEmptyString(datasetId, 'datasetId')
ensureFormData(form, 'createDocumentByFile')
return this.http.request({
method: "POST",
method: 'POST',
path: `/datasets/${datasetId}/document/create_by_file`,
data: form,
});
})
}
async updateDocumentByFile(
datasetId: string,
documentId: string,
form: unknown
form: unknown,
): Promise<DifyResponse<KnowledgeBaseResponse>> {
ensureNonEmptyString(datasetId, "datasetId");
ensureNonEmptyString(documentId, "documentId");
ensureFormData(form, "updateDocumentByFile");
ensureNonEmptyString(datasetId, 'datasetId')
ensureNonEmptyString(documentId, 'documentId')
ensureFormData(form, 'updateDocumentByFile')
return this.http.request({
method: "POST",
method: 'POST',
path: `/datasets/${datasetId}/documents/${documentId}/update_by_file`,
data: form,
});
})
}
async listDocuments(
datasetId: string,
options?: DocumentListOptions
options?: DocumentListOptions,
): Promise<DifyResponse<KnowledgeBaseResponse>> {
ensureNonEmptyString(datasetId, "datasetId");
ensureOptionalInt(options?.page, "page");
ensureOptionalInt(options?.limit, "limit");
ensureOptionalString(options?.keyword, "keyword");
ensureOptionalString(options?.status, "status");
ensureNonEmptyString(datasetId, 'datasetId')
ensureOptionalInt(options?.page, 'page')
ensureOptionalInt(options?.limit, 'limit')
ensureOptionalString(options?.keyword, 'keyword')
ensureOptionalString(options?.status, 'status')
return this.http.request({
method: "GET",
method: 'GET',
path: `/datasets/${datasetId}/documents`,
query: {
page: options?.page,
@@ -296,183 +279,183 @@ export class KnowledgeBaseClient extends DifyClient {
keyword: options?.keyword ?? undefined,
status: options?.status ?? undefined,
},
});
})
}
async getDocument(
datasetId: string,
documentId: string,
options?: DocumentGetOptions
options?: DocumentGetOptions,
): Promise<DifyResponse<KnowledgeBaseResponse>> {
ensureNonEmptyString(datasetId, "datasetId");
ensureNonEmptyString(documentId, "documentId");
ensureNonEmptyString(datasetId, 'datasetId')
ensureNonEmptyString(documentId, 'documentId')
if (options?.metadata) {
const allowed = new Set(["all", "only", "without"]);
const allowed = new Set(['all', 'only', 'without'])
if (!allowed.has(options.metadata)) {
throw new ValidationError("metadata must be one of all, only, without");
throw new ValidationError('metadata must be one of all, only, without')
}
}
return this.http.request({
method: "GET",
method: 'GET',
path: `/datasets/${datasetId}/documents/${documentId}`,
query: {
metadata: options?.metadata ?? undefined,
},
});
})
}
async deleteDocument(
datasetId: string,
documentId: string
documentId: string,
): Promise<DifyResponse<KnowledgeBaseResponse>> {
ensureNonEmptyString(datasetId, "datasetId");
ensureNonEmptyString(documentId, "documentId");
ensureNonEmptyString(datasetId, 'datasetId')
ensureNonEmptyString(documentId, 'documentId')
return this.http.request({
method: "DELETE",
method: 'DELETE',
path: `/datasets/${datasetId}/documents/${documentId}`,
});
})
}
async getDocumentIndexingStatus(
datasetId: string,
batch: string
batch: string,
): Promise<DifyResponse<KnowledgeBaseResponse>> {
ensureNonEmptyString(datasetId, "datasetId");
ensureNonEmptyString(batch, "batch");
ensureNonEmptyString(datasetId, 'datasetId')
ensureNonEmptyString(batch, 'batch')
return this.http.request({
method: "GET",
method: 'GET',
path: `/datasets/${datasetId}/documents/${batch}/indexing-status`,
});
})
}
async createSegments(
datasetId: string,
documentId: string,
request: SegmentCreateRequest
request: SegmentCreateRequest,
): Promise<DifyResponse<KnowledgeBaseResponse>> {
ensureNonEmptyString(datasetId, "datasetId");
ensureNonEmptyString(documentId, "documentId");
ensureNonEmptyArray(request.segments, "segments");
ensureNonEmptyString(datasetId, 'datasetId')
ensureNonEmptyString(documentId, 'documentId')
ensureNonEmptyArray(request.segments, 'segments')
return this.http.request({
method: "POST",
method: 'POST',
path: `/datasets/${datasetId}/documents/${documentId}/segments`,
data: request,
});
})
}
async listSegments(
datasetId: string,
documentId: string,
options?: SegmentListOptions
options?: SegmentListOptions,
): Promise<DifyResponse<KnowledgeBaseResponse>> {
ensureNonEmptyString(datasetId, "datasetId");
ensureNonEmptyString(documentId, "documentId");
ensureOptionalInt(options?.page, "page");
ensureOptionalInt(options?.limit, "limit");
ensureOptionalString(options?.keyword, "keyword");
ensureNonEmptyString(datasetId, 'datasetId')
ensureNonEmptyString(documentId, 'documentId')
ensureOptionalInt(options?.page, 'page')
ensureOptionalInt(options?.limit, 'limit')
ensureOptionalString(options?.keyword, 'keyword')
if (options?.status && options.status.length > 0) {
ensureStringArray(options.status, "status");
ensureStringArray(options.status, 'status')
}
const query: QueryParams = {
page: options?.page,
limit: options?.limit,
keyword: options?.keyword ?? undefined,
};
}
if (options?.status && options.status.length > 0) {
query.status = options.status;
query.status = options.status
}
return this.http.request({
method: "GET",
method: 'GET',
path: `/datasets/${datasetId}/documents/${documentId}/segments`,
query,
});
})
}
async getSegment(
datasetId: string,
documentId: string,
segmentId: string
segmentId: string,
): Promise<DifyResponse<KnowledgeBaseResponse>> {
ensureNonEmptyString(datasetId, "datasetId");
ensureNonEmptyString(documentId, "documentId");
ensureNonEmptyString(segmentId, "segmentId");
ensureNonEmptyString(datasetId, 'datasetId')
ensureNonEmptyString(documentId, 'documentId')
ensureNonEmptyString(segmentId, 'segmentId')
return this.http.request({
method: "GET",
method: 'GET',
path: `/datasets/${datasetId}/documents/${documentId}/segments/${segmentId}`,
});
})
}
async updateSegment(
datasetId: string,
documentId: string,
segmentId: string,
request: SegmentUpdateRequest
request: SegmentUpdateRequest,
): Promise<DifyResponse<KnowledgeBaseResponse>> {
ensureNonEmptyString(datasetId, "datasetId");
ensureNonEmptyString(documentId, "documentId");
ensureNonEmptyString(segmentId, "segmentId");
ensureNonEmptyString(datasetId, 'datasetId')
ensureNonEmptyString(documentId, 'documentId')
ensureNonEmptyString(segmentId, 'segmentId')
return this.http.request({
method: "POST",
method: 'POST',
path: `/datasets/${datasetId}/documents/${documentId}/segments/${segmentId}`,
data: request,
});
})
}
async deleteSegment(
datasetId: string,
documentId: string,
segmentId: string
segmentId: string,
): Promise<DifyResponse<KnowledgeBaseResponse>> {
ensureNonEmptyString(datasetId, "datasetId");
ensureNonEmptyString(documentId, "documentId");
ensureNonEmptyString(segmentId, "segmentId");
ensureNonEmptyString(datasetId, 'datasetId')
ensureNonEmptyString(documentId, 'documentId')
ensureNonEmptyString(segmentId, 'segmentId')
return this.http.request({
method: "DELETE",
method: 'DELETE',
path: `/datasets/${datasetId}/documents/${documentId}/segments/${segmentId}`,
});
})
}
async createChildChunk(
datasetId: string,
documentId: string,
segmentId: string,
request: ChildChunkCreateRequest
request: ChildChunkCreateRequest,
): Promise<DifyResponse<KnowledgeBaseResponse>> {
ensureNonEmptyString(datasetId, "datasetId");
ensureNonEmptyString(documentId, "documentId");
ensureNonEmptyString(segmentId, "segmentId");
ensureNonEmptyString(request.content, "content");
ensureNonEmptyString(datasetId, 'datasetId')
ensureNonEmptyString(documentId, 'documentId')
ensureNonEmptyString(segmentId, 'segmentId')
ensureNonEmptyString(request.content, 'content')
return this.http.request({
method: "POST",
method: 'POST',
path: `/datasets/${datasetId}/documents/${documentId}/segments/${segmentId}/child_chunks`,
data: request,
});
})
}
async listChildChunks(
datasetId: string,
documentId: string,
segmentId: string,
options?: ChildChunkListOptions
options?: ChildChunkListOptions,
): Promise<DifyResponse<KnowledgeBaseResponse>> {
ensureNonEmptyString(datasetId, "datasetId");
ensureNonEmptyString(documentId, "documentId");
ensureNonEmptyString(segmentId, "segmentId");
ensureOptionalInt(options?.page, "page");
ensureOptionalInt(options?.limit, "limit");
ensureOptionalString(options?.keyword, "keyword");
ensureNonEmptyString(datasetId, 'datasetId')
ensureNonEmptyString(documentId, 'documentId')
ensureNonEmptyString(segmentId, 'segmentId')
ensureOptionalInt(options?.page, 'page')
ensureOptionalInt(options?.limit, 'limit')
ensureOptionalString(options?.keyword, 'keyword')
return this.http.request({
method: "GET",
method: 'GET',
path: `/datasets/${datasetId}/documents/${documentId}/segments/${segmentId}/child_chunks`,
query: {
page: options?.page,
limit: options?.limit,
keyword: options?.keyword ?? undefined,
},
});
})
}
async updateChildChunk(
@@ -480,212 +463,206 @@ export class KnowledgeBaseClient extends DifyClient {
documentId: string,
segmentId: string,
childChunkId: string,
request: ChildChunkUpdateRequest
request: ChildChunkUpdateRequest,
): Promise<DifyResponse<KnowledgeBaseResponse>> {
ensureNonEmptyString(datasetId, "datasetId");
ensureNonEmptyString(documentId, "documentId");
ensureNonEmptyString(segmentId, "segmentId");
ensureNonEmptyString(childChunkId, "childChunkId");
ensureNonEmptyString(request.content, "content");
ensureNonEmptyString(datasetId, 'datasetId')
ensureNonEmptyString(documentId, 'documentId')
ensureNonEmptyString(segmentId, 'segmentId')
ensureNonEmptyString(childChunkId, 'childChunkId')
ensureNonEmptyString(request.content, 'content')
return this.http.request({
method: "PATCH",
method: 'PATCH',
path: `/datasets/${datasetId}/documents/${documentId}/segments/${segmentId}/child_chunks/${childChunkId}`,
data: request,
});
})
}
async deleteChildChunk(
datasetId: string,
documentId: string,
segmentId: string,
childChunkId: string
childChunkId: string,
): Promise<DifyResponse<KnowledgeBaseResponse>> {
ensureNonEmptyString(datasetId, "datasetId");
ensureNonEmptyString(documentId, "documentId");
ensureNonEmptyString(segmentId, "segmentId");
ensureNonEmptyString(childChunkId, "childChunkId");
ensureNonEmptyString(datasetId, 'datasetId')
ensureNonEmptyString(documentId, 'documentId')
ensureNonEmptyString(segmentId, 'segmentId')
ensureNonEmptyString(childChunkId, 'childChunkId')
return this.http.request({
method: "DELETE",
method: 'DELETE',
path: `/datasets/${datasetId}/documents/${documentId}/segments/${segmentId}/child_chunks/${childChunkId}`,
});
})
}
async listMetadata(
datasetId: string
): Promise<DifyResponse<KnowledgeBaseResponse>> {
ensureNonEmptyString(datasetId, "datasetId");
async listMetadata(datasetId: string): Promise<DifyResponse<KnowledgeBaseResponse>> {
ensureNonEmptyString(datasetId, 'datasetId')
return this.http.request({
method: "GET",
method: 'GET',
path: `/datasets/${datasetId}/metadata`,
});
})
}
async createMetadata(
datasetId: string,
request: MetadataCreateRequest
request: MetadataCreateRequest,
): Promise<DifyResponse<KnowledgeBaseResponse>> {
ensureNonEmptyString(datasetId, "datasetId");
ensureNonEmptyString(request.name, "name");
ensureNonEmptyString(request.type, "type");
ensureNonEmptyString(datasetId, 'datasetId')
ensureNonEmptyString(request.name, 'name')
ensureNonEmptyString(request.type, 'type')
return this.http.request({
method: "POST",
method: 'POST',
path: `/datasets/${datasetId}/metadata`,
data: request,
});
})
}
async updateMetadata(
datasetId: string,
metadataId: string,
request: MetadataUpdateRequest
request: MetadataUpdateRequest,
): Promise<DifyResponse<KnowledgeBaseResponse>> {
ensureNonEmptyString(datasetId, "datasetId");
ensureNonEmptyString(metadataId, "metadataId");
ensureNonEmptyString(request.name, "name");
ensureNonEmptyString(datasetId, 'datasetId')
ensureNonEmptyString(metadataId, 'metadataId')
ensureNonEmptyString(request.name, 'name')
return this.http.request({
method: "PATCH",
method: 'PATCH',
path: `/datasets/${datasetId}/metadata/${metadataId}`,
data: request,
});
})
}
async deleteMetadata(
datasetId: string,
metadataId: string
metadataId: string,
): Promise<DifyResponse<KnowledgeBaseResponse>> {
ensureNonEmptyString(datasetId, "datasetId");
ensureNonEmptyString(metadataId, "metadataId");
ensureNonEmptyString(datasetId, 'datasetId')
ensureNonEmptyString(metadataId, 'metadataId')
return this.http.request({
method: "DELETE",
method: 'DELETE',
path: `/datasets/${datasetId}/metadata/${metadataId}`,
});
})
}
async listBuiltInMetadata(
datasetId: string
): Promise<DifyResponse<KnowledgeBaseResponse>> {
ensureNonEmptyString(datasetId, "datasetId");
async listBuiltInMetadata(datasetId: string): Promise<DifyResponse<KnowledgeBaseResponse>> {
ensureNonEmptyString(datasetId, 'datasetId')
return this.http.request({
method: "GET",
method: 'GET',
path: `/datasets/${datasetId}/metadata/built-in`,
});
})
}
async updateBuiltInMetadata(
datasetId: string,
action: "enable" | "disable"
action: 'enable' | 'disable',
): Promise<DifyResponse<KnowledgeBaseResponse>> {
ensureNonEmptyString(datasetId, "datasetId");
ensureNonEmptyString(action, "action");
ensureNonEmptyString(datasetId, 'datasetId')
ensureNonEmptyString(action, 'action')
return this.http.request({
method: "POST",
method: 'POST',
path: `/datasets/${datasetId}/metadata/built-in/${action}`,
});
})
}
async updateDocumentsMetadata(
datasetId: string,
request: MetadataOperationRequest
request: MetadataOperationRequest,
): Promise<DifyResponse<KnowledgeBaseResponse>> {
ensureNonEmptyString(datasetId, "datasetId");
ensureNonEmptyArray(request.operation_data, "operation_data");
ensureNonEmptyString(datasetId, 'datasetId')
ensureNonEmptyArray(request.operation_data, 'operation_data')
return this.http.request({
method: "POST",
method: 'POST',
path: `/datasets/${datasetId}/documents/metadata`,
data: request,
});
})
}
async hitTesting(
datasetId: string,
request: HitTestingRequest
request: HitTestingRequest,
): Promise<DifyResponse<KnowledgeBaseResponse>> {
ensureNonEmptyString(datasetId, "datasetId");
ensureNonEmptyString(datasetId, 'datasetId')
if (request.query !== undefined && request.query !== null) {
ensureOptionalString(request.query, "query");
ensureOptionalString(request.query, 'query')
}
if (request.attachment_ids && request.attachment_ids.length > 0) {
ensureStringArray(request.attachment_ids, "attachment_ids");
ensureStringArray(request.attachment_ids, 'attachment_ids')
}
return this.http.request({
method: "POST",
method: 'POST',
path: `/datasets/${datasetId}/hit-testing`,
data: request,
});
})
}
async retrieve(
datasetId: string,
request: HitTestingRequest
request: HitTestingRequest,
): Promise<DifyResponse<KnowledgeBaseResponse>> {
ensureNonEmptyString(datasetId, "datasetId");
ensureNonEmptyString(datasetId, 'datasetId')
return this.http.request({
method: "POST",
method: 'POST',
path: `/datasets/${datasetId}/retrieve`,
data: request,
});
})
}
async listDatasourcePlugins(
datasetId: string,
options?: DatasourcePluginListOptions
options?: DatasourcePluginListOptions,
): Promise<DifyResponse<KnowledgeBaseResponse>> {
ensureNonEmptyString(datasetId, "datasetId");
ensureOptionalBoolean(options?.isPublished, "isPublished");
ensureNonEmptyString(datasetId, 'datasetId')
ensureOptionalBoolean(options?.isPublished, 'isPublished')
return this.http.request({
method: "GET",
method: 'GET',
path: `/datasets/${datasetId}/pipeline/datasource-plugins`,
query: {
is_published: options?.isPublished ?? undefined,
},
});
})
}
async runDatasourceNode(
datasetId: string,
nodeId: string,
request: DatasourceNodeRunRequest
request: DatasourceNodeRunRequest,
): Promise<DifyStream<PipelineStreamEvent>> {
ensureNonEmptyString(datasetId, "datasetId");
ensureNonEmptyString(nodeId, "nodeId");
ensureNonEmptyString(request.datasource_type, "datasource_type");
ensureNonEmptyString(datasetId, 'datasetId')
ensureNonEmptyString(nodeId, 'nodeId')
ensureNonEmptyString(request.datasource_type, 'datasource_type')
return this.http.requestStream<PipelineStreamEvent>({
method: "POST",
method: 'POST',
path: `/datasets/${datasetId}/pipeline/datasource/nodes/${nodeId}/run`,
data: request,
});
})
}
async runPipeline(
datasetId: string,
request: PipelineRunRequest
request: PipelineRunRequest,
): Promise<DifyResponse<KnowledgeBaseResponse> | DifyStream<PipelineStreamEvent>> {
ensureNonEmptyString(datasetId, "datasetId");
ensureNonEmptyString(request.datasource_type, "datasource_type");
ensureNonEmptyString(request.start_node_id, "start_node_id");
const shouldStream = request.response_mode === "streaming";
ensureNonEmptyString(datasetId, 'datasetId')
ensureNonEmptyString(request.datasource_type, 'datasource_type')
ensureNonEmptyString(request.start_node_id, 'start_node_id')
const shouldStream = request.response_mode === 'streaming'
if (shouldStream) {
return this.http.requestStream<PipelineStreamEvent>({
method: "POST",
method: 'POST',
path: `/datasets/${datasetId}/pipeline/run`,
data: request,
});
})
}
return this.http.request<KnowledgeBaseResponse>({
method: "POST",
method: 'POST',
path: `/datasets/${datasetId}/pipeline/run`,
data: request,
});
})
}
async uploadPipelineFile(
form: unknown
): Promise<DifyResponse<KnowledgeBaseResponse>> {
ensureFormData(form, "uploadPipelineFile");
async uploadPipelineFile(form: unknown): Promise<DifyResponse<KnowledgeBaseResponse>> {
ensureFormData(form, 'uploadPipelineFile')
return this.http.request({
method: "POST",
path: "/datasets/pipeline/file-upload",
method: 'POST',
path: '/datasets/pipeline/file-upload',
data: form,
});
})
}
}
@@ -1,4 +1,4 @@
import { describe, expect, it } from "vitest";
import { describe, expect, it } from 'vitest'
import {
ensureNonEmptyString,
ensureOptionalBoolean,
@@ -8,81 +8,80 @@ import {
ensureRating,
ensureStringArray,
validateParams,
} from "./validation";
} from './validation'
const makeLongString = (length: number) => "a".repeat(length);
const makeLongString = (length: number) => 'a'.repeat(length)
describe("validation utilities", () => {
it("ensureNonEmptyString throws on empty or whitespace", () => {
expect(() => ensureNonEmptyString("", "name")).toThrow();
expect(() => ensureNonEmptyString(" ", "name")).toThrow();
});
describe('validation utilities', () => {
it('ensureNonEmptyString throws on empty or whitespace', () => {
expect(() => ensureNonEmptyString('', 'name')).toThrow()
expect(() => ensureNonEmptyString(' ', 'name')).toThrow()
})
it("ensureNonEmptyString throws on overly long strings", () => {
expect(() => ensureNonEmptyString(makeLongString(10001), "name")).toThrow();
});
it('ensureNonEmptyString throws on overly long strings', () => {
expect(() => ensureNonEmptyString(makeLongString(10001), 'name')).toThrow()
})
it("ensureOptionalString ignores undefined and validates when set", () => {
expect(() => ensureOptionalString(undefined, "opt")).not.toThrow();
expect(() => ensureOptionalString("", "opt")).toThrow();
});
it('ensureOptionalString ignores undefined and validates when set', () => {
expect(() => ensureOptionalString(undefined, 'opt')).not.toThrow()
expect(() => ensureOptionalString('', 'opt')).toThrow()
})
it("ensureOptionalString throws on overly long strings", () => {
expect(() => ensureOptionalString(makeLongString(10001), "opt")).toThrow();
});
it('ensureOptionalString throws on overly long strings', () => {
expect(() => ensureOptionalString(makeLongString(10001), 'opt')).toThrow()
})
it("ensureOptionalInt validates integer", () => {
expect(() => ensureOptionalInt(undefined, "limit")).not.toThrow();
expect(() => ensureOptionalInt(1.2, "limit")).toThrow();
});
it('ensureOptionalInt validates integer', () => {
expect(() => ensureOptionalInt(undefined, 'limit')).not.toThrow()
expect(() => ensureOptionalInt(1.2, 'limit')).toThrow()
})
it("ensureOptionalBoolean validates boolean", () => {
expect(() => ensureOptionalBoolean(undefined, "flag")).not.toThrow();
expect(() => ensureOptionalBoolean("yes", "flag")).toThrow();
});
it('ensureOptionalBoolean validates boolean', () => {
expect(() => ensureOptionalBoolean(undefined, 'flag')).not.toThrow()
expect(() => ensureOptionalBoolean('yes', 'flag')).toThrow()
})
it("ensureStringArray enforces size and content", () => {
expect(() => ensureStringArray([], "items")).toThrow();
expect(() => ensureStringArray([""], "items")).toThrow();
it('ensureStringArray enforces size and content', () => {
expect(() => ensureStringArray([], 'items')).toThrow()
expect(() => ensureStringArray([''], 'items')).toThrow()
expect(() =>
ensureStringArray(Array.from({ length: 1001 }, () => "a"), "items")
).toThrow();
expect(() => ensureStringArray(["ok"], "items")).not.toThrow();
});
ensureStringArray(
Array.from({ length: 1001 }, () => 'a'),
'items',
),
).toThrow()
expect(() => ensureStringArray(['ok'], 'items')).not.toThrow()
})
it("ensureOptionalStringArray ignores undefined", () => {
expect(() => ensureOptionalStringArray(undefined, "tags")).not.toThrow();
});
it('ensureOptionalStringArray ignores undefined', () => {
expect(() => ensureOptionalStringArray(undefined, 'tags')).not.toThrow()
})
it("ensureOptionalStringArray validates when set", () => {
expect(() => ensureOptionalStringArray(["valid"], "tags")).not.toThrow();
expect(() => ensureOptionalStringArray([], "tags")).toThrow();
expect(() => ensureOptionalStringArray([""], "tags")).toThrow();
});
it('ensureOptionalStringArray validates when set', () => {
expect(() => ensureOptionalStringArray(['valid'], 'tags')).not.toThrow()
expect(() => ensureOptionalStringArray([], 'tags')).toThrow()
expect(() => ensureOptionalStringArray([''], 'tags')).toThrow()
})
it("ensureRating validates allowed values", () => {
expect(() => ensureRating(undefined)).not.toThrow();
expect(() => ensureRating("like")).not.toThrow();
expect(() => ensureRating("bad")).toThrow();
});
it('ensureRating validates allowed values', () => {
expect(() => ensureRating(undefined)).not.toThrow()
expect(() => ensureRating('like')).not.toThrow()
expect(() => ensureRating('bad')).toThrow()
})
it("validateParams enforces generic rules", () => {
expect(() => validateParams({ user: 123 })).toThrow();
expect(() => validateParams({ rating: "bad" })).toThrow();
expect(() => validateParams({ page: 1.1 })).toThrow();
expect(() => validateParams({ files: "bad" })).toThrow();
expect(() => validateParams({ keyword: "" })).not.toThrow();
expect(() => validateParams({ name: makeLongString(10001) })).toThrow();
expect(() =>
validateParams({ items: Array.from({ length: 1001 }, () => "a") })
).toThrow();
it('validateParams enforces generic rules', () => {
expect(() => validateParams({ user: 123 })).toThrow()
expect(() => validateParams({ rating: 'bad' })).toThrow()
expect(() => validateParams({ page: 1.1 })).toThrow()
expect(() => validateParams({ files: 'bad' })).toThrow()
expect(() => validateParams({ keyword: '' })).not.toThrow()
expect(() => validateParams({ name: makeLongString(10001) })).toThrow()
expect(() => validateParams({ items: Array.from({ length: 1001 }, () => 'a') })).toThrow()
expect(() =>
validateParams({
data: Object.fromEntries(
Array.from({ length: 101 }, (_, i) => [String(i), i])
),
})
).toThrow();
expect(() => validateParams({ user: "u", page: 1 })).not.toThrow();
});
});
data: Object.fromEntries(Array.from({ length: 101 }, (_, i) => [String(i), i])),
}),
).toThrow()
expect(() => validateParams({ user: 'u', page: 1 })).not.toThrow()
})
})
+45 -57
View File
@@ -1,21 +1,16 @@
import { ValidationError } from "../errors/dify-error";
import { isRecord } from "../internal/type-guards";
import { ValidationError } from '../errors/dify-error'
import { isRecord } from '../internal/type-guards'
const MAX_STRING_LENGTH = 10000;
const MAX_LIST_LENGTH = 1000;
const MAX_DICT_LENGTH = 100;
const MAX_STRING_LENGTH = 10000
const MAX_LIST_LENGTH = 1000
const MAX_DICT_LENGTH = 100
export function ensureNonEmptyString(
value: unknown,
name: string
): asserts value is string {
if (typeof value !== "string" || value.trim().length === 0) {
throw new ValidationError(`${name} must be a non-empty string`);
export function ensureNonEmptyString(value: unknown, name: string): asserts value is string {
if (typeof value !== 'string' || value.trim().length === 0) {
throw new ValidationError(`${name} must be a non-empty string`)
}
if (value.length > MAX_STRING_LENGTH) {
throw new ValidationError(
`${name} exceeds maximum length of ${MAX_STRING_LENGTH} characters`
);
throw new ValidationError(`${name} exceeds maximum length of ${MAX_STRING_LENGTH} characters`)
}
}
@@ -28,110 +23,103 @@ export function ensureNonEmptyString(
*/
export function ensureOptionalString(value: unknown, name: string): void {
if (value === undefined || value === null) {
return;
return
}
if (typeof value !== "string" || value.trim().length === 0) {
throw new ValidationError(`${name} must be a non-empty string when set`);
if (typeof value !== 'string' || value.trim().length === 0) {
throw new ValidationError(`${name} must be a non-empty string when set`)
}
if (value.length > MAX_STRING_LENGTH) {
throw new ValidationError(
`${name} exceeds maximum length of ${MAX_STRING_LENGTH} characters`
);
throw new ValidationError(`${name} exceeds maximum length of ${MAX_STRING_LENGTH} characters`)
}
}
export function ensureOptionalInt(value: unknown, name: string): void {
if (value === undefined || value === null) {
return;
return
}
if (!Number.isInteger(value)) {
throw new ValidationError(`${name} must be an integer when set`);
throw new ValidationError(`${name} must be an integer when set`)
}
}
export function ensureOptionalBoolean(value: unknown, name: string): void {
if (value === undefined || value === null) {
return;
return
}
if (typeof value !== "boolean") {
throw new ValidationError(`${name} must be a boolean when set`);
if (typeof value !== 'boolean') {
throw new ValidationError(`${name} must be a boolean when set`)
}
}
export function ensureStringArray(value: unknown, name: string): void {
if (!Array.isArray(value) || value.length === 0) {
throw new ValidationError(`${name} must be a non-empty string array`);
throw new ValidationError(`${name} must be a non-empty string array`)
}
if (value.length > MAX_LIST_LENGTH) {
throw new ValidationError(
`${name} exceeds maximum size of ${MAX_LIST_LENGTH} items`
);
throw new ValidationError(`${name} exceeds maximum size of ${MAX_LIST_LENGTH} items`)
}
value.forEach((item) => {
if (typeof item !== "string" || item.trim().length === 0) {
throw new ValidationError(`${name} must contain non-empty strings`);
if (typeof item !== 'string' || item.trim().length === 0) {
throw new ValidationError(`${name} must contain non-empty strings`)
}
});
})
}
export function ensureOptionalStringArray(value: unknown, name: string): void {
if (value === undefined || value === null) {
return;
return
}
ensureStringArray(value, name);
ensureStringArray(value, name)
}
export function ensureRating(value: unknown): void {
if (value === undefined || value === null) {
return;
return
}
if (value !== "like" && value !== "dislike") {
throw new ValidationError("rating must be either 'like' or 'dislike'");
if (value !== 'like' && value !== 'dislike') {
throw new ValidationError("rating must be either 'like' or 'dislike'")
}
}
export function validateParams(params: Record<string, unknown>): void {
Object.entries(params).forEach(([key, value]) => {
if (value === undefined || value === null) {
return;
return
}
// Only check max length for strings; empty strings are allowed for optional params
// Required fields are validated at method level via ensureNonEmptyString
if (typeof value === "string") {
if (typeof value === 'string') {
if (value.length > MAX_STRING_LENGTH) {
throw new ValidationError(
`Parameter '${key}' exceeds maximum length of ${MAX_STRING_LENGTH} characters`
);
`Parameter '${key}' exceeds maximum length of ${MAX_STRING_LENGTH} characters`,
)
}
} else if (Array.isArray(value)) {
if (value.length > MAX_LIST_LENGTH) {
throw new ValidationError(
`Parameter '${key}' exceeds maximum size of ${MAX_LIST_LENGTH} items`
);
`Parameter '${key}' exceeds maximum size of ${MAX_LIST_LENGTH} items`,
)
}
} else if (isRecord(value)) {
if (Object.keys(value).length > MAX_DICT_LENGTH) {
throw new ValidationError(
`Parameter '${key}' exceeds maximum size of ${MAX_DICT_LENGTH} items`
);
`Parameter '${key}' exceeds maximum size of ${MAX_DICT_LENGTH} items`,
)
}
}
if (key === "user" && typeof value !== "string") {
throw new ValidationError(`Parameter '${key}' must be a string`);
if (key === 'user' && typeof value !== 'string') {
throw new ValidationError(`Parameter '${key}' must be a string`)
}
if (
(key === "page" || key === "limit" || key === "page_size") &&
!Number.isInteger(value)
) {
throw new ValidationError(`Parameter '${key}' must be an integer`);
if ((key === 'page' || key === 'limit' || key === 'page_size') && !Number.isInteger(value)) {
throw new ValidationError(`Parameter '${key}' must be an integer`)
}
if (key === "files" && !Array.isArray(value) && typeof value !== "object") {
throw new ValidationError(`Parameter '${key}' must be a list or dict`);
if (key === 'files' && !Array.isArray(value) && typeof value !== 'object') {
throw new ValidationError(`Parameter '${key}' must be a list or dict`)
}
if (key === "rating" && value !== "like" && value !== "dislike") {
throw new ValidationError(`Parameter '${key}' must be 'like' or 'dislike'`);
if (key === 'rating' && value !== 'like' && value !== 'dislike') {
throw new ValidationError(`Parameter '${key}' must be 'like' or 'dislike'`)
}
});
})
}
+81 -81
View File
@@ -1,118 +1,118 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import { WorkflowClient } from "./workflow";
import { createHttpClientWithSpies } from "../../tests/test-utils";
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { createHttpClientWithSpies } from '../../tests/test-utils'
import { WorkflowClient } from './workflow'
describe("WorkflowClient", () => {
describe('WorkflowClient', () => {
beforeEach(() => {
vi.restoreAllMocks();
});
vi.restoreAllMocks()
})
it("runs workflows with blocking and streaming modes", async () => {
const { client, request, requestStream } = createHttpClientWithSpies();
const workflow = new WorkflowClient(client);
it('runs workflows with blocking and streaming modes', async () => {
const { client, request, requestStream } = createHttpClientWithSpies()
const workflow = new WorkflowClient(client)
await workflow.run({ inputs: { input: "x" }, user: "user" });
await workflow.run({ input: "x" }, "user", true);
await workflow.run({ inputs: { input: 'x' }, user: 'user' })
await workflow.run({ input: 'x' }, 'user', true)
expect(request).toHaveBeenCalledWith({
method: "POST",
path: "/workflows/run",
method: 'POST',
path: '/workflows/run',
data: {
inputs: { input: "x" },
user: "user",
inputs: { input: 'x' },
user: 'user',
},
});
})
expect(requestStream).toHaveBeenCalledWith({
method: "POST",
path: "/workflows/run",
method: 'POST',
path: '/workflows/run',
data: {
inputs: { input: "x" },
user: "user",
response_mode: "streaming",
inputs: { input: 'x' },
user: 'user',
response_mode: 'streaming',
},
});
});
})
})
it("runs workflow by id", async () => {
const { client, request, requestStream } = createHttpClientWithSpies();
const workflow = new WorkflowClient(client);
it('runs workflow by id', async () => {
const { client, request, requestStream } = createHttpClientWithSpies()
const workflow = new WorkflowClient(client)
await workflow.runById("wf", {
inputs: { input: "x" },
user: "user",
response_mode: "blocking",
});
await workflow.runById("wf", {
inputs: { input: "x" },
user: "user",
response_mode: "streaming",
});
await workflow.runById('wf', {
inputs: { input: 'x' },
user: 'user',
response_mode: 'blocking',
})
await workflow.runById('wf', {
inputs: { input: 'x' },
user: 'user',
response_mode: 'streaming',
})
expect(request).toHaveBeenCalledWith({
method: "POST",
path: "/workflows/wf/run",
method: 'POST',
path: '/workflows/wf/run',
data: {
inputs: { input: "x" },
user: "user",
response_mode: "blocking",
inputs: { input: 'x' },
user: 'user',
response_mode: 'blocking',
},
});
})
expect(requestStream).toHaveBeenCalledWith({
method: "POST",
path: "/workflows/wf/run",
method: 'POST',
path: '/workflows/wf/run',
data: {
inputs: { input: "x" },
user: "user",
response_mode: "streaming",
inputs: { input: 'x' },
user: 'user',
response_mode: 'streaming',
},
});
});
})
})
it("gets run details and stops workflow", async () => {
const { client, request } = createHttpClientWithSpies();
const workflow = new WorkflowClient(client);
it('gets run details and stops workflow', async () => {
const { client, request } = createHttpClientWithSpies()
const workflow = new WorkflowClient(client)
await workflow.getRun("run");
await workflow.stop("task", "user");
await workflow.getRun('run')
await workflow.stop('task', 'user')
expect(request).toHaveBeenCalledWith({
method: "GET",
path: "/workflows/run/run",
});
method: 'GET',
path: '/workflows/run/run',
})
expect(request).toHaveBeenCalledWith({
method: "POST",
path: "/workflows/tasks/task/stop",
data: { user: "user" },
});
});
method: 'POST',
path: '/workflows/tasks/task/stop',
data: { user: 'user' },
})
})
it("fetches workflow logs", async () => {
const { client, request } = createHttpClientWithSpies();
const workflow = new WorkflowClient(client);
it('fetches workflow logs', async () => {
const { client, request } = createHttpClientWithSpies()
const workflow = new WorkflowClient(client)
await workflow.getLogs({
keyword: "k",
status: "succeeded",
startTime: "2024-01-01",
endTime: "2024-01-02",
createdByEndUserSessionId: "session-123",
keyword: 'k',
status: 'succeeded',
startTime: '2024-01-01',
endTime: '2024-01-02',
createdByEndUserSessionId: 'session-123',
page: 1,
limit: 20,
});
})
expect(request).toHaveBeenCalledWith({
method: "GET",
path: "/workflows/logs",
method: 'GET',
path: '/workflows/logs',
query: {
keyword: "k",
status: "succeeded",
created_at__before: "2024-01-02",
created_at__after: "2024-01-01",
created_by_end_user_session_id: "session-123",
keyword: 'k',
status: 'succeeded',
created_at__before: '2024-01-02',
created_at__after: '2024-01-01',
created_by_end_user_session_id: 'session-123',
created_by_account: undefined,
page: 1,
limit: 20,
},
});
});
});
})
})
})
+66 -76
View File
@@ -1,103 +1,96 @@
import { DifyClient } from "./base";
import type { WorkflowRunRequest, WorkflowRunResponse } from "../types/workflow";
import type {
DifyResponse,
DifyStream,
JsonObject,
QueryParams,
SuccessResponse,
} from "../types/common";
import {
ensureNonEmptyString,
ensureOptionalInt,
ensureOptionalString,
} from "./validation";
} from '../types/common'
import type { WorkflowRunRequest, WorkflowRunResponse } from '../types/workflow'
import { DifyClient } from './base'
import { ensureNonEmptyString, ensureOptionalInt, ensureOptionalString } from './validation'
export class WorkflowClient extends DifyClient {
run(
request: WorkflowRunRequest
): Promise<DifyResponse<WorkflowRunResponse> | DifyStream<WorkflowRunResponse>>;
request: WorkflowRunRequest,
): Promise<DifyResponse<WorkflowRunResponse> | DifyStream<WorkflowRunResponse>>
run(
inputs: JsonObject,
user: string,
stream?: boolean
): Promise<DifyResponse<WorkflowRunResponse> | DifyStream<WorkflowRunResponse>>;
stream?: boolean,
): Promise<DifyResponse<WorkflowRunResponse> | DifyStream<WorkflowRunResponse>>
run(
inputOrRequest: WorkflowRunRequest | JsonObject,
user?: string,
stream = false
stream = false,
): Promise<DifyResponse<WorkflowRunResponse> | DifyStream<WorkflowRunResponse>> {
let payload: WorkflowRunRequest;
let shouldStream = stream;
let payload: WorkflowRunRequest
let shouldStream = stream
if (user === undefined && "user" in (inputOrRequest as WorkflowRunRequest)) {
payload = inputOrRequest as WorkflowRunRequest;
shouldStream = payload.response_mode === "streaming";
if (user === undefined && 'user' in (inputOrRequest as WorkflowRunRequest)) {
payload = inputOrRequest as WorkflowRunRequest
shouldStream = payload.response_mode === 'streaming'
} else {
ensureNonEmptyString(user, "user");
ensureNonEmptyString(user, 'user')
payload = {
inputs: inputOrRequest,
user,
response_mode: stream ? "streaming" : "blocking",
};
response_mode: stream ? 'streaming' : 'blocking',
}
}
ensureNonEmptyString(payload.user, "user");
ensureNonEmptyString(payload.user, 'user')
if (shouldStream) {
return this.http.requestStream<WorkflowRunResponse>({
method: "POST",
path: "/workflows/run",
method: 'POST',
path: '/workflows/run',
data: payload,
});
})
}
return this.http.request<WorkflowRunResponse>({
method: "POST",
path: "/workflows/run",
method: 'POST',
path: '/workflows/run',
data: payload,
});
})
}
runById(
workflowId: string,
request: WorkflowRunRequest
request: WorkflowRunRequest,
): Promise<DifyResponse<WorkflowRunResponse> | DifyStream<WorkflowRunResponse>> {
ensureNonEmptyString(workflowId, "workflowId");
ensureNonEmptyString(request.user, "user");
if (request.response_mode === "streaming") {
ensureNonEmptyString(workflowId, 'workflowId')
ensureNonEmptyString(request.user, 'user')
if (request.response_mode === 'streaming') {
return this.http.requestStream<WorkflowRunResponse>({
method: "POST",
method: 'POST',
path: `/workflows/${workflowId}/run`,
data: request,
});
})
}
return this.http.request<WorkflowRunResponse>({
method: "POST",
method: 'POST',
path: `/workflows/${workflowId}/run`,
data: request,
});
})
}
getRun(workflowRunId: string): Promise<DifyResponse<WorkflowRunResponse>> {
ensureNonEmptyString(workflowRunId, "workflowRunId");
ensureNonEmptyString(workflowRunId, 'workflowRunId')
return this.http.request({
method: "GET",
method: 'GET',
path: `/workflows/run/${workflowRunId}`,
});
})
}
stop(
taskId: string,
user: string
): Promise<DifyResponse<SuccessResponse>> {
ensureNonEmptyString(taskId, "taskId");
ensureNonEmptyString(user, "user");
stop(taskId: string, user: string): Promise<DifyResponse<SuccessResponse>> {
ensureNonEmptyString(taskId, 'taskId')
ensureNonEmptyString(user, 'user')
return this.http.request<SuccessResponse>({
method: "POST",
method: 'POST',
path: `/workflows/tasks/${taskId}/stop`,
data: { user },
});
})
}
/**
@@ -107,49 +100,46 @@ export class WorkflowClient extends DifyClient {
* or `createdByAccount` (account ID), not by a generic `user` parameter.
*/
getLogs(options?: {
keyword?: string;
status?: string;
createdAtBefore?: string;
createdAtAfter?: string;
createdByEndUserSessionId?: string;
createdByAccount?: string;
page?: number;
limit?: number;
startTime?: string;
endTime?: string;
keyword?: string
status?: string
createdAtBefore?: string
createdAtAfter?: string
createdByEndUserSessionId?: string
createdByAccount?: string
page?: number
limit?: number
startTime?: string
endTime?: string
}): Promise<DifyResponse<JsonObject>> {
if (options?.keyword) {
ensureOptionalString(options.keyword, "keyword");
ensureOptionalString(options.keyword, 'keyword')
}
if (options?.status) {
ensureOptionalString(options.status, "status");
ensureOptionalString(options.status, 'status')
}
if (options?.createdAtBefore) {
ensureOptionalString(options.createdAtBefore, "createdAtBefore");
ensureOptionalString(options.createdAtBefore, 'createdAtBefore')
}
if (options?.createdAtAfter) {
ensureOptionalString(options.createdAtAfter, "createdAtAfter");
ensureOptionalString(options.createdAtAfter, 'createdAtAfter')
}
if (options?.createdByEndUserSessionId) {
ensureOptionalString(
options.createdByEndUserSessionId,
"createdByEndUserSessionId"
);
ensureOptionalString(options.createdByEndUserSessionId, 'createdByEndUserSessionId')
}
if (options?.createdByAccount) {
ensureOptionalString(options.createdByAccount, "createdByAccount");
ensureOptionalString(options.createdByAccount, 'createdByAccount')
}
if (options?.startTime) {
ensureOptionalString(options.startTime, "startTime");
ensureOptionalString(options.startTime, 'startTime')
}
if (options?.endTime) {
ensureOptionalString(options.endTime, "endTime");
ensureOptionalString(options.endTime, 'endTime')
}
ensureOptionalInt(options?.page, "page");
ensureOptionalInt(options?.limit, "limit");
ensureOptionalInt(options?.page, 'page')
ensureOptionalInt(options?.limit, 'limit')
const createdAtAfter = options?.createdAtAfter ?? options?.startTime;
const createdAtBefore = options?.createdAtBefore ?? options?.endTime;
const createdAtAfter = options?.createdAtAfter ?? options?.startTime
const createdAtBefore = options?.createdAtBefore ?? options?.endTime
const query: QueryParams = {
keyword: options?.keyword,
@@ -160,12 +150,12 @@ export class WorkflowClient extends DifyClient {
created_by_account: options?.createdByAccount,
page: options?.page,
limit: options?.limit,
};
}
return this.http.request({
method: "GET",
path: "/workflows/logs",
method: 'GET',
path: '/workflows/logs',
query,
});
})
}
}
+15 -15
View File
@@ -1,21 +1,21 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import { WorkspaceClient } from "./workspace";
import { createHttpClientWithSpies } from "../../tests/test-utils";
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { createHttpClientWithSpies } from '../../tests/test-utils'
import { WorkspaceClient } from './workspace'
describe("WorkspaceClient", () => {
describe('WorkspaceClient', () => {
beforeEach(() => {
vi.restoreAllMocks();
});
vi.restoreAllMocks()
})
it("gets models by type", async () => {
const { client, request } = createHttpClientWithSpies();
const workspace = new WorkspaceClient(client);
it('gets models by type', async () => {
const { client, request } = createHttpClientWithSpies()
const workspace = new WorkspaceClient(client)
await workspace.getModelsByType("llm");
await workspace.getModelsByType('llm')
expect(request).toHaveBeenCalledWith({
method: "GET",
path: "/workspaces/current/models/model-types/llm",
});
});
});
method: 'GET',
path: '/workspaces/current/models/model-types/llm',
})
})
})
+8 -8
View File
@@ -1,16 +1,16 @@
import { DifyClient } from "./base";
import type { WorkspaceModelType, WorkspaceModelsResponse } from "../types/workspace";
import type { DifyResponse } from "../types/common";
import { ensureNonEmptyString } from "./validation";
import type { DifyResponse } from '../types/common'
import type { WorkspaceModelType, WorkspaceModelsResponse } from '../types/workspace'
import { DifyClient } from './base'
import { ensureNonEmptyString } from './validation'
export class WorkspaceClient extends DifyClient {
async getModelsByType(
modelType: WorkspaceModelType
modelType: WorkspaceModelType,
): Promise<DifyResponse<WorkspaceModelsResponse>> {
ensureNonEmptyString(modelType, "modelType");
ensureNonEmptyString(modelType, 'modelType')
return this.http.request({
method: "GET",
method: 'GET',
path: `/workspaces/current/models/model-types/${modelType}`,
});
})
}
}
@@ -1,4 +1,4 @@
import { describe, expect, it } from "vitest";
import { describe, expect, it } from 'vitest'
import {
APIError,
AuthenticationError,
@@ -8,30 +8,30 @@ import {
RateLimitError,
TimeoutError,
ValidationError,
} from "./dify-error";
} from './dify-error'
describe("Dify errors", () => {
it("sets base error fields", () => {
const err = new DifyError("base", {
describe('Dify errors', () => {
it('sets base error fields', () => {
const err = new DifyError('base', {
statusCode: 400,
responseBody: { message: "bad" },
requestId: "req",
responseBody: { message: 'bad' },
requestId: 'req',
retryAfter: 1,
});
expect(err.name).toBe("DifyError");
expect(err.statusCode).toBe(400);
expect(err.responseBody).toEqual({ message: "bad" });
expect(err.requestId).toBe("req");
expect(err.retryAfter).toBe(1);
});
})
expect(err.name).toBe('DifyError')
expect(err.statusCode).toBe(400)
expect(err.responseBody).toEqual({ message: 'bad' })
expect(err.requestId).toBe('req')
expect(err.retryAfter).toBe(1)
})
it("creates specific error types", () => {
expect(new APIError("api").name).toBe("APIError");
expect(new AuthenticationError("auth").name).toBe("AuthenticationError");
expect(new RateLimitError("rate").name).toBe("RateLimitError");
expect(new ValidationError("val").name).toBe("ValidationError");
expect(new NetworkError("net").name).toBe("NetworkError");
expect(new TimeoutError("timeout").name).toBe("TimeoutError");
expect(new FileUploadError("upload").name).toBe("FileUploadError");
});
});
it('creates specific error types', () => {
expect(new APIError('api').name).toBe('APIError')
expect(new AuthenticationError('auth').name).toBe('AuthenticationError')
expect(new RateLimitError('rate').name).toBe('RateLimitError')
expect(new ValidationError('val').name).toBe('ValidationError')
expect(new NetworkError('net').name).toBe('NetworkError')
expect(new TimeoutError('timeout').name).toBe('TimeoutError')
expect(new FileUploadError('upload').name).toBe('FileUploadError')
})
})
+31 -31
View File
@@ -1,75 +1,75 @@
export type DifyErrorOptions = {
statusCode?: number;
responseBody?: unknown;
requestId?: string;
retryAfter?: number;
cause?: unknown;
};
statusCode?: number
responseBody?: unknown
requestId?: string
retryAfter?: number
cause?: unknown
}
export class DifyError extends Error {
statusCode?: number;
responseBody?: unknown;
requestId?: string;
retryAfter?: number;
statusCode?: number
responseBody?: unknown
requestId?: string
retryAfter?: number
constructor(message: string, options: DifyErrorOptions = {}) {
super(message);
this.name = "DifyError";
this.statusCode = options.statusCode;
this.responseBody = options.responseBody;
this.requestId = options.requestId;
this.retryAfter = options.retryAfter;
super(message)
this.name = 'DifyError'
this.statusCode = options.statusCode
this.responseBody = options.responseBody
this.requestId = options.requestId
this.retryAfter = options.retryAfter
if (options.cause) {
(this as { cause?: unknown }).cause = options.cause;
;(this as { cause?: unknown }).cause = options.cause
}
}
}
export class APIError extends DifyError {
constructor(message: string, options: DifyErrorOptions = {}) {
super(message, options);
this.name = "APIError";
super(message, options)
this.name = 'APIError'
}
}
export class AuthenticationError extends APIError {
constructor(message: string, options: DifyErrorOptions = {}) {
super(message, options);
this.name = "AuthenticationError";
super(message, options)
this.name = 'AuthenticationError'
}
}
export class RateLimitError extends APIError {
constructor(message: string, options: DifyErrorOptions = {}) {
super(message, options);
this.name = "RateLimitError";
super(message, options)
this.name = 'RateLimitError'
}
}
export class ValidationError extends APIError {
constructor(message: string, options: DifyErrorOptions = {}) {
super(message, options);
this.name = "ValidationError";
super(message, options)
this.name = 'ValidationError'
}
}
export class NetworkError extends DifyError {
constructor(message: string, options: DifyErrorOptions = {}) {
super(message, options);
this.name = "NetworkError";
super(message, options)
this.name = 'NetworkError'
}
}
export class TimeoutError extends DifyError {
constructor(message: string, options: DifyErrorOptions = {}) {
super(message, options);
this.name = "TimeoutError";
super(message, options)
this.name = 'TimeoutError'
}
}
export class FileUploadError extends DifyError {
constructor(message: string, options: DifyErrorOptions = {}) {
super(message, options);
this.name = "FileUploadError";
super(message, options)
this.name = 'FileUploadError'
}
}
+333 -352
View File
@@ -1,5 +1,5 @@
import { Readable, Stream } from "node:stream";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { Readable, Stream } from 'node:stream'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import {
APIError,
AuthenticationError,
@@ -8,43 +8,40 @@ import {
RateLimitError,
TimeoutError,
ValidationError,
} from "../errors/dify-error";
import { HttpClient } from "./client";
} from '../errors/dify-error'
import { HttpClient } from './client'
const stubFetch = (): ReturnType<typeof vi.fn> => {
const fetchMock = vi.fn();
vi.stubGlobal("fetch", fetchMock);
return fetchMock;
};
const fetchMock = vi.fn()
vi.stubGlobal('fetch', fetchMock)
return fetchMock
}
const getFetchCall = (
fetchMock: ReturnType<typeof vi.fn>,
index = 0
index = 0,
): [string, RequestInit | undefined] => {
const call = fetchMock.mock.calls[index];
const call = fetchMock.mock.calls[index]
if (!call) {
throw new Error(`Missing fetch call at index ${index}`);
throw new Error(`Missing fetch call at index ${index}`)
}
return call as [string, RequestInit | undefined];
};
return call as [string, RequestInit | undefined]
}
const getDuplex = (init: RequestInit | undefined) =>
init && "duplex" in init ? init.duplex : undefined;
init && 'duplex' in init ? init.duplex : undefined
const toHeaderRecord = (headers: HeadersInit | undefined): Record<string, string> =>
Object.fromEntries(new Headers(headers).entries());
Object.fromEntries(new Headers(headers).entries())
const jsonResponse = (
body: unknown,
init: ResponseInit = {}
): Response =>
const jsonResponse = (body: unknown, init: ResponseInit = {}): Response =>
new Response(JSON.stringify(body), {
...init,
headers: {
"content-type": "application/json",
'content-type': 'application/json',
...init.headers,
},
});
})
const textResponse = (body: string, init: ResponseInit = {}): Response =>
new Response(body, {
@@ -52,479 +49,463 @@ const textResponse = (body: string, init: ResponseInit = {}): Response =>
headers: {
...init.headers,
},
});
})
describe("HttpClient", () => {
describe('HttpClient', () => {
beforeEach(() => {
vi.restoreAllMocks();
vi.unstubAllGlobals();
});
vi.restoreAllMocks()
vi.unstubAllGlobals()
})
it("builds requests with auth headers and JSON content type", async () => {
const fetchMock = stubFetch();
it('builds requests with auth headers and JSON content type', async () => {
const fetchMock = stubFetch()
fetchMock.mockResolvedValueOnce(
jsonResponse({ ok: true }, { status: 200, headers: { "x-request-id": "req" } })
);
jsonResponse({ ok: true }, { status: 200, headers: { 'x-request-id': 'req' } }),
)
const client = new HttpClient({ apiKey: "test" });
const client = new HttpClient({ apiKey: 'test' })
const response = await client.request({
method: "POST",
path: "/chat-messages",
data: { user: "u" },
});
method: 'POST',
path: '/chat-messages',
data: { user: 'u' },
})
expect(response.requestId).toBe("req");
expect(fetchMock).toHaveBeenCalledTimes(1);
const [url, init] = getFetchCall(fetchMock);
expect(url).toBe("https://api.dify.ai/v1/chat-messages");
expect(response.requestId).toBe('req')
expect(fetchMock).toHaveBeenCalledTimes(1)
const [url, init] = getFetchCall(fetchMock)
expect(url).toBe('https://api.dify.ai/v1/chat-messages')
expect(toHeaderRecord(init?.headers)).toMatchObject({
authorization: "Bearer test",
"content-type": "application/json",
"user-agent": "dify-client-node",
});
expect(init?.body).toBe(JSON.stringify({ user: "u" }));
});
authorization: 'Bearer test',
'content-type': 'application/json',
'user-agent': 'dify-client-node',
})
expect(init?.body).toBe(JSON.stringify({ user: 'u' }))
})
it("serializes array query params", async () => {
const fetchMock = stubFetch();
fetchMock.mockResolvedValueOnce(jsonResponse("ok", { status: 200 }));
it('serializes array query params', async () => {
const fetchMock = stubFetch()
fetchMock.mockResolvedValueOnce(jsonResponse('ok', { status: 200 }))
const client = new HttpClient({ apiKey: "test" });
const client = new HttpClient({ apiKey: 'test' })
await client.requestRaw({
method: "GET",
path: "/datasets",
query: { tag_ids: ["a", "b"], limit: 2 },
});
method: 'GET',
path: '/datasets',
query: { tag_ids: ['a', 'b'], limit: 2 },
})
const [url] = getFetchCall(fetchMock);
expect(new URL(url).searchParams.toString()).toBe(
"tag_ids=a&tag_ids=b&limit=2"
);
});
const [url] = getFetchCall(fetchMock)
expect(new URL(url).searchParams.toString()).toBe('tag_ids=a&tag_ids=b&limit=2')
})
it("returns SSE stream helpers", async () => {
const fetchMock = stubFetch();
it('returns SSE stream helpers', async () => {
const fetchMock = stubFetch()
fetchMock.mockResolvedValueOnce(
new Response('data: {"text":"hi"}\n\n', {
status: 200,
headers: { "x-request-id": "req" },
})
);
headers: { 'x-request-id': 'req' },
}),
)
const client = new HttpClient({ apiKey: "test" });
const client = new HttpClient({ apiKey: 'test' })
const stream = await client.requestStream({
method: "POST",
path: "/chat-messages",
data: { user: "u" },
});
method: 'POST',
path: '/chat-messages',
data: { user: 'u' },
})
expect(stream.status).toBe(200);
expect(stream.requestId).toBe("req");
await expect(stream.toText()).resolves.toBe("hi");
});
expect(stream.status).toBe(200)
expect(stream.requestId).toBe('req')
await expect(stream.toText()).resolves.toBe('hi')
})
it("returns binary stream helpers", async () => {
const fetchMock = stubFetch();
it('returns binary stream helpers', async () => {
const fetchMock = stubFetch()
fetchMock.mockResolvedValueOnce(
new Response("chunk", {
new Response('chunk', {
status: 200,
headers: { "x-request-id": "req" },
})
);
headers: { 'x-request-id': 'req' },
}),
)
const client = new HttpClient({ apiKey: "test" });
const client = new HttpClient({ apiKey: 'test' })
const stream = await client.requestBinaryStream({
method: "POST",
path: "/text-to-audio",
data: { user: "u", text: "hi" },
});
method: 'POST',
path: '/text-to-audio',
data: { user: 'u', text: 'hi' },
})
expect(stream.status).toBe(200);
expect(stream.requestId).toBe("req");
expect(stream.data).toBeInstanceOf(Readable);
});
expect(stream.status).toBe(200)
expect(stream.requestId).toBe('req')
expect(stream.data).toBeInstanceOf(Readable)
})
it("respects form-data headers", async () => {
const fetchMock = stubFetch();
fetchMock.mockResolvedValueOnce(jsonResponse("ok", { status: 200 }));
it('respects form-data headers', async () => {
const fetchMock = stubFetch()
fetchMock.mockResolvedValueOnce(jsonResponse('ok', { status: 200 }))
const client = new HttpClient({ apiKey: "test" });
const form = new FormData();
form.append("file", new Blob(["abc"]), "file.txt");
const client = new HttpClient({ apiKey: 'test' })
const form = new FormData()
form.append('file', new Blob(['abc']), 'file.txt')
await client.requestRaw({
method: "POST",
path: "/files/upload",
method: 'POST',
path: '/files/upload',
data: form,
});
})
const [, init] = getFetchCall(fetchMock);
const [, init] = getFetchCall(fetchMock)
expect(toHeaderRecord(init?.headers)).toMatchObject({
authorization: "Bearer test",
});
expect(toHeaderRecord(init?.headers)["content-type"]).toBeUndefined();
});
authorization: 'Bearer test',
})
expect(toHeaderRecord(init?.headers)['content-type']).toBeUndefined()
})
it("sends legacy form-data as a readable request body", async () => {
const fetchMock = stubFetch();
fetchMock.mockResolvedValueOnce(jsonResponse("ok", { status: 200 }));
it('sends legacy form-data as a readable request body', async () => {
const fetchMock = stubFetch()
fetchMock.mockResolvedValueOnce(jsonResponse('ok', { status: 200 }))
const client = new HttpClient({ apiKey: "test" });
const legacyForm = Object.assign(Readable.from(["chunk"]), {
const client = new HttpClient({ apiKey: 'test' })
const legacyForm = Object.assign(Readable.from(['chunk']), {
append: vi.fn(),
getHeaders: () => ({
"content-type": "multipart/form-data; boundary=test",
'content-type': 'multipart/form-data; boundary=test',
}),
});
})
await client.requestRaw({
method: "POST",
path: "/files/upload",
method: 'POST',
path: '/files/upload',
data: legacyForm,
});
})
const [, init] = getFetchCall(fetchMock);
const [, init] = getFetchCall(fetchMock)
expect(toHeaderRecord(init?.headers)).toMatchObject({
authorization: "Bearer test",
"content-type": "multipart/form-data; boundary=test",
});
expect(getDuplex(init)).toBe(
"half"
);
expect(init?.body).not.toBe(legacyForm);
});
authorization: 'Bearer test',
'content-type': 'multipart/form-data; boundary=test',
})
expect(getDuplex(init)).toBe('half')
expect(init?.body).not.toBe(legacyForm)
})
it("rejects legacy form-data objects that are not readable streams", async () => {
const fetchMock = stubFetch();
const client = new HttpClient({ apiKey: "test" });
it('rejects legacy form-data objects that are not readable streams', async () => {
const fetchMock = stubFetch()
const client = new HttpClient({ apiKey: 'test' })
const legacyForm = {
append: vi.fn(),
getHeaders: () => ({
"content-type": "multipart/form-data; boundary=test",
'content-type': 'multipart/form-data; boundary=test',
}),
};
}
await expect(
client.requestRaw({
method: "POST",
path: "/files/upload",
method: 'POST',
path: '/files/upload',
data: legacyForm,
})
).rejects.toBeInstanceOf(FileUploadError);
}),
).rejects.toBeInstanceOf(FileUploadError)
expect(fetchMock).not.toHaveBeenCalled();
});
expect(fetchMock).not.toHaveBeenCalled()
})
it("accepts legacy pipeable streams that are not Readable instances", async () => {
const fetchMock = stubFetch();
fetchMock.mockResolvedValueOnce(jsonResponse("ok", { status: 200 }));
const client = new HttpClient({ apiKey: "test" });
it('accepts legacy pipeable streams that are not Readable instances', async () => {
const fetchMock = stubFetch()
fetchMock.mockResolvedValueOnce(jsonResponse('ok', { status: 200 }))
const client = new HttpClient({ apiKey: 'test' })
const legacyStream = new Stream() as Stream &
NodeJS.ReadableStream & {
append: ReturnType<typeof vi.fn>;
getHeaders: () => Record<string, string>;
};
legacyStream.readable = true;
legacyStream.pause = () => legacyStream;
legacyStream.resume = () => legacyStream;
legacyStream.append = vi.fn();
append: ReturnType<typeof vi.fn>
getHeaders: () => Record<string, string>
}
legacyStream.readable = true
legacyStream.pause = () => legacyStream
legacyStream.resume = () => legacyStream
legacyStream.append = vi.fn()
legacyStream.getHeaders = () => ({
"content-type": "multipart/form-data; boundary=test",
});
'content-type': 'multipart/form-data; boundary=test',
})
queueMicrotask(() => {
legacyStream.emit("data", Buffer.from("chunk"));
legacyStream.emit("end");
});
legacyStream.emit('data', Buffer.from('chunk'))
legacyStream.emit('end')
})
await client.requestRaw({
method: "POST",
path: "/files/upload",
method: 'POST',
path: '/files/upload',
data: legacyStream as unknown as FormData,
});
})
const [, init] = getFetchCall(fetchMock);
expect(getDuplex(init)).toBe(
"half"
);
});
const [, init] = getFetchCall(fetchMock)
expect(getDuplex(init)).toBe('half')
})
it("returns buffers for byte responses", async () => {
const fetchMock = stubFetch();
it('returns buffers for byte responses', async () => {
const fetchMock = stubFetch()
fetchMock.mockResolvedValueOnce(
new Response(Uint8Array.from([1, 2, 3]), {
status: 200,
headers: { "content-type": "application/octet-stream" },
})
);
headers: { 'content-type': 'application/octet-stream' },
}),
)
const client = new HttpClient({ apiKey: "test" });
const response = await client.request<Buffer, "bytes">({
method: "GET",
path: "/files/file-1/preview",
responseType: "bytes",
});
const client = new HttpClient({ apiKey: 'test' })
const response = await client.request<Buffer, 'bytes'>({
method: 'GET',
path: '/files/file-1/preview',
responseType: 'bytes',
})
expect(Buffer.isBuffer(response.data)).toBe(true);
expect(Array.from(response.data.values())).toEqual([1, 2, 3]);
});
expect(Buffer.isBuffer(response.data)).toBe(true)
expect(Array.from(response.data.values())).toEqual([1, 2, 3])
})
it("keeps arraybuffer as a backward-compatible binary alias", async () => {
const fetchMock = stubFetch();
it('keeps arraybuffer as a backward-compatible binary alias', async () => {
const fetchMock = stubFetch()
fetchMock.mockResolvedValueOnce(
new Response(Uint8Array.from([4, 5, 6]), {
status: 200,
headers: { "content-type": "application/octet-stream" },
})
);
headers: { 'content-type': 'application/octet-stream' },
}),
)
const client = new HttpClient({ apiKey: "test" });
const response = await client.request<Buffer, "arraybuffer">({
method: "GET",
path: "/files/file-1/preview",
responseType: "arraybuffer",
});
const client = new HttpClient({ apiKey: 'test' })
const response = await client.request<Buffer, 'arraybuffer'>({
method: 'GET',
path: '/files/file-1/preview',
responseType: 'arraybuffer',
})
expect(Buffer.isBuffer(response.data)).toBe(true);
expect(Array.from(response.data.values())).toEqual([4, 5, 6]);
});
expect(Buffer.isBuffer(response.data)).toBe(true)
expect(Array.from(response.data.values())).toEqual([4, 5, 6])
})
it("returns null for empty no-content responses", async () => {
const fetchMock = stubFetch();
fetchMock.mockResolvedValueOnce(new Response(null, { status: 204 }));
it('returns null for empty no-content responses', async () => {
const fetchMock = stubFetch()
fetchMock.mockResolvedValueOnce(new Response(null, { status: 204 }))
const client = new HttpClient({ apiKey: "test" });
const client = new HttpClient({ apiKey: 'test' })
const response = await client.requestRaw({
method: "GET",
path: "/meta",
});
method: 'GET',
path: '/meta',
})
expect(response.data).toBeNull();
});
expect(response.data).toBeNull()
})
it("maps 401 and 429 errors", async () => {
const fetchMock = stubFetch();
it('maps 401 and 429 errors', async () => {
const fetchMock = stubFetch()
fetchMock
.mockResolvedValueOnce(jsonResponse({ message: 'unauthorized' }, { status: 401 }))
.mockResolvedValueOnce(
jsonResponse({ message: "unauthorized" }, { status: 401 })
jsonResponse({ message: 'rate' }, { status: 429, headers: { 'retry-after': '2' } }),
)
.mockResolvedValueOnce(
jsonResponse({ message: "rate" }, { status: 429, headers: { "retry-after": "2" } })
);
const client = new HttpClient({ apiKey: "test", maxRetries: 0 });
const client = new HttpClient({ apiKey: 'test', maxRetries: 0 })
await expect(
client.requestRaw({ method: "GET", path: "/meta" })
).rejects.toBeInstanceOf(AuthenticationError);
await expect(client.requestRaw({ method: 'GET', path: '/meta' })).rejects.toBeInstanceOf(
AuthenticationError,
)
const error = await client
.requestRaw({ method: "GET", path: "/meta" })
.catch((err: unknown) => err);
expect(error).toBeInstanceOf(RateLimitError);
expect((error as RateLimitError).retryAfter).toBe(2);
});
.requestRaw({ method: 'GET', path: '/meta' })
.catch((err: unknown) => err)
expect(error).toBeInstanceOf(RateLimitError)
expect((error as RateLimitError).retryAfter).toBe(2)
})
it("maps validation and upload errors", async () => {
const fetchMock = stubFetch();
it('maps validation and upload errors', async () => {
const fetchMock = stubFetch()
fetchMock
.mockResolvedValueOnce(jsonResponse({ message: "invalid" }, { status: 422 }))
.mockResolvedValueOnce(jsonResponse({ message: "bad upload" }, { status: 400 }));
const client = new HttpClient({ apiKey: "test", maxRetries: 0 });
.mockResolvedValueOnce(jsonResponse({ message: 'invalid' }, { status: 422 }))
.mockResolvedValueOnce(jsonResponse({ message: 'bad upload' }, { status: 400 }))
const client = new HttpClient({ apiKey: 'test', maxRetries: 0 })
await expect(
client.requestRaw({ method: "POST", path: "/chat-messages", data: { user: "u" } })
).rejects.toBeInstanceOf(ValidationError);
client.requestRaw({ method: 'POST', path: '/chat-messages', data: { user: 'u' } }),
).rejects.toBeInstanceOf(ValidationError)
await expect(
client.requestRaw({ method: "POST", path: "/files/upload", data: { user: "u" } })
).rejects.toBeInstanceOf(FileUploadError);
});
client.requestRaw({ method: 'POST', path: '/files/upload', data: { user: 'u' } }),
).rejects.toBeInstanceOf(FileUploadError)
})
it("maps timeout and network errors", async () => {
const fetchMock = stubFetch();
it('maps timeout and network errors', async () => {
const fetchMock = stubFetch()
fetchMock
.mockRejectedValueOnce(Object.assign(new Error("timeout"), { name: "AbortError" }))
.mockRejectedValueOnce(new Error("network"));
const client = new HttpClient({ apiKey: "test", maxRetries: 0 });
.mockRejectedValueOnce(Object.assign(new Error('timeout'), { name: 'AbortError' }))
.mockRejectedValueOnce(new Error('network'))
const client = new HttpClient({ apiKey: 'test', maxRetries: 0 })
await expect(
client.requestRaw({ method: "GET", path: "/meta" })
).rejects.toBeInstanceOf(TimeoutError);
await expect(client.requestRaw({ method: 'GET', path: '/meta' })).rejects.toBeInstanceOf(
TimeoutError,
)
await expect(
client.requestRaw({ method: "GET", path: "/meta" })
).rejects.toBeInstanceOf(NetworkError);
});
await expect(client.requestRaw({ method: 'GET', path: '/meta' })).rejects.toBeInstanceOf(
NetworkError,
)
})
it("maps unknown transport failures to NetworkError", async () => {
const fetchMock = stubFetch();
fetchMock.mockRejectedValueOnce("boom");
const client = new HttpClient({ apiKey: "test", maxRetries: 0 });
it('maps unknown transport failures to NetworkError', async () => {
const fetchMock = stubFetch()
fetchMock.mockRejectedValueOnce('boom')
const client = new HttpClient({ apiKey: 'test', maxRetries: 0 })
await expect(
client.requestRaw({ method: "GET", path: "/meta" })
).rejects.toMatchObject({
name: "NetworkError",
message: "Unexpected network error",
});
});
await expect(client.requestRaw({ method: 'GET', path: '/meta' })).rejects.toMatchObject({
name: 'NetworkError',
message: 'Unexpected network error',
})
})
it("retries on timeout errors", async () => {
const fetchMock = stubFetch();
it('retries on timeout errors', async () => {
const fetchMock = stubFetch()
fetchMock
.mockRejectedValueOnce(Object.assign(new Error("timeout"), { name: "AbortError" }))
.mockResolvedValueOnce(jsonResponse("ok", { status: 200 }));
const client = new HttpClient({ apiKey: "test", maxRetries: 1, retryDelay: 0 });
.mockRejectedValueOnce(Object.assign(new Error('timeout'), { name: 'AbortError' }))
.mockResolvedValueOnce(jsonResponse('ok', { status: 200 }))
const client = new HttpClient({ apiKey: 'test', maxRetries: 1, retryDelay: 0 })
await client.requestRaw({ method: "GET", path: "/meta" });
expect(fetchMock).toHaveBeenCalledTimes(2);
});
await client.requestRaw({ method: 'GET', path: '/meta' })
expect(fetchMock).toHaveBeenCalledTimes(2)
})
it("does not retry non-replayable readable request bodies", async () => {
const fetchMock = stubFetch();
fetchMock.mockRejectedValueOnce(new Error("network"));
const client = new HttpClient({ apiKey: "test", maxRetries: 2, retryDelay: 0 });
it('does not retry non-replayable readable request bodies', async () => {
const fetchMock = stubFetch()
fetchMock.mockRejectedValueOnce(new Error('network'))
const client = new HttpClient({ apiKey: 'test', maxRetries: 2, retryDelay: 0 })
await expect(
client.requestRaw({
method: "POST",
path: "/chat-messages",
data: Readable.from(["chunk"]),
})
).rejects.toBeInstanceOf(NetworkError);
method: 'POST',
path: '/chat-messages',
data: Readable.from(['chunk']),
}),
).rejects.toBeInstanceOf(NetworkError)
expect(fetchMock).toHaveBeenCalledTimes(1);
const [, init] = getFetchCall(fetchMock);
expect(getDuplex(init)).toBe(
"half"
);
});
expect(fetchMock).toHaveBeenCalledTimes(1)
const [, init] = getFetchCall(fetchMock)
expect(getDuplex(init)).toBe('half')
})
it("validates query parameters before request", async () => {
const fetchMock = stubFetch();
const client = new HttpClient({ apiKey: "test" });
it('validates query parameters before request', async () => {
const fetchMock = stubFetch()
const client = new HttpClient({ apiKey: 'test' })
await expect(
client.requestRaw({ method: "GET", path: "/meta", query: { user: 1 } })
).rejects.toBeInstanceOf(ValidationError);
expect(fetchMock).not.toHaveBeenCalled();
});
client.requestRaw({ method: 'GET', path: '/meta', query: { user: 1 } }),
).rejects.toBeInstanceOf(ValidationError)
expect(fetchMock).not.toHaveBeenCalled()
})
it("returns APIError for other http failures", async () => {
const fetchMock = stubFetch();
fetchMock.mockResolvedValueOnce(jsonResponse({ message: "server" }, { status: 500 }));
const client = new HttpClient({ apiKey: "test", maxRetries: 0 });
it('returns APIError for other http failures', async () => {
const fetchMock = stubFetch()
fetchMock.mockResolvedValueOnce(jsonResponse({ message: 'server' }, { status: 500 }))
const client = new HttpClient({ apiKey: 'test', maxRetries: 0 })
await expect(
client.requestRaw({ method: "GET", path: "/meta" })
).rejects.toBeInstanceOf(APIError);
});
await expect(client.requestRaw({ method: 'GET', path: '/meta' })).rejects.toBeInstanceOf(
APIError,
)
})
it("uses plain text bodies when json parsing is not possible", async () => {
const fetchMock = stubFetch();
it('uses plain text bodies when json parsing is not possible', async () => {
const fetchMock = stubFetch()
fetchMock.mockResolvedValueOnce(
textResponse("plain text", {
textResponse('plain text', {
status: 200,
headers: { "content-type": "text/plain" },
})
);
const client = new HttpClient({ apiKey: "test" });
headers: { 'content-type': 'text/plain' },
}),
)
const client = new HttpClient({ apiKey: 'test' })
const response = await client.requestRaw({
method: "GET",
path: "/info",
});
method: 'GET',
path: '/info',
})
expect(response.data).toBe("plain text");
});
expect(response.data).toBe('plain text')
})
it("keeps invalid json error bodies as API errors", async () => {
const fetchMock = stubFetch();
it('keeps invalid json error bodies as API errors', async () => {
const fetchMock = stubFetch()
fetchMock.mockResolvedValueOnce(
textResponse("{invalid", {
textResponse('{invalid', {
status: 500,
headers: { "content-type": "application/json", "x-request-id": "req-500" },
})
);
const client = new HttpClient({ apiKey: "test", maxRetries: 0 });
headers: { 'content-type': 'application/json', 'x-request-id': 'req-500' },
}),
)
const client = new HttpClient({ apiKey: 'test', maxRetries: 0 })
await expect(
client.requestRaw({ method: "GET", path: "/meta" })
).rejects.toMatchObject({
name: "APIError",
await expect(client.requestRaw({ method: 'GET', path: '/meta' })).rejects.toMatchObject({
name: 'APIError',
statusCode: 500,
requestId: "req-500",
responseBody: "{invalid",
});
});
requestId: 'req-500',
responseBody: '{invalid',
})
})
it("sends raw string bodies without additional json encoding", async () => {
const fetchMock = stubFetch();
fetchMock.mockResolvedValueOnce(jsonResponse("ok", { status: 200 }));
const client = new HttpClient({ apiKey: "test" });
it('sends raw string bodies without additional json encoding', async () => {
const fetchMock = stubFetch()
fetchMock.mockResolvedValueOnce(jsonResponse('ok', { status: 200 }))
const client = new HttpClient({ apiKey: 'test' })
await client.requestRaw({
method: "POST",
path: "/meta",
method: 'POST',
path: '/meta',
data: '{"pre":"serialized"}',
headers: { "Content-Type": "application/custom+json" },
});
headers: { 'Content-Type': 'application/custom+json' },
})
const [, init] = getFetchCall(fetchMock);
expect(init?.body).toBe('{"pre":"serialized"}');
const [, init] = getFetchCall(fetchMock)
expect(init?.body).toBe('{"pre":"serialized"}')
expect(toHeaderRecord(init?.headers)).toMatchObject({
"content-type": "application/custom+json",
});
});
'content-type': 'application/custom+json',
})
})
it("preserves explicit user-agent headers", async () => {
const fetchMock = stubFetch();
fetchMock.mockResolvedValueOnce(jsonResponse({ ok: true }, { status: 200 }));
const client = new HttpClient({ apiKey: "test" });
it('preserves explicit user-agent headers', async () => {
const fetchMock = stubFetch()
fetchMock.mockResolvedValueOnce(jsonResponse({ ok: true }, { status: 200 }))
const client = new HttpClient({ apiKey: 'test' })
await client.requestRaw({
method: "GET",
path: "/meta",
headers: { "User-Agent": "custom-agent" },
});
method: 'GET',
path: '/meta',
headers: { 'User-Agent': 'custom-agent' },
})
const [, init] = getFetchCall(fetchMock);
const [, init] = getFetchCall(fetchMock)
expect(toHeaderRecord(init?.headers)).toMatchObject({
"user-agent": "custom-agent",
});
});
'user-agent': 'custom-agent',
})
})
it("logs requests and responses when enableLogging is true", async () => {
const fetchMock = stubFetch();
fetchMock.mockResolvedValueOnce(jsonResponse({ ok: true }, { status: 200 }));
const consoleInfo = vi.spyOn(console, "info").mockImplementation(() => {});
it('logs requests and responses when enableLogging is true', async () => {
const fetchMock = stubFetch()
fetchMock.mockResolvedValueOnce(jsonResponse({ ok: true }, { status: 200 }))
const consoleInfo = vi.spyOn(console, 'info').mockImplementation(() => {})
const client = new HttpClient({ apiKey: "test", enableLogging: true });
await client.requestRaw({ method: "GET", path: "/meta" });
const client = new HttpClient({ apiKey: 'test', enableLogging: true })
await client.requestRaw({ method: 'GET', path: '/meta' })
expect(consoleInfo).toHaveBeenCalledWith(
expect.stringContaining("dify-client-node response 200 GET")
);
});
expect.stringContaining('dify-client-node response 200 GET'),
)
})
it("logs retry attempts when enableLogging is true", async () => {
const fetchMock = stubFetch();
it('logs retry attempts when enableLogging is true', async () => {
const fetchMock = stubFetch()
fetchMock
.mockRejectedValueOnce(Object.assign(new Error("timeout"), { name: "AbortError" }))
.mockResolvedValueOnce(jsonResponse("ok", { status: 200 }));
const consoleInfo = vi.spyOn(console, "info").mockImplementation(() => {});
.mockRejectedValueOnce(Object.assign(new Error('timeout'), { name: 'AbortError' }))
.mockResolvedValueOnce(jsonResponse('ok', { status: 200 }))
const consoleInfo = vi.spyOn(console, 'info').mockImplementation(() => {})
const client = new HttpClient({
apiKey: "test",
apiKey: 'test',
maxRetries: 1,
retryDelay: 0,
enableLogging: true,
});
})
await client.requestRaw({ method: "GET", path: "/meta" });
await client.requestRaw({ method: 'GET', path: '/meta' })
expect(consoleInfo).toHaveBeenCalledWith(
expect.stringContaining("dify-client-node retry")
);
});
});
expect(consoleInfo).toHaveBeenCalledWith(expect.stringContaining('dify-client-node retry'))
})
})
+241 -266
View File
@@ -1,10 +1,3 @@
import { Readable } from "node:stream";
import {
DEFAULT_BASE_URL,
DEFAULT_MAX_RETRIES,
DEFAULT_RETRY_DELAY_SECONDS,
DEFAULT_TIMEOUT_SECONDS,
} from "../types/common";
import type {
BinaryStream,
DifyClientConfig,
@@ -14,7 +7,10 @@ import type {
JsonValue,
QueryParams,
RequestMethod,
} from "../types/common";
} from '../types/common'
import type { SdkFormData } from './form-data'
import { Readable } from 'node:stream'
import { validateParams } from '../client/validation'
import {
APIError,
AuthenticationError,
@@ -24,17 +20,21 @@ import {
RateLimitError,
TimeoutError,
ValidationError,
} from "../errors/dify-error";
import type { SdkFormData } from "./form-data";
import { getFormDataHeaders, isFormData } from "./form-data";
import { createBinaryStream, createSseStream } from "./sse";
import { getRetryDelayMs, shouldRetry, sleep } from "./retry";
import { validateParams } from "../client/validation";
import { hasStringProperty, isRecord } from "../internal/type-guards";
} from '../errors/dify-error'
import { hasStringProperty, isRecord } from '../internal/type-guards'
import {
DEFAULT_BASE_URL,
DEFAULT_MAX_RETRIES,
DEFAULT_RETRY_DELAY_SECONDS,
DEFAULT_TIMEOUT_SECONDS,
} from '../types/common'
import { getFormDataHeaders, isFormData } from './form-data'
import { getRetryDelayMs, shouldRetry, sleep } from './retry'
import { createBinaryStream, createSseStream } from './sse'
const DEFAULT_USER_AGENT = "dify-client-node";
const DEFAULT_USER_AGENT = 'dify-client-node'
export type HttpResponseType = "json" | "bytes" | "stream" | "arraybuffer";
export type HttpResponseType = 'json' | 'bytes' | 'stream' | 'arraybuffer'
export type HttpRequestBody =
| JsonValue
@@ -45,54 +45,51 @@ export type HttpRequestBody =
| ArrayBufferView
| Blob
| string
| null;
| null
export type ResponseDataFor<TResponseType extends HttpResponseType> =
TResponseType extends "stream"
? Readable
: TResponseType extends "bytes" | "arraybuffer"
? Buffer
: JsonValue | string | null;
export type ResponseDataFor<TResponseType extends HttpResponseType> = TResponseType extends 'stream'
? Readable
: TResponseType extends 'bytes' | 'arraybuffer'
? Buffer
: JsonValue | string | null
export type RawHttpResponse<TData = unknown> = {
data: TData;
status: number;
headers: Headers;
requestId?: string;
url: string;
};
data: TData
status: number
headers: Headers
requestId?: string
url: string
}
export type RequestOptions<TResponseType extends HttpResponseType = "json"> = {
method: RequestMethod;
path: string;
query?: QueryParams;
data?: HttpRequestBody;
headers?: Headers;
responseType?: TResponseType;
};
export type RequestOptions<TResponseType extends HttpResponseType = 'json'> = {
method: RequestMethod
path: string
query?: QueryParams
data?: HttpRequestBody
headers?: Headers
responseType?: TResponseType
}
export type HttpClientSettings = Required<
Omit<DifyClientConfig, "apiKey">
> & {
apiKey: string;
};
export type HttpClientSettings = Required<Omit<DifyClientConfig, 'apiKey'>> & {
apiKey: string
}
type FetchRequestInit = RequestInit & {
duplex?: "half";
};
duplex?: 'half'
}
type PreparedRequestBody = {
body?: BodyInit | null;
headers: Headers;
duplex?: "half";
replayable: boolean;
};
body?: BodyInit | null
headers: Headers
duplex?: 'half'
replayable: boolean
}
type TimeoutContext = {
cleanup: () => void;
reason: Error;
signal: AbortSignal;
};
cleanup: () => void
reason: Error
signal: AbortSignal
}
const normalizeSettings = (config: DifyClientConfig): HttpClientSettings => ({
apiKey: config.apiKey,
@@ -101,304 +98,292 @@ const normalizeSettings = (config: DifyClientConfig): HttpClientSettings => ({
maxRetries: config.maxRetries ?? DEFAULT_MAX_RETRIES,
retryDelay: config.retryDelay ?? DEFAULT_RETRY_DELAY_SECONDS,
enableLogging: config.enableLogging ?? false,
});
})
const normalizeHeaders = (headers: globalThis.Headers): Headers => {
const result: Headers = {};
const result: Headers = {}
headers.forEach((value, key) => {
result[key.toLowerCase()] = value;
});
return result;
};
result[key.toLowerCase()] = value
})
return result
}
const resolveRequestId = (headers: Headers): string | undefined =>
headers["x-request-id"] ?? headers["x-requestid"];
headers['x-request-id'] ?? headers['x-requestid']
const buildRequestUrl = (
baseUrl: string,
path: string,
query?: QueryParams
): string => {
const trimmed = baseUrl.replace(/\/+$/, "");
const url = new URL(`${trimmed}${path}`);
const queryString = buildQueryString(query);
const buildRequestUrl = (baseUrl: string, path: string, query?: QueryParams): string => {
const trimmed = baseUrl.replace(/\/+$/, '')
const url = new URL(`${trimmed}${path}`)
const queryString = buildQueryString(query)
if (queryString) {
url.search = queryString;
url.search = queryString
}
return url.toString();
};
return url.toString()
}
const buildQueryString = (params?: QueryParams): string => {
if (!params) {
return "";
return ''
}
const searchParams = new URLSearchParams();
const searchParams = new URLSearchParams()
Object.entries(params).forEach(([key, value]) => {
if (value === undefined || value === null) {
return;
return
}
if (Array.isArray(value)) {
value.forEach((item) => {
searchParams.append(key, String(item));
});
return;
searchParams.append(key, String(item))
})
return
}
searchParams.append(key, String(value));
});
return searchParams.toString();
};
searchParams.append(key, String(value))
})
return searchParams.toString()
}
const parseRetryAfterSeconds = (headerValue?: string): number | undefined => {
if (!headerValue) {
return undefined;
return undefined
}
const asNumber = Number.parseInt(headerValue, 10);
const asNumber = Number.parseInt(headerValue, 10)
if (!Number.isNaN(asNumber)) {
return asNumber;
return asNumber
}
const asDate = Date.parse(headerValue);
const asDate = Date.parse(headerValue)
if (!Number.isNaN(asDate)) {
const diff = asDate - Date.now();
return diff > 0 ? Math.ceil(diff / 1000) : 0;
const diff = asDate - Date.now()
return diff > 0 ? Math.ceil(diff / 1000) : 0
}
return undefined;
};
return undefined
}
const isPipeableStream = (value: unknown): value is { pipe: (destination: unknown) => unknown } => {
if (!value || typeof value !== "object") {
return false;
if (!value || typeof value !== 'object') {
return false
}
return typeof (value as { pipe?: unknown }).pipe === "function";
};
return typeof (value as { pipe?: unknown }).pipe === 'function'
}
const toNodeReadable = (value: unknown): Readable | null => {
if (value instanceof Readable) {
return value;
return value
}
if (!isPipeableStream(value)) {
return null;
return null
}
const readable = new Readable({
read() {},
});
return readable.wrap(value as NodeJS.ReadableStream);
};
})
return readable.wrap(value as NodeJS.ReadableStream)
}
const isBinaryBody = (
value: unknown
): value is ArrayBuffer | ArrayBufferView | Blob => {
const isBinaryBody = (value: unknown): value is ArrayBuffer | ArrayBufferView | Blob => {
if (value instanceof Blob) {
return true;
return true
}
if (value instanceof ArrayBuffer) {
return true;
return true
}
return ArrayBuffer.isView(value);
};
return ArrayBuffer.isView(value)
}
const isJsonBody = (value: unknown): value is Exclude<JsonValue, string> =>
value === null ||
typeof value === "boolean" ||
typeof value === "number" ||
typeof value === 'boolean' ||
typeof value === 'number' ||
Array.isArray(value) ||
isRecord(value);
isRecord(value)
const isUploadLikeRequest = (path: string): boolean => {
const normalizedPath = path.toLowerCase();
const normalizedPath = path.toLowerCase()
return (
normalizedPath.includes("upload") ||
normalizedPath.includes("/files/") ||
normalizedPath.includes("audio-to-text") ||
normalizedPath.includes("create_by_file") ||
normalizedPath.includes("update_by_file")
);
};
normalizedPath.includes('upload') ||
normalizedPath.includes('/files/') ||
normalizedPath.includes('audio-to-text') ||
normalizedPath.includes('create_by_file') ||
normalizedPath.includes('update_by_file')
)
}
const resolveErrorMessage = (status: number, responseBody: unknown): string => {
if (typeof responseBody === "string" && responseBody.trim().length > 0) {
return responseBody;
if (typeof responseBody === 'string' && responseBody.trim().length > 0) {
return responseBody
}
if (hasStringProperty(responseBody, "message")) {
const message = responseBody.message.trim();
if (hasStringProperty(responseBody, 'message')) {
const message = responseBody.message.trim()
if (message.length > 0) {
return message;
return message
}
}
return `Request failed with status code ${status}`;
};
return `Request failed with status code ${status}`
}
const parseJsonLikeText = (
value: string,
contentType?: string | null
contentType?: string | null,
): JsonValue | string | null => {
if (value.length === 0) {
return null;
return null
}
const shouldParseJson =
contentType?.includes("application/json") === true ||
contentType?.includes("+json") === true;
contentType?.includes('application/json') === true || contentType?.includes('+json') === true
if (!shouldParseJson) {
try {
return JSON.parse(value) as JsonValue;
return JSON.parse(value) as JsonValue
} catch {
return value;
return value
}
}
return JSON.parse(value) as JsonValue;
};
return JSON.parse(value) as JsonValue
}
const prepareRequestBody = (
method: RequestMethod,
data: HttpRequestBody | undefined
data: HttpRequestBody | undefined,
): PreparedRequestBody => {
if (method === "GET" || data === undefined) {
if (method === 'GET' || data === undefined) {
return {
body: undefined,
headers: {},
replayable: true,
};
}
}
if (isFormData(data)) {
if ("getHeaders" in data && typeof data.getHeaders === "function") {
const readable = toNodeReadable(data);
if ('getHeaders' in data && typeof data.getHeaders === 'function') {
const readable = toNodeReadable(data)
if (!readable) {
throw new FileUploadError(
"Legacy FormData must be a readable stream when used with fetch"
);
throw new FileUploadError('Legacy FormData must be a readable stream when used with fetch')
}
return {
body: Readable.toWeb(readable) as BodyInit,
headers: getFormDataHeaders(data),
duplex: "half",
duplex: 'half',
replayable: false,
};
}
}
return {
body: data as BodyInit,
headers: getFormDataHeaders(data),
replayable: true,
};
}
}
if (typeof data === "string") {
if (typeof data === 'string') {
return {
body: data,
headers: {},
replayable: true,
};
}
}
const readable = toNodeReadable(data);
const readable = toNodeReadable(data)
if (readable) {
return {
body: Readable.toWeb(readable) as BodyInit,
headers: {},
duplex: "half",
duplex: 'half',
replayable: false,
};
}
}
if (data instanceof URLSearchParams || isBinaryBody(data)) {
const body =
ArrayBuffer.isView(data) && !(data instanceof Uint8Array)
? new Uint8Array(data.buffer, data.byteOffset, data.byteLength)
: data;
: data
return {
body: body as BodyInit,
headers: {},
replayable: true,
};
}
}
if (isJsonBody(data)) {
return {
body: JSON.stringify(data),
headers: {
"Content-Type": "application/json",
'Content-Type': 'application/json',
},
replayable: true,
};
}
}
throw new ValidationError("Unsupported request body type");
};
throw new ValidationError('Unsupported request body type')
}
const createTimeoutContext = (timeoutMs: number): TimeoutContext => {
const controller = new AbortController();
const reason = new Error("Request timed out");
const controller = new AbortController()
const reason = new Error('Request timed out')
const timer = setTimeout(() => {
controller.abort(reason);
}, timeoutMs);
controller.abort(reason)
}, timeoutMs)
return {
signal: controller.signal,
reason,
cleanup: () => {
clearTimeout(timer);
clearTimeout(timer)
},
};
};
}
}
const parseResponseBody = async <TResponseType extends HttpResponseType>(
response: Response,
responseType: TResponseType
responseType: TResponseType,
): Promise<ResponseDataFor<TResponseType>> => {
if (responseType === "stream") {
if (responseType === 'stream') {
if (!response.body) {
throw new NetworkError("Response body is empty");
throw new NetworkError('Response body is empty')
}
return Readable.fromWeb(
response.body as unknown as Parameters<typeof Readable.fromWeb>[0]
) as ResponseDataFor<TResponseType>;
response.body as unknown as Parameters<typeof Readable.fromWeb>[0],
) as ResponseDataFor<TResponseType>
}
if (responseType === "bytes" || responseType === "arraybuffer") {
const bytes = Buffer.from(await response.arrayBuffer());
return bytes as ResponseDataFor<TResponseType>;
if (responseType === 'bytes' || responseType === 'arraybuffer') {
const bytes = Buffer.from(await response.arrayBuffer())
return bytes as ResponseDataFor<TResponseType>
}
if (response.status === 204 || response.status === 205 || response.status === 304) {
return null as ResponseDataFor<TResponseType>;
return null as ResponseDataFor<TResponseType>
}
const text = await response.text();
const text = await response.text()
try {
return parseJsonLikeText(
text,
response.headers.get("content-type")
) as ResponseDataFor<TResponseType>;
response.headers.get('content-type'),
) as ResponseDataFor<TResponseType>
} catch (error) {
if (!response.ok && error instanceof SyntaxError) {
return text as ResponseDataFor<TResponseType>;
return text as ResponseDataFor<TResponseType>
}
throw error;
throw error
}
};
}
const mapHttpError = (
response: RawHttpResponse,
path: string
): DifyError => {
const status = response.status;
const responseBody = response.data;
const message = resolveErrorMessage(status, responseBody);
const mapHttpError = (response: RawHttpResponse, path: string): DifyError => {
const status = response.status
const responseBody = response.data
const message = resolveErrorMessage(status, responseBody)
if (status === 401) {
return new AuthenticationError(message, {
statusCode: status,
responseBody,
requestId: response.requestId,
});
})
}
if (status === 429) {
const retryAfter = parseRetryAfterSeconds(response.headers["retry-after"]);
const retryAfter = parseRetryAfterSeconds(response.headers['retry-after'])
return new RateLimitError(message, {
statusCode: status,
responseBody,
requestId: response.requestId,
retryAfter,
});
})
}
if (status === 422) {
@@ -406,7 +391,7 @@ const mapHttpError = (
statusCode: status,
responseBody,
requestId: response.requestId,
});
})
}
if (status === 400 && isUploadLikeRequest(path)) {
@@ -414,187 +399,177 @@ const mapHttpError = (
statusCode: status,
responseBody,
requestId: response.requestId,
});
})
}
return new APIError(message, {
statusCode: status,
responseBody,
requestId: response.requestId,
});
};
})
}
const mapTransportError = (
error: unknown,
timeoutContext: TimeoutContext
): DifyError => {
const mapTransportError = (error: unknown, timeoutContext: TimeoutContext): DifyError => {
if (error instanceof DifyError) {
return error;
return error
}
if (
timeoutContext.signal.aborted &&
timeoutContext.signal.reason === timeoutContext.reason
) {
return new TimeoutError("Request timed out", { cause: error });
if (timeoutContext.signal.aborted && timeoutContext.signal.reason === timeoutContext.reason) {
return new TimeoutError('Request timed out', { cause: error })
}
if (error instanceof Error) {
if (error.name === "AbortError" || error.name === "TimeoutError") {
return new TimeoutError("Request timed out", { cause: error });
if (error.name === 'AbortError' || error.name === 'TimeoutError') {
return new TimeoutError('Request timed out', { cause: error })
}
return new NetworkError(error.message, { cause: error });
return new NetworkError(error.message, { cause: error })
}
return new NetworkError("Unexpected network error", { cause: error });
};
return new NetworkError('Unexpected network error', { cause: error })
}
export class HttpClient {
private settings: HttpClientSettings;
private settings: HttpClientSettings
constructor(config: DifyClientConfig) {
this.settings = normalizeSettings(config);
this.settings = normalizeSettings(config)
}
updateApiKey(apiKey: string): void {
this.settings.apiKey = apiKey;
this.settings.apiKey = apiKey
}
getSettings(): HttpClientSettings {
return { ...this.settings };
return { ...this.settings }
}
async request<
T,
TResponseType extends HttpResponseType = "json",
>(options: RequestOptions<TResponseType>): Promise<DifyResponse<T>> {
const response = await this.requestRaw(options);
async request<T, TResponseType extends HttpResponseType = 'json'>(
options: RequestOptions<TResponseType>,
): Promise<DifyResponse<T>> {
const response = await this.requestRaw(options)
return {
data: response.data as T,
status: response.status,
headers: response.headers,
requestId: response.requestId,
};
}
}
async requestStream<T>(options: RequestOptions): Promise<DifyStream<T>> {
const response = await this.requestRaw({
...options,
responseType: "stream",
});
responseType: 'stream',
})
return createSseStream<T>(response.data, {
status: response.status,
headers: response.headers,
requestId: response.requestId,
});
})
}
async requestBinaryStream(options: RequestOptions): Promise<BinaryStream> {
const response = await this.requestRaw({
...options,
responseType: "stream",
});
responseType: 'stream',
})
return createBinaryStream(response.data, {
status: response.status,
headers: response.headers,
requestId: response.requestId,
});
})
}
async requestRaw<TResponseType extends HttpResponseType = "json">(
options: RequestOptions<TResponseType>
async requestRaw<TResponseType extends HttpResponseType = 'json'>(
options: RequestOptions<TResponseType>,
): Promise<RawHttpResponse<ResponseDataFor<TResponseType>>> {
const responseType = options.responseType ?? "json";
const { method, path, query, data, headers } = options;
const { apiKey, enableLogging, maxRetries, retryDelay, timeout } = this.settings;
const responseType = options.responseType ?? 'json'
const { method, path, query, data, headers } = options
const { apiKey, enableLogging, maxRetries, retryDelay, timeout } = this.settings
if (query) {
validateParams(query);
validateParams(query)
}
if (isRecord(data) && !Array.isArray(data) && !isFormData(data) && !isPipeableStream(data)) {
validateParams(data);
validateParams(data)
}
const url = buildRequestUrl(this.settings.baseUrl, path, query);
const url = buildRequestUrl(this.settings.baseUrl, path, query)
if (enableLogging) {
console.info(`dify-client-node request ${method} ${url}`);
console.info(`dify-client-node request ${method} ${url}`)
}
let attempt = 0;
let attempt = 0
while (true) {
const preparedBody = prepareRequestBody(method, data);
const preparedBody = prepareRequestBody(method, data)
const requestHeaders: Headers = {
Authorization: `Bearer ${apiKey}`,
...preparedBody.headers,
...headers,
};
if (
typeof process !== "undefined" &&
!!process.versions?.node &&
!requestHeaders["User-Agent"] &&
!requestHeaders["user-agent"]
) {
requestHeaders["User-Agent"] = DEFAULT_USER_AGENT;
}
const timeoutContext = createTimeoutContext(timeout * 1000);
if (
typeof process !== 'undefined' &&
!!process.versions?.node &&
!requestHeaders['User-Agent'] &&
!requestHeaders['user-agent']
) {
requestHeaders['User-Agent'] = DEFAULT_USER_AGENT
}
const timeoutContext = createTimeoutContext(timeout * 1000)
const requestInit: FetchRequestInit = {
method,
headers: requestHeaders,
body: preparedBody.body,
signal: timeoutContext.signal,
};
}
if (preparedBody.duplex) {
requestInit.duplex = preparedBody.duplex;
requestInit.duplex = preparedBody.duplex
}
try {
const fetchResponse = await fetch(url, requestInit);
const responseHeaders = normalizeHeaders(fetchResponse.headers);
const parsedBody =
(await parseResponseBody(fetchResponse, responseType)) as ResponseDataFor<TResponseType>;
const fetchResponse = await fetch(url, requestInit)
const responseHeaders = normalizeHeaders(fetchResponse.headers)
const parsedBody = (await parseResponseBody(
fetchResponse,
responseType,
)) as ResponseDataFor<TResponseType>
const response: RawHttpResponse<ResponseDataFor<TResponseType>> = {
data: parsedBody,
status: fetchResponse.status,
headers: responseHeaders,
requestId: resolveRequestId(responseHeaders),
url,
};
}
if (!fetchResponse.ok) {
throw mapHttpError(response, path);
throw mapHttpError(response, path)
}
if (enableLogging) {
console.info(
`dify-client-node response ${response.status} ${method} ${url}`
);
console.info(`dify-client-node response ${response.status} ${method} ${url}`)
}
return response;
return response
} catch (error) {
const mapped = mapTransportError(error, timeoutContext);
const mapped = mapTransportError(error, timeoutContext)
const shouldRetryRequest =
preparedBody.replayable && shouldRetry(mapped, attempt, maxRetries);
preparedBody.replayable && shouldRetry(mapped, attempt, maxRetries)
if (!shouldRetryRequest) {
throw mapped;
throw mapped
}
const retryAfterSeconds =
mapped instanceof RateLimitError ? mapped.retryAfter : undefined;
const delay = getRetryDelayMs(attempt + 1, retryDelay, retryAfterSeconds);
const retryAfterSeconds = mapped instanceof RateLimitError ? mapped.retryAfter : undefined
const delay = getRetryDelayMs(attempt + 1, retryDelay, retryAfterSeconds)
if (enableLogging) {
console.info(
`dify-client-node retry ${attempt + 1} in ${delay}ms for ${method} ${url}`
);
console.info(`dify-client-node retry ${attempt + 1} in ${delay}ms for ${method} ${url}`)
}
attempt += 1;
await sleep(delay);
attempt += 1
await sleep(delay)
} finally {
timeoutContext.cleanup();
timeoutContext.cleanup()
}
}
}
+21 -21
View File
@@ -1,29 +1,29 @@
import { describe, expect, it, vi } from "vitest";
import { getFormDataHeaders, isFormData } from "./form-data";
import { describe, expect, it, vi } from 'vitest'
import { getFormDataHeaders, isFormData } from './form-data'
describe("form-data helpers", () => {
it("detects form-data like objects", () => {
describe('form-data helpers', () => {
it('detects form-data like objects', () => {
const formLike = {
append: () => {},
getHeaders: () => ({ "content-type": "multipart/form-data" }),
};
expect(isFormData(formLike)).toBe(true);
expect(isFormData({})).toBe(false);
});
getHeaders: () => ({ 'content-type': 'multipart/form-data' }),
}
expect(isFormData(formLike)).toBe(true)
expect(isFormData({})).toBe(false)
})
it("detects native FormData", () => {
const form = new FormData();
form.append("field", "value");
expect(isFormData(form)).toBe(true);
});
it('detects native FormData', () => {
const form = new FormData()
form.append('field', 'value')
expect(isFormData(form)).toBe(true)
})
it("returns headers from form-data", () => {
it('returns headers from form-data', () => {
const formLike = {
append: vi.fn(),
getHeaders: () => ({ "content-type": "multipart/form-data" }),
};
getHeaders: () => ({ 'content-type': 'multipart/form-data' }),
}
expect(getFormDataHeaders(formLike)).toEqual({
"content-type": "multipart/form-data",
});
});
});
'content-type': 'multipart/form-data',
})
})
})
+23 -23
View File
@@ -1,37 +1,37 @@
import type { Headers } from "../types/common";
import type { Headers } from '../types/common'
type FormDataAppendValue = Blob | string;
type FormDataAppendValue = Blob | string
export type WebFormData = FormData;
export type WebFormData = FormData
export type LegacyNodeFormData = {
append: (name: string, value: FormDataAppendValue, fileName?: string) => void;
getHeaders: () => Headers;
constructor?: { name?: string };
};
append: (name: string, value: FormDataAppendValue, fileName?: string) => void
getHeaders: () => Headers
constructor?: { name?: string }
}
export type SdkFormData = WebFormData | LegacyNodeFormData;
export type SdkFormData = WebFormData | LegacyNodeFormData
export const isFormData = (value: unknown): value is SdkFormData => {
if (!value || typeof value !== "object") {
return false;
if (!value || typeof value !== 'object') {
return false
}
if (typeof FormData !== "undefined" && value instanceof FormData) {
return true;
if (typeof FormData !== 'undefined' && value instanceof FormData) {
return true
}
const candidate = value as Partial<LegacyNodeFormData>;
if (typeof candidate.append !== "function") {
return false;
const candidate = value as Partial<LegacyNodeFormData>
if (typeof candidate.append !== 'function') {
return false
}
if (typeof candidate.getHeaders === "function") {
return true;
if (typeof candidate.getHeaders === 'function') {
return true
}
return candidate.constructor?.name === "FormData";
};
return candidate.constructor?.name === 'FormData'
}
export const getFormDataHeaders = (form: SdkFormData): Headers => {
if ("getHeaders" in form && typeof form.getHeaders === "function") {
return form.getHeaders();
if ('getHeaders' in form && typeof form.getHeaders === 'function') {
return form.getHeaders()
}
return {};
};
return {}
}
+28 -28
View File
@@ -1,38 +1,38 @@
import { describe, expect, it } from "vitest";
import { getRetryDelayMs, shouldRetry } from "./retry";
import { NetworkError, RateLimitError, TimeoutError } from "../errors/dify-error";
import { describe, expect, it } from 'vitest'
import { NetworkError, RateLimitError, TimeoutError } from '../errors/dify-error'
import { getRetryDelayMs, shouldRetry } from './retry'
const withMockedRandom = (value: number, fn: () => void): void => {
const original = Math.random;
Math.random = () => value;
const original = Math.random
Math.random = () => value
try {
fn();
fn()
} finally {
Math.random = original;
Math.random = original
}
};
}
describe("retry helpers", () => {
it("getRetryDelayMs honors retry-after header", () => {
expect(getRetryDelayMs(1, 1, 3)).toBe(3000);
});
describe('retry helpers', () => {
it('getRetryDelayMs honors retry-after header', () => {
expect(getRetryDelayMs(1, 1, 3)).toBe(3000)
})
it("getRetryDelayMs uses exponential backoff with jitter", () => {
it('getRetryDelayMs uses exponential backoff with jitter', () => {
withMockedRandom(0, () => {
expect(getRetryDelayMs(1, 1)).toBe(1000);
expect(getRetryDelayMs(2, 1)).toBe(2000);
expect(getRetryDelayMs(3, 1)).toBe(4000);
});
});
expect(getRetryDelayMs(1, 1)).toBe(1000)
expect(getRetryDelayMs(2, 1)).toBe(2000)
expect(getRetryDelayMs(3, 1)).toBe(4000)
})
})
it("shouldRetry respects max retries", () => {
expect(shouldRetry(new TimeoutError("timeout"), 3, 3)).toBe(false);
});
it('shouldRetry respects max retries', () => {
expect(shouldRetry(new TimeoutError('timeout'), 3, 3)).toBe(false)
})
it("shouldRetry retries on network, timeout, and rate limit", () => {
expect(shouldRetry(new TimeoutError("timeout"), 0, 3)).toBe(true);
expect(shouldRetry(new NetworkError("network"), 0, 3)).toBe(true);
expect(shouldRetry(new RateLimitError("limit"), 0, 3)).toBe(true);
expect(shouldRetry(new Error("other"), 0, 3)).toBe(false);
});
});
it('shouldRetry retries on network, timeout, and rate limit', () => {
expect(shouldRetry(new TimeoutError('timeout'), 0, 3)).toBe(true)
expect(shouldRetry(new NetworkError('network'), 0, 3)).toBe(true)
expect(shouldRetry(new RateLimitError('limit'), 0, 3)).toBe(true)
expect(shouldRetry(new Error('other'), 0, 3)).toBe(false)
})
})
+17 -21
View File
@@ -1,40 +1,36 @@
import { RateLimitError, NetworkError, TimeoutError } from "../errors/dify-error";
import { RateLimitError, NetworkError, TimeoutError } from '../errors/dify-error'
export const sleep = (ms: number): Promise<void> =>
new Promise((resolve) => {
setTimeout(resolve, ms);
});
setTimeout(resolve, ms)
})
export const getRetryDelayMs = (
attempt: number,
retryDelaySeconds: number,
retryAfterSeconds?: number
retryAfterSeconds?: number,
): number => {
if (retryAfterSeconds && retryAfterSeconds > 0) {
return retryAfterSeconds * 1000;
return retryAfterSeconds * 1000
}
const base = retryDelaySeconds * 1000;
const exponential = base * Math.pow(2, Math.max(0, attempt - 1));
const jitter = Math.random() * base;
return exponential + jitter;
};
const base = retryDelaySeconds * 1000
const exponential = base * Math.pow(2, Math.max(0, attempt - 1))
const jitter = Math.random() * base
return exponential + jitter
}
export const shouldRetry = (
error: unknown,
attempt: number,
maxRetries: number
): boolean => {
export const shouldRetry = (error: unknown, attempt: number, maxRetries: number): boolean => {
if (attempt >= maxRetries) {
return false;
return false
}
if (error instanceof TimeoutError) {
return true;
return true
}
if (error instanceof NetworkError) {
return true;
return true
}
if (error instanceof RateLimitError) {
return true;
return true
}
return false;
};
return false
}
+63 -70
View File
@@ -1,95 +1,88 @@
import { Readable } from "node:stream";
import { describe, expect, it } from "vitest";
import { createBinaryStream, createSseStream, parseSseStream } from "./sse";
import { Readable } from 'node:stream'
import { describe, expect, it } from 'vitest'
import { createBinaryStream, createSseStream, parseSseStream } from './sse'
describe("sse parsing", () => {
it("parses event and data lines", async () => {
const stream = Readable.from([
"event: message\n",
'data: {"answer":"hi"}\n',
"\n",
]);
const events: Array<{ event?: string; data: unknown; raw: string }> = [];
describe('sse parsing', () => {
it('parses event and data lines', async () => {
const stream = Readable.from(['event: message\n', 'data: {"answer":"hi"}\n', '\n'])
const events: Array<{ event?: string; data: unknown; raw: string }> = []
for await (const event of parseSseStream(stream)) {
events.push(event);
events.push(event)
}
expect(events).toHaveLength(1);
expect(events[0]!.event).toBe("message");
expect(events[0]!.data).toEqual({ answer: "hi" });
});
expect(events).toHaveLength(1)
expect(events[0]!.event).toBe('message')
expect(events[0]!.data).toEqual({ answer: 'hi' })
})
it("handles multi-line data payloads", async () => {
const stream = Readable.from(["data: line1\n", "data: line2\n", "\n"]);
const events: Array<{ event?: string; data: unknown; raw: string }> = [];
it('handles multi-line data payloads', async () => {
const stream = Readable.from(['data: line1\n', 'data: line2\n', '\n'])
const events: Array<{ event?: string; data: unknown; raw: string }> = []
for await (const event of parseSseStream(stream)) {
events.push(event);
events.push(event)
}
expect(events[0]!.raw).toBe("line1\nline2");
expect(events[0]!.data).toBe("line1\nline2");
});
expect(events[0]!.raw).toBe('line1\nline2')
expect(events[0]!.data).toBe('line1\nline2')
})
it("ignores comments and flushes the last event without a trailing separator", async () => {
it('ignores comments and flushes the last event without a trailing separator', async () => {
const stream = Readable.from([
Buffer.from(": keep-alive\n"),
Buffer.from(': keep-alive\n'),
Uint8Array.from(Buffer.from('event: message\ndata: {"delta":"hi"}\n')),
]);
const events: Array<{ event?: string; data: unknown; raw: string }> = [];
])
const events: Array<{ event?: string; data: unknown; raw: string }> = []
for await (const event of parseSseStream(stream)) {
events.push(event);
events.push(event)
}
expect(events).toEqual([
{
event: "message",
data: { delta: "hi" },
event: 'message',
data: { delta: 'hi' },
raw: '{"delta":"hi"}',
},
]);
});
])
})
it("createSseStream exposes toText", async () => {
const stream = Readable.from([
'data: {"answer":"hello"}\n\n',
'data: {"delta":" world"}\n\n',
]);
it('createSseStream exposes toText', async () => {
const stream = Readable.from(['data: {"answer":"hello"}\n\n', 'data: {"delta":" world"}\n\n'])
const sseStream = createSseStream(stream, {
status: 200,
headers: {},
requestId: "req",
});
const text = await sseStream.toText();
expect(text).toBe("hello world");
});
requestId: 'req',
})
const text = await sseStream.toText()
expect(text).toBe('hello world')
})
it("toText extracts text from string data", async () => {
const stream = Readable.from(["data: plain text\n\n"]);
const sseStream = createSseStream(stream, { status: 200, headers: {} });
const text = await sseStream.toText();
expect(text).toBe("plain text");
});
it('toText extracts text from string data', async () => {
const stream = Readable.from(['data: plain text\n\n'])
const sseStream = createSseStream(stream, { status: 200, headers: {} })
const text = await sseStream.toText()
expect(text).toBe('plain text')
})
it("toText extracts text field from object", async () => {
const stream = Readable.from(['data: {"text":"hello"}\n\n']);
const sseStream = createSseStream(stream, { status: 200, headers: {} });
const text = await sseStream.toText();
expect(text).toBe("hello");
});
it('toText extracts text field from object', async () => {
const stream = Readable.from(['data: {"text":"hello"}\n\n'])
const sseStream = createSseStream(stream, { status: 200, headers: {} })
const text = await sseStream.toText()
expect(text).toBe('hello')
})
it("toText returns empty for invalid data", async () => {
const stream = Readable.from(["data: null\n\n", "data: 123\n\n"]);
const sseStream = createSseStream(stream, { status: 200, headers: {} });
const text = await sseStream.toText();
expect(text).toBe("");
});
it('toText returns empty for invalid data', async () => {
const stream = Readable.from(['data: null\n\n', 'data: 123\n\n'])
const sseStream = createSseStream(stream, { status: 200, headers: {} })
const text = await sseStream.toText()
expect(text).toBe('')
})
it("createBinaryStream exposes metadata", () => {
const stream = Readable.from(["chunk"]);
it('createBinaryStream exposes metadata', () => {
const stream = Readable.from(['chunk'])
const binary = createBinaryStream(stream, {
status: 200,
headers: { "content-type": "audio/mpeg" },
requestId: "req",
});
expect(binary.status).toBe(200);
expect(binary.headers["content-type"]).toBe("audio/mpeg");
expect(binary.toReadable()).toBe(stream);
});
});
headers: { 'content-type': 'audio/mpeg' },
requestId: 'req',
})
expect(binary.status).toBe(200)
expect(binary.headers['content-type']).toBe('audio/mpeg')
expect(binary.toReadable()).toBe(stream)
})
})
+68 -76
View File
@@ -1,123 +1,115 @@
import type { Readable } from "node:stream";
import { StringDecoder } from "node:string_decoder";
import type {
BinaryStream,
DifyStream,
Headers,
JsonValue,
StreamEvent,
} from "../types/common";
import { isRecord } from "../internal/type-guards";
import type { Readable } from 'node:stream'
import type { BinaryStream, DifyStream, Headers, JsonValue, StreamEvent } from '../types/common'
import { StringDecoder } from 'node:string_decoder'
import { isRecord } from '../internal/type-guards'
const toBufferChunk = (chunk: unknown): Buffer => {
if (Buffer.isBuffer(chunk)) {
return chunk;
return chunk
}
if (chunk instanceof Uint8Array) {
return Buffer.from(chunk);
return Buffer.from(chunk)
}
return Buffer.from(String(chunk));
};
return Buffer.from(String(chunk))
}
const readLines = async function* (stream: Readable): AsyncIterable<string> {
const decoder = new StringDecoder("utf8");
let buffered = "";
const decoder = new StringDecoder('utf8')
let buffered = ''
for await (const chunk of stream) {
buffered += decoder.write(toBufferChunk(chunk));
let index = buffered.indexOf("\n");
buffered += decoder.write(toBufferChunk(chunk))
let index = buffered.indexOf('\n')
while (index >= 0) {
let line = buffered.slice(0, index);
buffered = buffered.slice(index + 1);
if (line.endsWith("\r")) {
line = line.slice(0, -1);
let line = buffered.slice(0, index)
buffered = buffered.slice(index + 1)
if (line.endsWith('\r')) {
line = line.slice(0, -1)
}
yield line;
index = buffered.indexOf("\n");
yield line
index = buffered.indexOf('\n')
}
}
buffered += decoder.end();
buffered += decoder.end()
if (buffered) {
yield buffered;
yield buffered
}
};
}
const parseMaybeJson = (value: string): JsonValue | string | null => {
if (!value) {
return null;
return null
}
try {
return JSON.parse(value) as JsonValue;
return JSON.parse(value) as JsonValue
} catch {
return value;
return value
}
};
}
export const parseSseStream = async function* <T>(
stream: Readable
): AsyncIterable<StreamEvent<T>> {
let eventName: string | undefined;
const dataLines: string[] = [];
export const parseSseStream = async function* <T>(stream: Readable): AsyncIterable<StreamEvent<T>> {
let eventName: string | undefined
const dataLines: string[] = []
const emitEvent = function* (): Iterable<StreamEvent<T>> {
if (!eventName && dataLines.length === 0) {
return;
return
}
const raw = dataLines.join("\n");
const parsed = parseMaybeJson(raw) as T | string | null;
const raw = dataLines.join('\n')
const parsed = parseMaybeJson(raw) as T | string | null
yield {
event: eventName,
data: parsed,
raw,
};
eventName = undefined;
dataLines.length = 0;
};
}
eventName = undefined
dataLines.length = 0
}
for await (const line of readLines(stream)) {
if (!line) {
yield* emitEvent();
continue;
yield* emitEvent()
continue
}
if (line.startsWith(":")) {
continue;
if (line.startsWith(':')) {
continue
}
if (line.startsWith("event:")) {
eventName = line.slice("event:".length).trim();
continue;
if (line.startsWith('event:')) {
eventName = line.slice('event:'.length).trim()
continue
}
if (line.startsWith("data:")) {
dataLines.push(line.slice("data:".length).trimStart());
continue;
if (line.startsWith('data:')) {
dataLines.push(line.slice('data:'.length).trimStart())
continue
}
}
yield* emitEvent();
};
yield* emitEvent()
}
const extractTextFromEvent = (data: unknown): string => {
if (typeof data === "string") {
return data;
if (typeof data === 'string') {
return data
}
if (!isRecord(data)) {
return "";
return ''
}
if (typeof data.answer === "string") {
return data.answer;
if (typeof data.answer === 'string') {
return data.answer
}
if (typeof data.text === "string") {
return data.text;
if (typeof data.text === 'string') {
return data.text
}
if (typeof data.delta === "string") {
return data.delta;
if (typeof data.delta === 'string') {
return data.delta
}
return "";
};
return ''
}
export const createSseStream = <T>(
stream: Readable,
meta: { status: number; headers: Headers; requestId?: string }
meta: { status: number; headers: Headers; requestId?: string },
): DifyStream<T> => {
const iterator = parseSseStream<T>(stream)[Symbol.asyncIterator]();
const iterator = parseSseStream<T>(stream)[Symbol.asyncIterator]()
const iterable = {
[Symbol.asyncIterator]: () => iterator,
data: stream,
@@ -126,24 +118,24 @@ export const createSseStream = <T>(
requestId: meta.requestId,
toReadable: () => stream,
toText: async () => {
let text = "";
let text = ''
for await (const event of iterable) {
text += extractTextFromEvent(event.data);
text += extractTextFromEvent(event.data)
}
return text;
return text
},
} satisfies DifyStream<T>;
} satisfies DifyStream<T>
return iterable;
};
return iterable
}
export const createBinaryStream = (
stream: Readable,
meta: { status: number; headers: Headers; requestId?: string }
meta: { status: number; headers: Headers; requestId?: string },
): BinaryStream => ({
data: stream,
status: meta.status,
headers: meta.headers,
requestId: meta.requestId,
toReadable: () => stream,
});
})
+156 -160
View File
@@ -1,102 +1,102 @@
import { Readable } from "node:stream";
import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
import { BASE_URL, ChatClient, DifyClient, WorkflowClient, routes } from "./index";
import { Readable } from 'node:stream'
import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest'
import { BASE_URL, ChatClient, DifyClient, WorkflowClient, routes } from './index'
const stubFetch = (): ReturnType<typeof vi.fn> => {
const fetchMock = vi.fn();
vi.stubGlobal("fetch", fetchMock);
return fetchMock;
};
const fetchMock = vi.fn()
vi.stubGlobal('fetch', fetchMock)
return fetchMock
}
const jsonResponse = (body: unknown, init: ResponseInit = {}): Response =>
new Response(JSON.stringify(body), {
status: 200,
...init,
headers: {
"content-type": "application/json",
'content-type': 'application/json',
...init.headers,
},
});
})
describe("Client", () => {
describe('Client', () => {
beforeEach(() => {
vi.restoreAllMocks();
vi.unstubAllGlobals();
});
vi.restoreAllMocks()
vi.unstubAllGlobals()
})
it("creates a client with default settings", () => {
const difyClient = new DifyClient("test");
it('creates a client with default settings', () => {
const difyClient = new DifyClient('test')
expect(difyClient.getHttpClient().getSettings()).toMatchObject({
apiKey: "test",
apiKey: 'test',
baseUrl: BASE_URL,
timeout: 60,
});
});
})
})
it("updates the api key", () => {
const difyClient = new DifyClient("test");
difyClient.updateApiKey("test2");
it('updates the api key', () => {
const difyClient = new DifyClient('test')
difyClient.updateApiKey('test2')
expect(difyClient.getHttpClient().getSettings().apiKey).toBe("test2");
});
});
expect(difyClient.getHttpClient().getSettings().apiKey).toBe('test2')
})
})
describe("Send Requests", () => {
describe('Send Requests', () => {
beforeEach(() => {
vi.restoreAllMocks();
vi.unstubAllGlobals();
});
vi.restoreAllMocks()
vi.unstubAllGlobals()
})
it("makes a successful request to the application parameter route", async () => {
const fetchMock = stubFetch();
const difyClient = new DifyClient("test");
const method = "GET";
const endpoint = routes.application.url();
it('makes a successful request to the application parameter route', async () => {
const fetchMock = stubFetch()
const difyClient = new DifyClient('test')
const method = 'GET'
const endpoint = routes.application.url()
fetchMock.mockResolvedValueOnce(jsonResponse("response"));
fetchMock.mockResolvedValueOnce(jsonResponse('response'))
const response = await difyClient.sendRequest(method, endpoint);
const response = await difyClient.sendRequest(method, endpoint)
expect(response).toMatchObject({
status: 200,
data: "response",
data: 'response',
headers: {
"content-type": "application/json",
'content-type': 'application/json',
},
});
const [url, init] = fetchMock.mock.calls[0] as [string, RequestInit];
expect(url).toBe(`${BASE_URL}${endpoint}`);
expect(init.method).toBe(method);
})
const [url, init] = fetchMock.mock.calls[0] as [string, RequestInit]
expect(url).toBe(`${BASE_URL}${endpoint}`)
expect(init.method).toBe(method)
expect(init.headers).toMatchObject({
Authorization: "Bearer test",
"User-Agent": "dify-client-node",
});
});
Authorization: 'Bearer test',
'User-Agent': 'dify-client-node',
})
})
it("uses the getMeta route configuration", async () => {
const fetchMock = stubFetch();
const difyClient = new DifyClient("test");
fetchMock.mockResolvedValueOnce(jsonResponse({ ok: true }));
it('uses the getMeta route configuration', async () => {
const fetchMock = stubFetch()
const difyClient = new DifyClient('test')
fetchMock.mockResolvedValueOnce(jsonResponse({ ok: true }))
await difyClient.getMeta("end-user");
await difyClient.getMeta('end-user')
const [url, init] = fetchMock.mock.calls[0] as [string, RequestInit];
expect(url).toBe(`${BASE_URL}${routes.getMeta.url()}?user=end-user`);
expect(init.method).toBe(routes.getMeta.method);
const [url, init] = fetchMock.mock.calls[0] as [string, RequestInit]
expect(url).toBe(`${BASE_URL}${routes.getMeta.url()}?user=end-user`)
expect(init.method).toBe(routes.getMeta.method)
expect(init.headers).toMatchObject({
Authorization: "Bearer test",
});
});
});
Authorization: 'Bearer test',
})
})
})
describe("File uploads", () => {
const OriginalFormData = globalThis.FormData;
describe('File uploads', () => {
const OriginalFormData = globalThis.FormData
beforeAll(() => {
globalThis.FormData = class FormDataMock extends Readable {
constructor() {
super();
super()
}
override _read() {}
@@ -105,136 +105,132 @@ describe("File uploads", () => {
getHeaders() {
return {
"content-type": "multipart/form-data; boundary=test",
};
'content-type': 'multipart/form-data; boundary=test',
}
}
} as unknown as typeof FormData;
});
} as unknown as typeof FormData
})
afterAll(() => {
globalThis.FormData = OriginalFormData;
});
globalThis.FormData = OriginalFormData
})
beforeEach(() => {
vi.restoreAllMocks();
vi.unstubAllGlobals();
});
vi.restoreAllMocks()
vi.unstubAllGlobals()
})
it("does not override multipart boundary headers for legacy FormData", async () => {
const fetchMock = stubFetch();
const difyClient = new DifyClient("test");
const form = new globalThis.FormData();
fetchMock.mockResolvedValueOnce(jsonResponse({ ok: true }));
it('does not override multipart boundary headers for legacy FormData', async () => {
const fetchMock = stubFetch()
const difyClient = new DifyClient('test')
const form = new globalThis.FormData()
fetchMock.mockResolvedValueOnce(jsonResponse({ ok: true }))
await difyClient.fileUpload(form, "end-user");
await difyClient.fileUpload(form, 'end-user')
const [url, init] = fetchMock.mock.calls[0] as [string, RequestInit];
expect(url).toBe(`${BASE_URL}${routes.fileUpload.url()}`);
expect(init.method).toBe(routes.fileUpload.method);
const [url, init] = fetchMock.mock.calls[0] as [string, RequestInit]
expect(url).toBe(`${BASE_URL}${routes.fileUpload.url()}`)
expect(init.method).toBe(routes.fileUpload.method)
expect(init.headers).toMatchObject({
Authorization: "Bearer test",
"content-type": "multipart/form-data; boundary=test",
});
expect(init.body).not.toBe(form);
expect((init as RequestInit & { duplex?: string }).duplex).toBe("half");
});
});
Authorization: 'Bearer test',
'content-type': 'multipart/form-data; boundary=test',
})
expect(init.body).not.toBe(form)
expect((init as RequestInit & { duplex?: string }).duplex).toBe('half')
})
})
describe("Workflow client", () => {
describe('Workflow client', () => {
beforeEach(() => {
vi.restoreAllMocks();
vi.unstubAllGlobals();
});
vi.restoreAllMocks()
vi.unstubAllGlobals()
})
it("uses tasks stop path for workflow stop", async () => {
const fetchMock = stubFetch();
const workflowClient = new WorkflowClient("test");
fetchMock.mockResolvedValueOnce(jsonResponse({ result: "success" }));
it('uses tasks stop path for workflow stop', async () => {
const fetchMock = stubFetch()
const workflowClient = new WorkflowClient('test')
fetchMock.mockResolvedValueOnce(jsonResponse({ result: 'success' }))
await workflowClient.stop("task-1", "end-user");
await workflowClient.stop('task-1', 'end-user')
const [url, init] = fetchMock.mock.calls[0] as [string, RequestInit];
expect(url).toBe(`${BASE_URL}${routes.stopWorkflow.url("task-1")}`);
expect(init.method).toBe(routes.stopWorkflow.method);
const [url, init] = fetchMock.mock.calls[0] as [string, RequestInit]
expect(url).toBe(`${BASE_URL}${routes.stopWorkflow.url('task-1')}`)
expect(init.method).toBe(routes.stopWorkflow.method)
expect(init.headers).toMatchObject({
Authorization: "Bearer test",
"Content-Type": "application/json",
});
expect(init.body).toBe(JSON.stringify({ user: "end-user" }));
});
Authorization: 'Bearer test',
'Content-Type': 'application/json',
})
expect(init.body).toBe(JSON.stringify({ user: 'end-user' }))
})
it("maps workflow log filters to service api params", async () => {
const fetchMock = stubFetch();
const workflowClient = new WorkflowClient("test");
fetchMock.mockResolvedValueOnce(jsonResponse({ ok: true }));
it('maps workflow log filters to service api params', async () => {
const fetchMock = stubFetch()
const workflowClient = new WorkflowClient('test')
fetchMock.mockResolvedValueOnce(jsonResponse({ ok: true }))
await workflowClient.getLogs({
createdAtAfter: "2024-01-01T00:00:00Z",
createdAtBefore: "2024-01-02T00:00:00Z",
createdByEndUserSessionId: "sess-1",
createdByAccount: "acc-1",
createdAtAfter: '2024-01-01T00:00:00Z',
createdAtBefore: '2024-01-02T00:00:00Z',
createdByEndUserSessionId: 'sess-1',
createdByAccount: 'acc-1',
page: 2,
limit: 10,
});
})
const [url] = fetchMock.mock.calls[0] as [string, RequestInit];
const parsedUrl = new URL(url);
expect(parsedUrl.origin + parsedUrl.pathname).toBe(`${BASE_URL}/workflows/logs`);
expect(parsedUrl.searchParams.get("created_at__before")).toBe(
"2024-01-02T00:00:00Z"
);
expect(parsedUrl.searchParams.get("created_at__after")).toBe(
"2024-01-01T00:00:00Z"
);
expect(parsedUrl.searchParams.get("created_by_end_user_session_id")).toBe(
"sess-1"
);
expect(parsedUrl.searchParams.get("created_by_account")).toBe("acc-1");
expect(parsedUrl.searchParams.get("page")).toBe("2");
expect(parsedUrl.searchParams.get("limit")).toBe("10");
});
});
const [url] = fetchMock.mock.calls[0] as [string, RequestInit]
const parsedUrl = new URL(url)
expect(parsedUrl.origin + parsedUrl.pathname).toBe(`${BASE_URL}/workflows/logs`)
expect(parsedUrl.searchParams.get('created_at__before')).toBe('2024-01-02T00:00:00Z')
expect(parsedUrl.searchParams.get('created_at__after')).toBe('2024-01-01T00:00:00Z')
expect(parsedUrl.searchParams.get('created_by_end_user_session_id')).toBe('sess-1')
expect(parsedUrl.searchParams.get('created_by_account')).toBe('acc-1')
expect(parsedUrl.searchParams.get('page')).toBe('2')
expect(parsedUrl.searchParams.get('limit')).toBe('10')
})
})
describe("Chat client", () => {
describe('Chat client', () => {
beforeEach(() => {
vi.restoreAllMocks();
vi.unstubAllGlobals();
});
vi.restoreAllMocks()
vi.unstubAllGlobals()
})
it("places user in query for suggested messages", async () => {
const fetchMock = stubFetch();
const chatClient = new ChatClient("test");
fetchMock.mockResolvedValueOnce(jsonResponse({ result: "success", data: [] }));
it('places user in query for suggested messages', async () => {
const fetchMock = stubFetch()
const chatClient = new ChatClient('test')
fetchMock.mockResolvedValueOnce(jsonResponse({ result: 'success', data: [] }))
await chatClient.getSuggested("msg-1", "end-user");
await chatClient.getSuggested('msg-1', 'end-user')
const [url, init] = fetchMock.mock.calls[0] as [string, RequestInit];
expect(url).toBe(`${BASE_URL}${routes.getSuggested.url("msg-1")}?user=end-user`);
expect(init.method).toBe(routes.getSuggested.method);
const [url, init] = fetchMock.mock.calls[0] as [string, RequestInit]
expect(url).toBe(`${BASE_URL}${routes.getSuggested.url('msg-1')}?user=end-user`)
expect(init.method).toBe(routes.getSuggested.method)
expect(init.headers).toMatchObject({
Authorization: "Bearer test",
});
});
Authorization: 'Bearer test',
})
})
it("uses last_id when listing conversations", async () => {
const fetchMock = stubFetch();
const chatClient = new ChatClient("test");
fetchMock.mockResolvedValueOnce(jsonResponse({ ok: true }));
it('uses last_id when listing conversations', async () => {
const fetchMock = stubFetch()
const chatClient = new ChatClient('test')
fetchMock.mockResolvedValueOnce(jsonResponse({ ok: true }))
await chatClient.getConversations("end-user", "last-1", 10);
await chatClient.getConversations('end-user', 'last-1', 10)
const [url] = fetchMock.mock.calls[0] as [string, RequestInit];
expect(url).toBe(`${BASE_URL}${routes.getConversations.url()}?user=end-user&last_id=last-1&limit=10`);
});
const [url] = fetchMock.mock.calls[0] as [string, RequestInit]
expect(url).toBe(
`${BASE_URL}${routes.getConversations.url()}?user=end-user&last_id=last-1&limit=10`,
)
})
it("lists app feedbacks without user params", async () => {
const fetchMock = stubFetch();
const chatClient = new ChatClient("test");
fetchMock.mockResolvedValueOnce(jsonResponse({ data: [] }));
it('lists app feedbacks without user params', async () => {
const fetchMock = stubFetch()
const chatClient = new ChatClient('test')
fetchMock.mockResolvedValueOnce(jsonResponse({ data: [] }))
await chatClient.getAppFeedbacks(1, 20);
await chatClient.getAppFeedbacks(1, 20)
const [url] = fetchMock.mock.calls[0] as [string, RequestInit];
expect(url).toBe(`${BASE_URL}/app/feedbacks?page=1&limit=20`);
});
});
const [url] = fetchMock.mock.calls[0] as [string, RequestInit]
expect(url).toBe(`${BASE_URL}/app/feedbacks?page=1&limit=20`)
})
})
+50 -50
View File
@@ -1,103 +1,103 @@
import { DEFAULT_BASE_URL } from "./types/common";
import { DEFAULT_BASE_URL } from './types/common'
export const BASE_URL = DEFAULT_BASE_URL;
export const BASE_URL = DEFAULT_BASE_URL
export const routes = {
feedback: {
method: "POST",
method: 'POST',
url: (messageId: string) => `/messages/${messageId}/feedbacks`,
},
application: {
method: "GET",
url: () => "/parameters",
method: 'GET',
url: () => '/parameters',
},
fileUpload: {
method: "POST",
url: () => "/files/upload",
method: 'POST',
url: () => '/files/upload',
},
filePreview: {
method: "GET",
method: 'GET',
url: (fileId: string) => `/files/${fileId}/preview`,
},
textToAudio: {
method: "POST",
url: () => "/text-to-audio",
method: 'POST',
url: () => '/text-to-audio',
},
audioToText: {
method: "POST",
url: () => "/audio-to-text",
method: 'POST',
url: () => '/audio-to-text',
},
getMeta: {
method: "GET",
url: () => "/meta",
method: 'GET',
url: () => '/meta',
},
getInfo: {
method: "GET",
url: () => "/info",
method: 'GET',
url: () => '/info',
},
getSite: {
method: "GET",
url: () => "/site",
method: 'GET',
url: () => '/site',
},
createCompletionMessage: {
method: "POST",
url: () => "/completion-messages",
method: 'POST',
url: () => '/completion-messages',
},
stopCompletionMessage: {
method: "POST",
method: 'POST',
url: (taskId: string) => `/completion-messages/${taskId}/stop`,
},
createChatMessage: {
method: "POST",
url: () => "/chat-messages",
method: 'POST',
url: () => '/chat-messages',
},
getSuggested: {
method: "GET",
method: 'GET',
url: (messageId: string) => `/messages/${messageId}/suggested`,
},
stopChatMessage: {
method: "POST",
method: 'POST',
url: (taskId: string) => `/chat-messages/${taskId}/stop`,
},
getConversations: {
method: "GET",
url: () => "/conversations",
method: 'GET',
url: () => '/conversations',
},
getConversationMessages: {
method: "GET",
url: () => "/messages",
method: 'GET',
url: () => '/messages',
},
renameConversation: {
method: "POST",
method: 'POST',
url: (conversationId: string) => `/conversations/${conversationId}/name`,
},
deleteConversation: {
method: "DELETE",
method: 'DELETE',
url: (conversationId: string) => `/conversations/${conversationId}`,
},
runWorkflow: {
method: "POST",
url: () => "/workflows/run",
method: 'POST',
url: () => '/workflows/run',
},
stopWorkflow: {
method: "POST",
method: 'POST',
url: (taskId: string) => `/workflows/tasks/${taskId}/stop`,
},
};
}
export { DifyClient } from "./client/base";
export { ChatClient } from "./client/chat";
export { CompletionClient } from "./client/completion";
export { WorkflowClient } from "./client/workflow";
export { KnowledgeBaseClient } from "./client/knowledge-base";
export { WorkspaceClient } from "./client/workspace";
export { DifyClient } from './client/base'
export { ChatClient } from './client/chat'
export { CompletionClient } from './client/completion'
export { WorkflowClient } from './client/workflow'
export { KnowledgeBaseClient } from './client/knowledge-base'
export { WorkspaceClient } from './client/workspace'
export * from "./errors/dify-error";
export * from "./types/common";
export * from "./types/annotation";
export * from "./types/chat";
export * from "./types/completion";
export * from "./types/knowledge-base";
export * from "./types/workflow";
export * from "./types/workspace";
export { HttpClient } from "./http/client";
export * from './errors/dify-error'
export * from './types/common'
export * from './types/annotation'
export * from './types/chat'
export * from './types/completion'
export * from './types/knowledge-base'
export * from './types/workflow'
export * from './types/workspace'
export { HttpClient } from './http/client'
@@ -1,9 +1,7 @@
export const isRecord = (value: unknown): value is Record<string, unknown> =>
typeof value === "object" && value !== null;
typeof value === 'object' && value !== null
export const hasStringProperty = <
TKey extends string,
>(
export const hasStringProperty = <TKey extends string>(
value: unknown,
key: TKey
): value is Record<TKey, string> => isRecord(value) && typeof value[key] === "string";
key: TKey,
): value is Record<TKey, string> => isRecord(value) && typeof value[key] === 'string'
+13 -13
View File
@@ -1,19 +1,19 @@
export type AnnotationCreateRequest = {
question: string;
answer: string;
};
question: string
answer: string
}
export type AnnotationReplyActionRequest = {
score_threshold: number;
embedding_provider_name: string;
embedding_model_name: string;
};
score_threshold: number
embedding_provider_name: string
embedding_model_name: string
}
export type AnnotationListOptions = {
page?: number;
limit?: number;
keyword?: string;
};
page?: number
limit?: number
keyword?: string
}
export type AnnotationResponse = JsonObject;
import type { JsonObject } from "./common";
export type AnnotationResponse = JsonObject
import type { JsonObject } from './common'
+14 -23
View File
@@ -1,28 +1,19 @@
import type {
DifyRequestFile,
JsonObject,
ResponseMode,
StreamEvent,
} from "./common";
import type { DifyRequestFile, JsonObject, ResponseMode, StreamEvent } from './common'
export type ChatMessageRequest = {
inputs?: JsonObject;
query: string;
user: string;
response_mode?: ResponseMode;
files?: DifyRequestFile[] | null;
conversation_id?: string;
auto_generate_name?: boolean;
workflow_id?: string;
retriever_from?: "app" | "dataset";
};
inputs?: JsonObject
query: string
user: string
response_mode?: ResponseMode
files?: DifyRequestFile[] | null
conversation_id?: string
auto_generate_name?: boolean
workflow_id?: string
retriever_from?: 'app' | 'dataset'
}
export type ChatMessageResponse = JsonObject;
export type ChatMessageResponse = JsonObject
export type ChatStreamEvent = StreamEvent<JsonObject>;
export type ChatStreamEvent = StreamEvent<JsonObject>
export type ConversationSortBy =
| "created_at"
| "-created_at"
| "updated_at"
| "-updated_at";
export type ConversationSortBy = 'created_at' | '-created_at' | 'updated_at' | '-updated_at'
+60 -60
View File
@@ -1,87 +1,87 @@
import type { Readable } from "node:stream";
import type { Readable } from 'node:stream'
export const DEFAULT_BASE_URL = "https://api.dify.ai/v1";
export const DEFAULT_TIMEOUT_SECONDS = 60;
export const DEFAULT_MAX_RETRIES = 3;
export const DEFAULT_RETRY_DELAY_SECONDS = 1;
export const DEFAULT_BASE_URL = 'https://api.dify.ai/v1'
export const DEFAULT_TIMEOUT_SECONDS = 60
export const DEFAULT_MAX_RETRIES = 3
export const DEFAULT_RETRY_DELAY_SECONDS = 1
export type RequestMethod = "GET" | "POST" | "PATCH" | "PUT" | "DELETE";
export type ResponseMode = "blocking" | "streaming";
export type JsonPrimitive = string | number | boolean | null;
export type JsonValue = JsonPrimitive | JsonObject | JsonArray;
export type RequestMethod = 'GET' | 'POST' | 'PATCH' | 'PUT' | 'DELETE'
export type ResponseMode = 'blocking' | 'streaming'
export type JsonPrimitive = string | number | boolean | null
export type JsonValue = JsonPrimitive | JsonObject | JsonArray
export type JsonObject = {
[key: string]: JsonValue;
};
export type JsonArray = JsonValue[];
[key: string]: JsonValue
}
export type JsonArray = JsonValue[]
export type QueryParamValue =
| string
| number
| boolean
| Array<string | number | boolean>
| undefined;
| undefined
export type QueryParams = Record<string, QueryParamValue>;
export type QueryParams = Record<string, QueryParamValue>
export type Headers = Record<string, string>;
export type DifyRequestFile = JsonObject;
export type Headers = Record<string, string>
export type DifyRequestFile = JsonObject
export type SuccessResponse = {
result: "success";
};
result: 'success'
}
export type SuggestedQuestionsResponse = SuccessResponse & {
data: string[];
};
data: string[]
}
export type DifyClientConfig = {
apiKey: string;
baseUrl?: string;
timeout?: number;
maxRetries?: number;
retryDelay?: number;
enableLogging?: boolean;
};
apiKey: string
baseUrl?: string
timeout?: number
maxRetries?: number
retryDelay?: number
enableLogging?: boolean
}
export type DifyResponse<T> = {
data: T;
status: number;
headers: Headers;
requestId?: string;
};
data: T
status: number
headers: Headers
requestId?: string
}
export type MessageFeedbackRequest = {
messageId: string;
user: string;
rating?: "like" | "dislike" | null;
content?: string | null;
};
messageId: string
user: string
rating?: 'like' | 'dislike' | null
content?: string | null
}
export type TextToAudioRequest = {
user: string;
text?: string;
message_id?: string;
streaming?: boolean;
voice?: string;
};
user: string
text?: string
message_id?: string
streaming?: boolean
voice?: string
}
export type StreamEvent<T = unknown> = {
event?: string;
data: T | string | null;
raw: string;
};
event?: string
data: T | string | null
raw: string
}
export type DifyStream<T = unknown> = AsyncIterable<StreamEvent<T>> & {
data: Readable;
status: number;
headers: Headers;
requestId?: string;
toText(): Promise<string>;
toReadable(): Readable;
};
data: Readable
status: number
headers: Headers
requestId?: string
toText(): Promise<string>
toReadable(): Readable
}
export type BinaryStream = {
data: Readable;
status: number;
headers: Headers;
requestId?: string;
toReadable(): Readable;
};
data: Readable
status: number
headers: Headers
requestId?: string
toReadable(): Readable
}
+9 -14
View File
@@ -1,18 +1,13 @@
import type {
DifyRequestFile,
JsonObject,
ResponseMode,
StreamEvent,
} from "./common";
import type { DifyRequestFile, JsonObject, ResponseMode, StreamEvent } from './common'
export type CompletionRequest = {
inputs?: JsonObject;
response_mode?: ResponseMode;
user: string;
files?: DifyRequestFile[] | null;
retriever_from?: "app" | "dataset";
};
inputs?: JsonObject
response_mode?: ResponseMode
user: string
files?: DifyRequestFile[] | null
retriever_from?: 'app' | 'dataset'
}
export type CompletionResponse = JsonObject;
export type CompletionResponse = JsonObject
export type CompletionStreamEvent = StreamEvent<JsonObject>;
export type CompletionStreamEvent = StreamEvent<JsonObject>
+129 -129
View File
@@ -1,185 +1,185 @@
export type DatasetListOptions = {
page?: number;
limit?: number;
keyword?: string | null;
tagIds?: string[];
includeAll?: boolean;
};
page?: number
limit?: number
keyword?: string | null
tagIds?: string[]
includeAll?: boolean
}
export type DatasetCreateRequest = {
name: string;
description?: string;
indexing_technique?: "high_quality" | "economy";
permission?: string | null;
external_knowledge_api_id?: string | null;
provider?: string;
external_knowledge_id?: string | null;
retrieval_model?: JsonObject | null;
embedding_model?: string | null;
embedding_model_provider?: string | null;
};
name: string
description?: string
indexing_technique?: 'high_quality' | 'economy'
permission?: string | null
external_knowledge_api_id?: string | null
provider?: string
external_knowledge_id?: string | null
retrieval_model?: JsonObject | null
embedding_model?: string | null
embedding_model_provider?: string | null
}
export type DatasetUpdateRequest = {
name?: string;
description?: string | null;
indexing_technique?: "high_quality" | "economy" | null;
permission?: string | null;
embedding_model?: string | null;
embedding_model_provider?: string | null;
retrieval_model?: JsonObject | null;
partial_member_list?: Array<Record<string, string>> | null;
external_retrieval_model?: JsonObject | null;
external_knowledge_id?: string | null;
external_knowledge_api_id?: string | null;
};
name?: string
description?: string | null
indexing_technique?: 'high_quality' | 'economy' | null
permission?: string | null
embedding_model?: string | null
embedding_model_provider?: string | null
retrieval_model?: JsonObject | null
partial_member_list?: Array<Record<string, string>> | null
external_retrieval_model?: JsonObject | null
external_knowledge_id?: string | null
external_knowledge_api_id?: string | null
}
export type DocumentStatusAction = "enable" | "disable" | "archive" | "un_archive";
export type DocumentStatusAction = 'enable' | 'disable' | 'archive' | 'un_archive'
export type DatasetTagCreateRequest = {
name: string;
};
name: string
}
export type DatasetTagUpdateRequest = {
tag_id: string;
name: string;
};
tag_id: string
name: string
}
export type DatasetTagDeleteRequest = {
tag_id: string;
};
tag_id: string
}
export type DatasetTagBindingRequest = {
tag_ids: string[];
target_id: string;
};
tag_ids: string[]
target_id: string
}
export type DatasetTagUnbindingRequest = {
tag_id: string;
target_id: string;
};
tag_id: string
target_id: string
}
export type DocumentTextCreateRequest = {
name: string;
text: string;
process_rule?: JsonObject | null;
original_document_id?: string | null;
doc_form?: string;
doc_language?: string;
indexing_technique?: string | null;
retrieval_model?: JsonObject | null;
embedding_model?: string | null;
embedding_model_provider?: string | null;
};
name: string
text: string
process_rule?: JsonObject | null
original_document_id?: string | null
doc_form?: string
doc_language?: string
indexing_technique?: string | null
retrieval_model?: JsonObject | null
embedding_model?: string | null
embedding_model_provider?: string | null
}
export type DocumentTextUpdateRequest = {
name?: string | null;
text?: string | null;
process_rule?: JsonObject | null;
doc_form?: string;
doc_language?: string;
retrieval_model?: JsonObject | null;
};
name?: string | null
text?: string | null
process_rule?: JsonObject | null
doc_form?: string
doc_language?: string
retrieval_model?: JsonObject | null
}
export type DocumentListOptions = {
page?: number;
limit?: number;
keyword?: string | null;
status?: string | null;
};
page?: number
limit?: number
keyword?: string | null
status?: string | null
}
export type DocumentGetOptions = {
metadata?: "all" | "only" | "without";
};
metadata?: 'all' | 'only' | 'without'
}
export type SegmentCreateRequest = {
segments: JsonObject[];
};
segments: JsonObject[]
}
export type SegmentUpdateRequest = {
segment: {
content?: string | null;
answer?: string | null;
keywords?: string[] | null;
regenerate_child_chunks?: boolean;
enabled?: boolean | null;
attachment_ids?: string[] | null;
};
};
content?: string | null
answer?: string | null
keywords?: string[] | null
regenerate_child_chunks?: boolean
enabled?: boolean | null
attachment_ids?: string[] | null
}
}
export type SegmentListOptions = {
page?: number;
limit?: number;
status?: string[];
keyword?: string | null;
};
page?: number
limit?: number
status?: string[]
keyword?: string | null
}
export type ChildChunkCreateRequest = {
content: string;
};
content: string
}
export type ChildChunkUpdateRequest = {
content: string;
};
content: string
}
export type ChildChunkListOptions = {
page?: number;
limit?: number;
keyword?: string | null;
};
page?: number
limit?: number
keyword?: string | null
}
export type MetadataCreateRequest = {
type: "string" | "number" | "time";
name: string;
};
type: 'string' | 'number' | 'time'
name: string
}
export type MetadataUpdateRequest = {
name: string;
value?: string | number | null;
};
name: string
value?: string | number | null
}
export type DocumentMetadataDetail = {
id: string;
name: string;
value?: string | number | null;
};
id: string
name: string
value?: string | number | null
}
export type DocumentMetadataOperation = {
document_id: string;
metadata_list: DocumentMetadataDetail[];
partial_update?: boolean;
};
document_id: string
metadata_list: DocumentMetadataDetail[]
partial_update?: boolean
}
export type MetadataOperationRequest = {
operation_data: DocumentMetadataOperation[];
};
operation_data: DocumentMetadataOperation[]
}
export type HitTestingRequest = {
query?: string | null;
retrieval_model?: JsonObject | null;
external_retrieval_model?: JsonObject | null;
attachment_ids?: string[] | null;
};
query?: string | null
retrieval_model?: JsonObject | null
external_retrieval_model?: JsonObject | null
attachment_ids?: string[] | null
}
export type DatasourcePluginListOptions = {
isPublished?: boolean;
};
isPublished?: boolean
}
export type DatasourceNodeRunRequest = {
inputs: JsonObject;
datasource_type: string;
credential_id?: string | null;
is_published: boolean;
};
inputs: JsonObject
datasource_type: string
credential_id?: string | null
is_published: boolean
}
export type PipelineRunRequest = {
inputs: JsonObject;
datasource_type: string;
datasource_info_list: JsonObject[];
start_node_id: string;
is_published: boolean;
response_mode: ResponseMode;
};
inputs: JsonObject
datasource_type: string
datasource_info_list: JsonObject[]
start_node_id: string
is_published: boolean
response_mode: ResponseMode
}
export type KnowledgeBaseResponse = JsonObject;
export type PipelineStreamEvent = JsonObject;
import type { JsonObject, ResponseMode } from "./common";
export type KnowledgeBaseResponse = JsonObject
export type PipelineStreamEvent = JsonObject
import type { JsonObject, ResponseMode } from './common'
+8 -13
View File
@@ -1,17 +1,12 @@
import type {
DifyRequestFile,
JsonObject,
ResponseMode,
StreamEvent,
} from "./common";
import type { DifyRequestFile, JsonObject, ResponseMode, StreamEvent } from './common'
export type WorkflowRunRequest = {
inputs?: JsonObject;
user: string;
response_mode?: ResponseMode;
files?: DifyRequestFile[] | null;
};
inputs?: JsonObject
user: string
response_mode?: ResponseMode
files?: DifyRequestFile[] | null
}
export type WorkflowRunResponse = JsonObject;
export type WorkflowRunResponse = JsonObject
export type WorkflowStreamEvent = StreamEvent<JsonObject>;
export type WorkflowStreamEvent = StreamEvent<JsonObject>
+3 -3
View File
@@ -1,4 +1,4 @@
import type { JsonObject } from "./common";
import type { JsonObject } from './common'
export type WorkspaceModelType = string;
export type WorkspaceModelsResponse = JsonObject;
export type WorkspaceModelType = string
export type WorkspaceModelsResponse = JsonObject
@@ -1,137 +1,137 @@
import { createServer } from "node:http";
import { Readable } from "node:stream";
import type { AddressInfo } from "node:net";
import { afterAll, beforeAll, describe, expect, it } from "vitest";
import { HttpClient } from "../src/http/client";
import type { AddressInfo } from 'node:net'
import { createServer } from 'node:http'
import { Readable } from 'node:stream'
import { afterAll, beforeAll, describe, expect, it } from 'vitest'
import { HttpClient } from '../src/http/client'
const readBody = async (stream: NodeJS.ReadableStream): Promise<Buffer> => {
const chunks: Buffer[] = [];
const chunks: Buffer[] = []
for await (const chunk of stream) {
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk))
}
return Buffer.concat(chunks);
};
return Buffer.concat(chunks)
}
describe("HttpClient integration", () => {
describe('HttpClient integration', () => {
const requests: Array<{
url: string;
method: string;
headers: Record<string, string | string[] | undefined>;
body: Buffer;
}> = [];
url: string
method: string
headers: Record<string, string | string[] | undefined>
body: Buffer
}> = []
const server = createServer((req, res) => {
void (async () => {
const body = await readBody(req);
const body = await readBody(req)
requests.push({
url: req.url ?? "",
method: req.method ?? "",
url: req.url ?? '',
method: req.method ?? '',
headers: req.headers,
body,
});
})
if (req.url?.startsWith("/json")) {
res.writeHead(200, { "content-type": "application/json", "x-request-id": "req-json" });
res.end(JSON.stringify({ ok: true }));
return;
if (req.url?.startsWith('/json')) {
res.writeHead(200, { 'content-type': 'application/json', 'x-request-id': 'req-json' })
res.end(JSON.stringify({ ok: true }))
return
}
if (req.url === "/stream") {
res.writeHead(200, { "content-type": "text/event-stream" });
res.end('data: {"answer":"hello"}\n\ndata: {"delta":" world"}\n\n');
return;
if (req.url === '/stream') {
res.writeHead(200, { 'content-type': 'text/event-stream' })
res.end('data: {"answer":"hello"}\n\ndata: {"delta":" world"}\n\n')
return
}
if (req.url === "/bytes") {
res.writeHead(200, { "content-type": "application/octet-stream" });
res.end(Buffer.from([1, 2, 3, 4]));
return;
if (req.url === '/bytes') {
res.writeHead(200, { 'content-type': 'application/octet-stream' })
res.end(Buffer.from([1, 2, 3, 4]))
return
}
if (req.url === "/upload-stream") {
res.writeHead(200, { "content-type": "application/json" });
res.end(JSON.stringify({ received: body.toString("utf8") }));
return;
if (req.url === '/upload-stream') {
res.writeHead(200, { 'content-type': 'application/json' })
res.end(JSON.stringify({ received: body.toString('utf8') }))
return
}
res.writeHead(404, { "content-type": "application/json" });
res.end(JSON.stringify({ message: "not found" }));
})();
});
res.writeHead(404, { 'content-type': 'application/json' })
res.end(JSON.stringify({ message: 'not found' }))
})()
})
let client: HttpClient;
let client: HttpClient
beforeAll(async () => {
await new Promise<void>((resolve) => {
server.listen(0, "127.0.0.1", () => resolve());
});
const address = server.address() as AddressInfo;
server.listen(0, '127.0.0.1', () => resolve())
})
const address = server.address() as AddressInfo
client = new HttpClient({
apiKey: "test-key",
apiKey: 'test-key',
baseUrl: `http://127.0.0.1:${address.port}`,
maxRetries: 0,
retryDelay: 0,
});
});
})
})
afterAll(async () => {
await new Promise<void>((resolve, reject) => {
server.close((error) => {
if (error) {
reject(error);
return;
reject(error)
return
}
resolve();
});
});
});
resolve()
})
})
})
it("uses real fetch for query serialization and json bodies", async () => {
it('uses real fetch for query serialization and json bodies', async () => {
const response = await client.request({
method: "POST",
path: "/json",
query: { tag_ids: ["a", "b"], limit: 2 },
data: { user: "u" },
});
method: 'POST',
path: '/json',
query: { tag_ids: ['a', 'b'], limit: 2 },
data: { user: 'u' },
})
expect(response.requestId).toBe("req-json");
expect(response.data).toEqual({ ok: true });
expect(response.requestId).toBe('req-json')
expect(response.data).toEqual({ ok: true })
expect(requests.at(-1)).toMatchObject({
url: "/json?tag_ids=a&tag_ids=b&limit=2",
method: "POST",
});
expect(requests.at(-1)?.headers.authorization).toBe("Bearer test-key");
expect(requests.at(-1)?.headers["content-type"]).toBe("application/json");
expect(requests.at(-1)?.body.toString("utf8")).toBe(JSON.stringify({ user: "u" }));
});
url: '/json?tag_ids=a&tag_ids=b&limit=2',
method: 'POST',
})
expect(requests.at(-1)?.headers.authorization).toBe('Bearer test-key')
expect(requests.at(-1)?.headers['content-type']).toBe('application/json')
expect(requests.at(-1)?.body.toString('utf8')).toBe(JSON.stringify({ user: 'u' }))
})
it("supports streaming request bodies with duplex fetch", async () => {
it('supports streaming request bodies with duplex fetch', async () => {
const response = await client.request<{ received: string }>({
method: "POST",
path: "/upload-stream",
data: Readable.from(["hello ", "world"]),
});
method: 'POST',
path: '/upload-stream',
data: Readable.from(['hello ', 'world']),
})
expect(response.data).toEqual({ received: "hello world" });
expect(requests.at(-1)?.body.toString("utf8")).toBe("hello world");
});
expect(response.data).toEqual({ received: 'hello world' })
expect(requests.at(-1)?.body.toString('utf8')).toBe('hello world')
})
it("parses real sse responses into text", async () => {
it('parses real sse responses into text', async () => {
const stream = await client.requestStream({
method: "GET",
path: "/stream",
});
method: 'GET',
path: '/stream',
})
await expect(stream.toText()).resolves.toBe("hello world");
});
await expect(stream.toText()).resolves.toBe('hello world')
})
it("parses real byte responses into buffers", async () => {
const response = await client.request<Buffer, "bytes">({
method: "GET",
path: "/bytes",
responseType: "bytes",
});
it('parses real byte responses into buffers', async () => {
const response = await client.request<Buffer, 'bytes'>({
method: 'GET',
path: '/bytes',
responseType: 'bytes',
})
expect(Array.from(response.data.values())).toEqual([1, 2, 3, 4]);
});
});
expect(Array.from(response.data.values())).toEqual([1, 2, 3, 4])
})
})
+28 -28
View File
@@ -1,48 +1,48 @@
import { vi } from "vitest";
import { HttpClient } from "../src/http/client";
import type { DifyClientConfig } from "../src/types/common";
import type { DifyClientConfig } from '../src/types/common'
import { vi } from 'vitest'
import { HttpClient } from '../src/http/client'
type FetchMock = ReturnType<typeof vi.fn>;
type RequestSpy = ReturnType<typeof vi.fn>;
type FetchMock = ReturnType<typeof vi.fn>
type RequestSpy = ReturnType<typeof vi.fn>
type HttpClientWithFetchMock = {
client: HttpClient;
fetchMock: FetchMock;
};
client: HttpClient
fetchMock: FetchMock
}
type HttpClientWithSpies = HttpClientWithFetchMock & {
request: RequestSpy;
requestStream: RequestSpy;
requestBinaryStream: RequestSpy;
};
request: RequestSpy
requestStream: RequestSpy
requestBinaryStream: RequestSpy
}
export const createHttpClient = (
configOverrides: Partial<DifyClientConfig> = {}
configOverrides: Partial<DifyClientConfig> = {},
): HttpClientWithFetchMock => {
const fetchMock = vi.fn();
vi.stubGlobal("fetch", fetchMock);
const client = new HttpClient({ apiKey: "test", ...configOverrides });
return { client, fetchMock };
};
const fetchMock = vi.fn()
vi.stubGlobal('fetch', fetchMock)
const client = new HttpClient({ apiKey: 'test', ...configOverrides })
return { client, fetchMock }
}
export const createHttpClientWithSpies = (
configOverrides: Partial<DifyClientConfig> = {}
configOverrides: Partial<DifyClientConfig> = {},
): HttpClientWithSpies => {
const { client, fetchMock } = createHttpClient(configOverrides);
const { client, fetchMock } = createHttpClient(configOverrides)
const request = vi
.spyOn(client, "request")
.mockResolvedValue({ data: "ok", status: 200, headers: {} });
.spyOn(client, 'request')
.mockResolvedValue({ data: 'ok', status: 200, headers: {} })
const requestStream = vi
.spyOn(client, "requestStream")
.mockResolvedValue({ data: null, status: 200, headers: {} } as never);
.spyOn(client, 'requestStream')
.mockResolvedValue({ data: null, status: 200, headers: {} } as never)
const requestBinaryStream = vi
.spyOn(client, "requestBinaryStream")
.mockResolvedValue({ data: null, status: 200, headers: {} } as never);
.spyOn(client, 'requestBinaryStream')
.mockResolvedValue({ data: null, status: 200, headers: {} } as never)
return {
client,
fetchMock,
request,
requestStream,
requestBinaryStream,
};
};
}
}
+12 -12
View File
@@ -1,26 +1,26 @@
import { defineConfig } from "vite-plus";
import { defineConfig } from 'vite-plus'
export default defineConfig({
pack: {
entry: ["src/index.ts"],
format: ["esm"],
platform: "node",
entry: ['src/index.ts'],
format: ['esm'],
platform: 'node',
dts: true,
clean: true,
sourcemap: true,
// splitting: false,
treeshake: true,
outDir: "dist",
outDir: 'dist',
target: false,
},
test: {
environment: "node",
include: ["**/*.test.ts"],
environment: 'node',
include: ['**/*.test.ts'],
coverage: {
provider: "v8",
reporter: ["text", "text-summary"],
include: ["src/**/*.ts"],
exclude: ["src/**/*.test.*", "src/**/*.spec.*"],
provider: 'v8',
reporter: ['text', 'text-summary'],
include: ['src/**/*.ts'],
exclude: ['src/**/*.test.*', 'src/**/*.spec.*'],
},
},
});
})