|
|
@@ -1,332 +1,349 @@
|
|
|
-import type { AxiosRequestConfig, Method, RawAxiosRequestHeaders } from 'axios'
|
|
|
-import axios from 'axios'
|
|
|
-import { IRestApiContext } from './types'
|
|
|
-
|
|
|
-export const NO_NETWORK_ERROR_CODE = 999
|
|
|
-export const STREAM_SEPARATOR = '⧉⇋⇋➽⌑⧉§§\n'
|
|
|
+import Axios, {
|
|
|
+ type AxiosInstance,
|
|
|
+ type AxiosRequestConfig,
|
|
|
+ type CustomParamsSerializer,
|
|
|
+ type AxiosResponse,
|
|
|
+ type InternalAxiosRequestConfig,
|
|
|
+ type AxiosError
|
|
|
+} from 'axios'
|
|
|
+import { stringify } from 'qs'
|
|
|
+
|
|
|
+// 基础配置
|
|
|
+const defaultConfig: AxiosRequestConfig = {
|
|
|
+ timeout: 6000,
|
|
|
+ headers: {
|
|
|
+ 'Content-Type': 'application/json;charset=utf-8'
|
|
|
+ },
|
|
|
+ paramsSerializer: {
|
|
|
+ serialize: stringify as unknown as CustomParamsSerializer
|
|
|
+ }
|
|
|
+}
|
|
|
|
|
|
-type GenericValue = string | number | boolean | object | null | undefined
|
|
|
-export interface IDataObject {
|
|
|
- [key: string]: GenericValue | IDataObject | GenericValue[] | IDataObject[]
|
|
|
+// 响应数据基础结构
|
|
|
+export interface BaseResponse {
|
|
|
+ code: number
|
|
|
+ error?: string
|
|
|
+ isAuthorized?: boolean
|
|
|
+ isSuccess?: boolean
|
|
|
}
|
|
|
|
|
|
-const BROWSER_ID_STORAGE_KEY = 'n8n-browserId'
|
|
|
+type OmitBaseResponse<T> = Omit<T, keyof BaseResponse>
|
|
|
|
|
|
-const baseURL = 'dev-n8n.shalu.com'
|
|
|
-const getBrowserId = () => {
|
|
|
- let browserId = localStorage.getItem(BROWSER_ID_STORAGE_KEY)
|
|
|
- if (!browserId) {
|
|
|
- browserId = crypto.randomUUID()
|
|
|
- localStorage.setItem(BROWSER_ID_STORAGE_KEY, browserId)
|
|
|
- }
|
|
|
- return browserId
|
|
|
-}
|
|
|
+export type ResponseData<T = any> = BaseResponse & OmitBaseResponse<T>
|
|
|
|
|
|
-export class ResponseError {
|
|
|
- name?: string
|
|
|
- // The HTTP status code of response
|
|
|
- httpStatusCode?: number
|
|
|
-
|
|
|
- // The error code in the response
|
|
|
- errorCode?: number
|
|
|
-
|
|
|
- // The stack trace of the server
|
|
|
- serverStackTrace?: string
|
|
|
-
|
|
|
- // Additional metadata from the server (e.g., EULA URL)
|
|
|
- meta?: Record<string, unknown>
|
|
|
-
|
|
|
- /**
|
|
|
- * Creates an instance of ResponseError.
|
|
|
- * @param {string} message The error message
|
|
|
- * @param {number} [errorCode] The error code which can be used by frontend to identify the actual error
|
|
|
- * @param {number} [httpStatusCode] The HTTP status code the response should have
|
|
|
- * @param {string} [stack] The stack trace
|
|
|
- * @param {Record<string, unknown>} [meta] Additional metadata from the server
|
|
|
- */
|
|
|
- constructor(
|
|
|
- message: string,
|
|
|
- options: {
|
|
|
- errorCode?: number
|
|
|
- httpStatusCode?: number
|
|
|
- stack?: string
|
|
|
- meta?: Record<string, unknown>
|
|
|
- } = {}
|
|
|
- ) {
|
|
|
- // super(message);
|
|
|
- this.name = 'ResponseError'
|
|
|
-
|
|
|
- const { errorCode, httpStatusCode, stack, meta } = options
|
|
|
- if (errorCode) {
|
|
|
- this.errorCode = errorCode
|
|
|
- }
|
|
|
- if (httpStatusCode) {
|
|
|
- this.httpStatusCode = httpStatusCode
|
|
|
- }
|
|
|
- if (stack) {
|
|
|
- this.serverStackTrace = stack
|
|
|
- }
|
|
|
- if (meta) {
|
|
|
- this.meta = meta
|
|
|
- }
|
|
|
- }
|
|
|
-}
|
|
|
+export type ResponseValidator<T = any> = (data: ResponseData<T>) => boolean
|
|
|
|
|
|
-// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
|
-const legacyParamSerializer = (params: Record<string, any>) =>
|
|
|
- Object.keys(params)
|
|
|
- .filter((key) => params[key] !== undefined)
|
|
|
- .map((key) => {
|
|
|
- if (Array.isArray(params[key])) {
|
|
|
- return params[key].map((v: string) => `${key}[]=${encodeURIComponent(v)}`).join('&')
|
|
|
- }
|
|
|
- if (typeof params[key] === 'object') {
|
|
|
- params[key] = JSON.stringify(params[key])
|
|
|
- }
|
|
|
- return `${key}=${encodeURIComponent(params[key])}`
|
|
|
- })
|
|
|
- .join('&')
|
|
|
-
|
|
|
-export async function request(
|
|
|
- endpoint: string,
|
|
|
- config: {
|
|
|
- method: Method
|
|
|
- // baseURL: string
|
|
|
- // endpoint: string
|
|
|
- headers?: RawAxiosRequestHeaders
|
|
|
- data?: GenericValue | GenericValue[]
|
|
|
- withCredentials?: boolean
|
|
|
- }
|
|
|
-) {
|
|
|
- const { method, headers, data } = config
|
|
|
- const options: AxiosRequestConfig = {
|
|
|
- method,
|
|
|
- url: endpoint,
|
|
|
- baseURL,
|
|
|
- headers: headers ?? {}
|
|
|
- }
|
|
|
- if (baseURL.startsWith('/')) {
|
|
|
- options.headers!['browser-id'] = getBrowserId()
|
|
|
- }
|
|
|
- if (
|
|
|
- import.meta.env.NODE_ENV !== 'production' &&
|
|
|
- !baseURL.includes('api.n8n.io') &&
|
|
|
- !baseURL.includes('n8n.cloud')
|
|
|
- ) {
|
|
|
- options.withCredentials = options.withCredentials ?? true
|
|
|
- }
|
|
|
- if (['POST', 'PATCH', 'PUT'].includes(method)) {
|
|
|
- options.data = data
|
|
|
- } else if (data) {
|
|
|
- options.params = data
|
|
|
- options.paramsSerializer = legacyParamSerializer
|
|
|
- }
|
|
|
-
|
|
|
- try {
|
|
|
- const response = await axios.request(options)
|
|
|
- return response.data
|
|
|
- } catch (error) {
|
|
|
- if (error.message === 'Network Error') {
|
|
|
- throw new ResponseError("Can't connect to n8n.", {
|
|
|
- errorCode: NO_NETWORK_ERROR_CODE
|
|
|
- })
|
|
|
- }
|
|
|
-
|
|
|
- const errorResponseData = error.response?.data
|
|
|
- if (errorResponseData?.mfaRequired === true) {
|
|
|
- // throw new MfaRequiredError();
|
|
|
- throw errorResponseData
|
|
|
- }
|
|
|
- if (errorResponseData?.message !== undefined) {
|
|
|
- if (errorResponseData.name === 'NodeApiError') {
|
|
|
- errorResponseData.httpStatusCode = error.response.status
|
|
|
- throw errorResponseData
|
|
|
- }
|
|
|
-
|
|
|
- throw new ResponseError(errorResponseData.message, {
|
|
|
- errorCode: errorResponseData.code,
|
|
|
- httpStatusCode: error.response.status,
|
|
|
- stack: errorResponseData.stack,
|
|
|
- meta: errorResponseData.meta
|
|
|
- })
|
|
|
- }
|
|
|
-
|
|
|
- throw error
|
|
|
- }
|
|
|
+// 重试配置
|
|
|
+export interface RetryConfig {
|
|
|
+ retries?: number
|
|
|
+ retryDelay?: number
|
|
|
+ retryCondition?: (error: AxiosError) => boolean
|
|
|
}
|
|
|
|
|
|
-/**
|
|
|
- * Sends a request to the API and returns the response without extracting the data key.
|
|
|
- * @param context Rest API context
|
|
|
- * @param method HTTP method
|
|
|
- * @param endpoint relative path to the API endpoint
|
|
|
- * @param data request data
|
|
|
- * @returns data and total count
|
|
|
- */
|
|
|
-export async function getFullApiResponse<T>(
|
|
|
- context: IRestApiContext,
|
|
|
- method: Method,
|
|
|
- endpoint: string,
|
|
|
- data?: GenericValue | GenericValue[]
|
|
|
-) {
|
|
|
- const response = await request({
|
|
|
- method,
|
|
|
- baseURL: context.baseUrl,
|
|
|
- endpoint,
|
|
|
- headers: { 'push-ref': context.pushRef },
|
|
|
- data
|
|
|
- })
|
|
|
-
|
|
|
- return response as { count: number; data: T }
|
|
|
+// 拦截器配置类型
|
|
|
+interface InterceptorsConfig {
|
|
|
+ requestInterceptor?: (config: InternalAxiosRequestConfig) => InternalAxiosRequestConfig
|
|
|
+ requestErrorInterceptor?: (error: AxiosError) => Promise<any>
|
|
|
+ responseInterceptor?: (response: AxiosResponse<ResponseData<any>>) => any
|
|
|
+ responseErrorInterceptor?: (error: AxiosError) => Promise<any>
|
|
|
}
|
|
|
|
|
|
-export async function makeRestApiRequest<T>(
|
|
|
- context: IRestApiContext,
|
|
|
- method: Method,
|
|
|
- endpoint: string,
|
|
|
- data?: GenericValue | GenericValue[]
|
|
|
-) {
|
|
|
- const response = await request({
|
|
|
- method,
|
|
|
- baseURL: context.baseUrl,
|
|
|
- endpoint,
|
|
|
- headers: { 'push-ref': context.pushRef },
|
|
|
- data
|
|
|
- })
|
|
|
-
|
|
|
- // All cli rest api endpoints return data wrapped in `data` key
|
|
|
- return response.data as T
|
|
|
-}
|
|
|
+// 请求唯一键
|
|
|
+type RequestKey = string | symbol
|
|
|
|
|
|
-export async function get(
|
|
|
- baseURL: string,
|
|
|
- endpoint: string,
|
|
|
- params?: IDataObject,
|
|
|
- headers?: RawAxiosRequestHeaders
|
|
|
-) {
|
|
|
- return await request({ method: 'GET', baseURL, endpoint, headers, data: params })
|
|
|
+/**
|
|
|
+ * 获取url参数
|
|
|
+ * @param url
|
|
|
+ * @returns
|
|
|
+ */
|
|
|
+export function getParams(url: string) {
|
|
|
+ const paramsString = url.split('?')[1]
|
|
|
+ const searchParams = new URLSearchParams(paramsString)
|
|
|
+ return Object.fromEntries(searchParams.entries())
|
|
|
}
|
|
|
|
|
|
-export async function post(
|
|
|
- baseURL: string,
|
|
|
- endpoint: string,
|
|
|
- params?: IDataObject,
|
|
|
- headers?: RawAxiosRequestHeaders
|
|
|
-) {
|
|
|
- return await request({ method: 'POST', baseURL, endpoint, headers, data: params })
|
|
|
+class HttpClient {
|
|
|
+ private instance: AxiosInstance
|
|
|
+ private requestInterceptorId?: number
|
|
|
+ private responseInterceptorId?: number
|
|
|
+ private abortControllers: Map<RequestKey, AbortController> = new Map()
|
|
|
+
|
|
|
+ constructor(customConfig?: AxiosRequestConfig, interceptors?: InterceptorsConfig) {
|
|
|
+ this.instance = Axios.create({ ...defaultConfig, ...customConfig })
|
|
|
+ this.initInterceptors(interceptors)
|
|
|
+ }
|
|
|
+
|
|
|
+ private initInterceptors(interceptors?: InterceptorsConfig): void {
|
|
|
+ this.initRequestInterceptor(
|
|
|
+ interceptors?.requestInterceptor,
|
|
|
+ interceptors?.requestErrorInterceptor
|
|
|
+ )
|
|
|
+ this.initResponseInterceptor(
|
|
|
+ interceptors?.responseInterceptor,
|
|
|
+ interceptors?.responseErrorInterceptor
|
|
|
+ )
|
|
|
+ }
|
|
|
+
|
|
|
+ private initRequestInterceptor(
|
|
|
+ customInterceptor?: InterceptorsConfig['requestInterceptor'],
|
|
|
+ customErrorInterceptor?: InterceptorsConfig['requestErrorInterceptor']
|
|
|
+ ): void {
|
|
|
+ const defaultInterceptor = (config: InternalAxiosRequestConfig): InternalAxiosRequestConfig => {
|
|
|
+ const search = getParams(window.location.href)
|
|
|
+ const enterpriseCode = search?.['enterpriseCode']
|
|
|
+ const token =
|
|
|
+ localStorage.getItem('token_' + enterpriseCode) ||
|
|
|
+ document.cookie.match(new RegExp('(^| )' + 'x-sessionId_b' + '=([^;]*)(;|$)'))?.[2]
|
|
|
+
|
|
|
+ // 添加token
|
|
|
+ if (token) {
|
|
|
+ if (!config.headers) {
|
|
|
+ config.headers = {} as InternalAxiosRequestConfig['headers']
|
|
|
+ }
|
|
|
+ config.headers.Authorization = token
|
|
|
+ }
|
|
|
+
|
|
|
+ return config
|
|
|
+ }
|
|
|
+
|
|
|
+ // 默认请求错误拦截器
|
|
|
+ const defaultErrorInterceptor = (error: AxiosError): Promise<any> => {
|
|
|
+ // todo 错误处理
|
|
|
+
|
|
|
+ return Promise.reject(error)
|
|
|
+ }
|
|
|
+
|
|
|
+ this.requestInterceptorId = this.instance.interceptors.request.use(
|
|
|
+ customInterceptor || defaultInterceptor,
|
|
|
+ customErrorInterceptor || defaultErrorInterceptor
|
|
|
+ )
|
|
|
+ }
|
|
|
+
|
|
|
+ private initResponseInterceptor(
|
|
|
+ customInterceptor?: InterceptorsConfig['responseInterceptor'],
|
|
|
+ customErrorInterceptor?: InterceptorsConfig['responseErrorInterceptor']
|
|
|
+ ): void {
|
|
|
+ this.responseInterceptorId = this.instance.interceptors.response.use(
|
|
|
+ customInterceptor,
|
|
|
+ customErrorInterceptor
|
|
|
+ )
|
|
|
+ }
|
|
|
+
|
|
|
+ private getRequestKey(config: AxiosRequestConfig): RequestKey | undefined {
|
|
|
+ if (!config.url) return undefined
|
|
|
+ return `${config.method?.toUpperCase()}-${config.url}`
|
|
|
+ }
|
|
|
+
|
|
|
+ private setupCancelController(
|
|
|
+ config: AxiosRequestConfig,
|
|
|
+ requestKey?: RequestKey
|
|
|
+ ): AxiosRequestConfig {
|
|
|
+ const key = requestKey || this.getRequestKey(config)
|
|
|
+ if (!key) return config
|
|
|
+
|
|
|
+ // 如果已有相同key的请求,先取消它
|
|
|
+ this.cancelRequest(key)
|
|
|
+
|
|
|
+ const controller = new AbortController()
|
|
|
+ this.abortControllers.set(key, controller)
|
|
|
+
|
|
|
+ return {
|
|
|
+ ...config,
|
|
|
+ signal: controller.signal
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ public removeRequestInterceptor(): void {
|
|
|
+ if (this.requestInterceptorId !== undefined) {
|
|
|
+ this.instance.interceptors.request.eject(this.requestInterceptorId)
|
|
|
+ this.requestInterceptorId = undefined // 重置ID,避免重复移除
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ public removeResponseInterceptor(): void {
|
|
|
+ if (this.responseInterceptorId !== undefined) {
|
|
|
+ this.instance.interceptors.response.eject(this.responseInterceptorId)
|
|
|
+ this.responseInterceptorId = undefined // 重置ID,避免重复移除
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ public setRequestInterceptor(
|
|
|
+ customInterceptor?: InterceptorsConfig['requestInterceptor'],
|
|
|
+ customErrorInterceptor?: InterceptorsConfig['requestErrorInterceptor']
|
|
|
+ ): void {
|
|
|
+ this.removeRequestInterceptor()
|
|
|
+ this.initRequestInterceptor(customInterceptor, customErrorInterceptor)
|
|
|
+ }
|
|
|
+
|
|
|
+ public setResponseInterceptor(
|
|
|
+ customInterceptor?: InterceptorsConfig['responseInterceptor'],
|
|
|
+ customErrorInterceptor?: InterceptorsConfig['responseErrorInterceptor']
|
|
|
+ ): void {
|
|
|
+ this.removeResponseInterceptor()
|
|
|
+ this.initResponseInterceptor(customInterceptor, customErrorInterceptor)
|
|
|
+ }
|
|
|
+
|
|
|
+ public getInstance(): AxiosInstance {
|
|
|
+ return this.instance
|
|
|
+ }
|
|
|
+
|
|
|
+ public cancelRequest(key: RequestKey, message?: string): boolean {
|
|
|
+ const controller = this.abortControllers.get(key)
|
|
|
+ if (controller) {
|
|
|
+ controller.abort(message || `取消请求: ${String(key)}`)
|
|
|
+ this.abortControllers.delete(key)
|
|
|
+ return true
|
|
|
+ }
|
|
|
+ return false
|
|
|
+ }
|
|
|
+
|
|
|
+ public cancelAllRequests(message?: string): void {
|
|
|
+ this.abortControllers.forEach((controller, key) => {
|
|
|
+ controller.abort(message || `取消所有请求: ${String(key)}`)
|
|
|
+ })
|
|
|
+ this.abortControllers.clear()
|
|
|
+ }
|
|
|
+
|
|
|
+ public static isCancel(error: unknown): boolean {
|
|
|
+ return Axios.isCancel(error)
|
|
|
+ }
|
|
|
+
|
|
|
+ private sleep(ms: number): Promise<void> {
|
|
|
+ return new Promise((resolve) => setTimeout(resolve, ms))
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 通用请求方法
|
|
|
+ * @param url 请求地址
|
|
|
+ * @param config 请求配置
|
|
|
+ * @returns 响应数据
|
|
|
+ */
|
|
|
+ public async request<T = any>(
|
|
|
+ url: string,
|
|
|
+ config?: AxiosRequestConfig & { requestKey?: RequestKey; retry?: RetryConfig }
|
|
|
+ ): Promise<ResponseData<T>> {
|
|
|
+ const { requestKey, retry, method, ...restConfig } = config || {}
|
|
|
+
|
|
|
+ const defaultRetryCondition = (error: AxiosError) => {
|
|
|
+ // 默认只重试网络错误或5xx服务器错误
|
|
|
+ return !error.response || (error.response.status >= 500 && error.response.status < 600)
|
|
|
+ }
|
|
|
+
|
|
|
+ const retryConfig = {
|
|
|
+ retries: 0,
|
|
|
+ retryDelay: 1000,
|
|
|
+ retryCondition: defaultRetryCondition,
|
|
|
+ ...retry
|
|
|
+ }
|
|
|
+
|
|
|
+ let lastError: any
|
|
|
+ const key = requestKey || this.getRequestKey({ ...restConfig, method, url })
|
|
|
+
|
|
|
+ for (let attempt = 0; attempt <= retryConfig.retries; attempt++) {
|
|
|
+ try {
|
|
|
+ if (attempt > 0 && key) {
|
|
|
+ this.abortControllers.delete(key)
|
|
|
+ }
|
|
|
+
|
|
|
+ const requestConfig = this.setupCancelController({ ...restConfig, method, url }, requestKey)
|
|
|
+
|
|
|
+ const response = await this.instance.request<ResponseData<T>>(requestConfig)
|
|
|
+
|
|
|
+ return response.data
|
|
|
+ } catch (error) {
|
|
|
+ lastError = error
|
|
|
+ if (
|
|
|
+ attempt === retryConfig.retries ||
|
|
|
+ !retryConfig.retryCondition(error as AxiosError) ||
|
|
|
+ HttpClient.isCancel(error)
|
|
|
+ ) {
|
|
|
+ break
|
|
|
+ }
|
|
|
+
|
|
|
+ // 延迟后重试
|
|
|
+ if (retryConfig.retryDelay > 0) {
|
|
|
+ await this.sleep(retryConfig.retryDelay)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return Promise.reject(lastError)
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * GET 请求
|
|
|
+ * @param url 请求地址
|
|
|
+ * @param config 请求配置
|
|
|
+ * @returns 响应数据
|
|
|
+ */
|
|
|
+ public get<T = any>(
|
|
|
+ url: string,
|
|
|
+ config?: AxiosRequestConfig & { requestKey?: RequestKey; retry?: RetryConfig }
|
|
|
+ ): Promise<ResponseData<T>> {
|
|
|
+ return this.request<T>(url, { ...config, method: 'GET' })
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * POST 请求
|
|
|
+ * @param url 请求地址
|
|
|
+ * @param data 请求数据
|
|
|
+ * @param config 请求配置
|
|
|
+ * @returns 响应数据
|
|
|
+ */
|
|
|
+ public post<T = any>(
|
|
|
+ url: string,
|
|
|
+ data?: any,
|
|
|
+ config?: AxiosRequestConfig & { requestKey?: RequestKey; retry?: RetryConfig }
|
|
|
+ ): Promise<ResponseData<T>> {
|
|
|
+ return this.request<T>(url, { ...config, data, method: 'POST' })
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * PUT 请求
|
|
|
+ * @param url 请求地址
|
|
|
+ * @param data 请求数据
|
|
|
+ * @param config 请求配置
|
|
|
+ * @returns 响应数据
|
|
|
+ */
|
|
|
+ public put<T = any>(
|
|
|
+ url: string,
|
|
|
+ data?: any,
|
|
|
+ config?: AxiosRequestConfig & { requestKey?: RequestKey; retry?: RetryConfig }
|
|
|
+ ): Promise<ResponseData<T>> {
|
|
|
+ return this.request<T>(url, { ...config, data, method: 'PUT' })
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * DELETE 请求
|
|
|
+ * @param url 请求地址
|
|
|
+ * @param config 请求配置
|
|
|
+ * @returns 响应数据
|
|
|
+ */
|
|
|
+ public delete<T = any>(
|
|
|
+ url: string,
|
|
|
+ config?: AxiosRequestConfig & { requestKey?: RequestKey; retry?: RetryConfig }
|
|
|
+ ): Promise<ResponseData<T>> {
|
|
|
+ return this.request<T>(url, { ...config, method: 'DELETE' })
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * PATCH 请求
|
|
|
+ * @param url 请求地址
|
|
|
+ * @param data 请求数据
|
|
|
+ * @param config 请求配置
|
|
|
+ * @returns 响应数据
|
|
|
+ */
|
|
|
+ public patch<T = any>(
|
|
|
+ url: string,
|
|
|
+ data?: any,
|
|
|
+ config?: AxiosRequestConfig & { requestKey?: RequestKey; retry?: RetryConfig }
|
|
|
+ ): Promise<ResponseData<T>> {
|
|
|
+ return this.request<T>(url, { ...config, data, method: 'PATCH' })
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
-export async function patch(
|
|
|
- baseURL: string,
|
|
|
- endpoint: string,
|
|
|
- params?: IDataObject,
|
|
|
- headers?: RawAxiosRequestHeaders
|
|
|
-) {
|
|
|
- return await request({ method: 'PATCH', baseURL, endpoint, headers, data: params })
|
|
|
-}
|
|
|
+const http = new HttpClient()
|
|
|
+// 导出request供api-service使用
|
|
|
+export const request = http.request.bind(http)
|
|
|
|
|
|
-export async function streamRequest<T extends object>(
|
|
|
- context: IRestApiContext,
|
|
|
- apiEndpoint: string,
|
|
|
- payload: object,
|
|
|
- onChunk?: (chunk: T) => void,
|
|
|
- onDone?: () => void,
|
|
|
- onError?: (e: Error) => void,
|
|
|
- separator = STREAM_SEPARATOR,
|
|
|
- abortSignal?: AbortSignal
|
|
|
-): Promise<void> {
|
|
|
- let onErrorOnce: ((e: Error) => void) | undefined = (e: Error) => {
|
|
|
- onErrorOnce = undefined
|
|
|
- onError?.(e)
|
|
|
- }
|
|
|
- const headers: Record<string, string> = {
|
|
|
- 'browser-id': getBrowserId(),
|
|
|
- 'Content-Type': 'application/json'
|
|
|
- }
|
|
|
- const assistantRequest: RequestInit = {
|
|
|
- headers,
|
|
|
- method: 'POST',
|
|
|
- credentials: 'include',
|
|
|
- body: JSON.stringify(payload),
|
|
|
- signal: abortSignal
|
|
|
- }
|
|
|
- try {
|
|
|
- const response = await fetch(`${context.baseUrl}${apiEndpoint}`, assistantRequest)
|
|
|
-
|
|
|
- if (response.body) {
|
|
|
- // Handle the streaming response
|
|
|
- const reader = response.body.getReader()
|
|
|
- const decoder = new TextDecoder('utf-8')
|
|
|
-
|
|
|
- let buffer = ''
|
|
|
-
|
|
|
- async function readStream() {
|
|
|
- const { done, value } = await reader.read()
|
|
|
- if (done) {
|
|
|
- if (response.ok) {
|
|
|
- onDone?.()
|
|
|
- } else {
|
|
|
- onErrorOnce?.(
|
|
|
- new ResponseError(response.statusText, {
|
|
|
- httpStatusCode: response.status
|
|
|
- })
|
|
|
- )
|
|
|
- }
|
|
|
- return
|
|
|
- }
|
|
|
- const chunk = decoder.decode(value)
|
|
|
- buffer += chunk
|
|
|
-
|
|
|
- const splitChunks = buffer.split(separator)
|
|
|
-
|
|
|
- buffer = ''
|
|
|
- for (const splitChunk of splitChunks) {
|
|
|
- if (splitChunk) {
|
|
|
- let data: T
|
|
|
- try {
|
|
|
- // data = jsonParse<T>(splitChunk, { errorMessage: 'Invalid json' });
|
|
|
- data = JSON.parse(splitChunk) as T
|
|
|
- } catch (e) {
|
|
|
- // incomplete json. append to buffer to complete
|
|
|
- buffer += splitChunk
|
|
|
-
|
|
|
- continue
|
|
|
- }
|
|
|
-
|
|
|
- try {
|
|
|
- if (response.ok) {
|
|
|
- // Call chunk callback if request was successful
|
|
|
- onChunk?.(data)
|
|
|
- } else {
|
|
|
- // Otherwise, call error callback
|
|
|
- const message = 'message' in data ? data.message : response.statusText
|
|
|
- onErrorOnce?.(
|
|
|
- new ResponseError(String(message), {
|
|
|
- httpStatusCode: response.status
|
|
|
- })
|
|
|
- )
|
|
|
- }
|
|
|
- } catch (e: unknown) {
|
|
|
- if (e instanceof Error) {
|
|
|
- onErrorOnce?.(e)
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
- await readStream()
|
|
|
- }
|
|
|
-
|
|
|
- // Start reading the stream
|
|
|
- await readStream()
|
|
|
- } else if (onErrorOnce) {
|
|
|
- onErrorOnce(new Error(response.statusText))
|
|
|
- }
|
|
|
- } catch (e: unknown) {
|
|
|
- const condition = e instanceof Error
|
|
|
- if (!condition) {
|
|
|
- // eslint-disable-next-line n8n-local-rules/no-plain-errors
|
|
|
- throw new Error('Assertion failed')
|
|
|
- }
|
|
|
- onErrorOnce?.(e)
|
|
|
- }
|
|
|
-}
|
|
|
+export default http
|