mirror of
https://github.com/langgenius/dify.git
synced 2026-09-21 05:11:22 +08:00
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:
co-authored by
autofix-ci[bot]
parent
fde08d24fe
commit
a84c2d36a3
@@ -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:
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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',
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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' },
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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' },
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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'`)
|
||||
}
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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'))
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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 {}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
})
|
||||
|
||||
@@ -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`)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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])
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.*'],
|
||||
},
|
||||
},
|
||||
});
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user