first relase

This commit is contained in:
Lê đ tú 2025-10-17 11:11:14 +07:00
parent d3a42ba6e9
commit c6afce22ed
288 changed files with 55505 additions and 192 deletions

226
service/fetch.ts Normal file
View file

@ -0,0 +1,226 @@
import Toast from '@/components/base/toast';
import {
API_PREFIX,
APP_VERSION,
MARKETPLACE_API_PREFIX,
PUBLIC_API_PREFIX,
} from '@/config';
import type {
AfterResponseHook,
BeforeErrorHook,
BeforeRequestHook,
Hooks,
} from 'ky';
import ky from 'ky';
const TIME_OUT = 100000;
export const ContentType = {
json: 'application/json',
stream: 'text/event-stream',
audio: 'audio/mpeg',
form: 'application/x-www-form-urlencoded; charset=UTF-8',
download: 'application/octet-stream', // for download
downloadZip: 'application/zip', // for download
upload: 'multipart/form-data', // for upload
};
export type FetchOptionType = Omit<RequestInit, 'body'> & {
params?: Record<string, any>;
body?: BodyInit | Record<string, any> | null;
};
const afterResponse204: AfterResponseHook = async (
_request,
_options,
response,
) => {
if (response.status === 204) return Response.json({ result: 'success' });
};
export type ResponseError = {
code: string;
message: string;
status: number;
};
const afterResponseErrorCode = (
otherOptions: IOtherOptions,
): AfterResponseHook => {
return async (
_request: Request,
_options: RequestInit,
response: Response,
) => {
const clonedResponse = response.clone();
if (!/^([23])\d{2}$/.test(String(clonedResponse.status))) {
const bodyJson = clonedResponse.json() as Promise<ResponseError>;
switch (clonedResponse.status) {
case 403:
bodyJson.then((data: ResponseError) => {
if (!otherOptions.silent)
Toast.notify({ type: 'error', message: data.message });
if (data.code === 'already_setup')
globalThis.location.href = `${globalThis.location.origin}/signin`;
});
break;
case 401:
return Promise.reject(response);
// fall through
default:
bodyJson.then((data: ResponseError) => {
if (!otherOptions.silent)
Toast.notify({ type: 'error', message: data.message });
});
return Promise.reject(response);
}
}
};
};
const beforeErrorToast = (otherOptions: IOtherOptions): BeforeErrorHook => {
return (error) => {
if (!otherOptions.silent)
Toast.notify({ type: 'error', message: error.message });
return error;
};
};
export async function getAccessToken(isPublicAPI?: boolean) {
if (isPublicAPI) {
const sharedToken = globalThis.location.pathname.split('/').slice(-1)[0];
const userId = (await getProcessedSystemVariablesFromUrlParams()).user_id;
const accessToken =
localStorage.getItem('token') || JSON.stringify({ version: 2 });
let accessTokenJson: Record<string, any> = { version: 2 };
try {
accessTokenJson = JSON.parse(accessToken);
if (isTokenV1(accessTokenJson)) accessTokenJson = getInitialTokenV2();
} catch {}
return accessTokenJson[sharedToken]?.[userId || 'DEFAULT'];
} else {
return localStorage.getItem('console_token') || '';
}
}
const beforeRequestPublicAuthorization: BeforeRequestHook = async (request) => {
const token = await getAccessToken(true);
request.headers.set('Authorization', `Bearer ${token}`);
};
const beforeRequestAuthorization: BeforeRequestHook = async (request) => {
const accessToken = await getAccessToken();
request.headers.set('Authorization', `Bearer ${accessToken}`);
};
const baseHooks: Hooks = {
afterResponse: [afterResponse204],
};
const baseClient = ky.create({
hooks: baseHooks,
timeout: TIME_OUT,
});
export const getBaseOptions = (): RequestInit => ({
method: 'GET',
mode: 'cors',
credentials: 'include', // always send cookies、HTTP Basic authentication.
headers: new Headers({
'Content-Type': ContentType.json,
}),
redirect: 'follow',
});
async function base<T>(
url: string,
options: FetchOptionType = {},
otherOptions: IOtherOptions = {},
): Promise<T> {
const baseOptions = getBaseOptions();
const { params, body, headers, ...init } = Object.assign(
{},
baseOptions,
options,
);
const {
isPublicAPI = false,
isMarketplaceAPI = false,
bodyStringify = true,
needAllResponseContent,
deleteContentType,
getAbortController,
} = otherOptions;
let base: string;
if (isMarketplaceAPI) base = MARKETPLACE_API_PREFIX;
else if (isPublicAPI) base = PUBLIC_API_PREFIX;
else base = API_PREFIX;
if (getAbortController) {
const abortController = new AbortController();
getAbortController(abortController);
options.signal = abortController.signal;
}
const fetchPathname = base + (url.startsWith('/') ? url : `/${url}`);
if (deleteContentType) (headers as any).delete('Content-Type');
// ! For Marketplace API, help to filter tags added in new version
if (isMarketplaceAPI) (headers as any).set('X-Dify-Version', APP_VERSION);
const client = baseClient.extend({
hooks: {
...baseHooks,
beforeError: [
...(baseHooks.beforeError || []),
beforeErrorToast(otherOptions),
],
beforeRequest: [
...(baseHooks.beforeRequest || []),
isPublicAPI && beforeRequestPublicAuthorization,
!isPublicAPI && !isMarketplaceAPI && beforeRequestAuthorization,
].filter((h): h is BeforeRequestHook => Boolean(h)),
afterResponse: [
...(baseHooks.afterResponse || []),
afterResponseErrorCode(otherOptions),
],
},
});
const res = await client(fetchPathname, {
...init,
headers,
credentials: isMarketplaceAPI ? 'omit' : options.credentials || 'include',
retry: {
methods: [],
},
...(bodyStringify ? { json: body } : { body: body as BodyInit }),
searchParams: params,
fetch(resource: RequestInfo | URL, options?: RequestInit) {
if (resource instanceof Request && options) {
const mergedHeaders = new Headers(options.headers || {});
resource.headers.forEach((value, key) => {
mergedHeaders.append(key, value);
});
options.headers = mergedHeaders;
}
return globalThis.fetch(resource, options);
},
});
if (needAllResponseContent) return res as T;
const contentType = res.headers.get('content-type');
if (
contentType &&
[ContentType.download, ContentType.audio, ContentType.downloadZip].includes(
contentType,
)
)
return (await res.blob()) as T;
return (await res.json()) as T;
}
export { base };