update tsrest code (#5755)

* doc

* update tsrest code
This commit is contained in:
Archer
2025-10-12 00:04:51 +08:00
committed by archer
parent bd33873fcf
commit 8e07edb99d
27 changed files with 505 additions and 355 deletions

View File

@@ -0,0 +1,58 @@
import type { Handler } from '@fastgpt/global/common/tsRest/type';
import { RestAPI } from '@/service/middleware/entry';
import { request } from 'http';
import { FastGPTProUrl } from '@fastgpt/service/common/system/constants';
const handler: Handler<any> = async ({ req, res }) => {
return new Promise((resolve, reject) => {
try {
const requestPath = req.url || '';
if (!requestPath) {
throw new Error('url is empty');
}
if (!FastGPTProUrl) {
throw new Error(`未配置商业版链接: ${requestPath}`);
}
const parsedUrl = new URL(FastGPTProUrl);
// 删除敏感的 header
const requestHeaders = { ...req.headers };
delete requestHeaders?.rootkey;
const requestResult = request({
protocol: parsedUrl.protocol,
hostname: parsedUrl.hostname,
port: parsedUrl.port,
path: requestPath,
method: req.method,
headers: requestHeaders
});
req.pipe(requestResult);
requestResult.on('response', (response) => {
Object.keys(response.headers).forEach((key) => {
// @ts-ignore
res.setHeader(key, response.headers[key]);
});
response.statusCode && res.writeHead(response.statusCode);
response.pipe(res);
// 代理完成后 resolve
response.on('end', () => {
resolve({} as any);
});
});
requestResult.on('error', (e) => {
reject(e);
});
} catch (error) {
reject(error);
}
});
};
export const proApi = RestAPI(handler);

View File

@@ -0,0 +1,15 @@
import type { Handler } from '@fastgpt/global/common/tsRest/type';
import { RestAPI } from '@/service/middleware/entry';
import { authCert, clearCookie } from '@fastgpt/service/support/permission/auth/common';
import { delUserAllSession } from '@fastgpt/service/support/user/session';
import type { accountContract } from '@fastgpt/global/common/tsRest/fastgpt/contracts/support/user/account';
const handler: Handler<typeof accountContract.logout> = async ({ req, res }) => {
try {
const { userId } = await authCert({ req, authToken: true });
await delUserAllSession(userId);
} catch (error) {}
clearCookie(res);
};
export const loginout = RestAPI(handler);

View File

@@ -0,0 +1,31 @@
import { createServerRouter } from '@fastgpt/global/common/tsRest/fastgpt/router';
import { proApi } from '@/apiRouters/proApi';
import { loginout } from '@/apiRouters/support/user/acccount/loginout';
import { createServerRoute } from '@fastgpt/global/common/tsRest/fastgpt/router';
const router = createServerRoute({
core: {
chat: {
setting: {
favourite: {
list: proApi,
update: proApi,
delete: proApi,
order: proApi,
tags: proApi
},
detail: proApi,
update: proApi
}
}
},
support: {
user: {
account: {
loginout
}
}
}
});
export default createServerRouter(router);

View File

@@ -1,14 +0,0 @@
import type { NextApiRequest, NextApiResponse } from 'next';
import { NextAPI } from '@/service/middleware/entry';
import { authCert, clearCookie } from '@fastgpt/service/support/permission/auth/common';
import { delUserAllSession } from '@fastgpt/service/support/user/session';
async function handler(req: NextApiRequest, res: NextApiResponse<any>) {
try {
const { userId } = await authCert({ req, authToken: true });
await delUserAllSession(userId);
} catch (error) {}
clearCookie(res);
}
export default NextAPI(handler);

View File

@@ -1,11 +1,10 @@
import NextHead from '@/components/common/NextHead';
import { contract } from '@fastgpt/global/common/tsRest/fastgpt/contracts';
import { generateOpenApiDocument } from '@fastgpt/global/common/tsRest/openapi';
import { ApiReferenceReact } from '@scalar/api-reference-react';
import { fastgptOpenApiDocument } from '@fastgpt/global/common/tsRest/fastgpt/openapi';
export default function OpenAPI() {
const config = {
content: generateOpenApiDocument(contract)
content: fastgptOpenApiDocument
};
return (

View File

@@ -30,9 +30,9 @@ export function RestAPI<T extends AppRoute>(handler: Handler<T>): Endpoint<T> {
status: 200 as const,
body: {
code: 200,
message: 'success',
data: body,
statusText: 'success'
message: '',
statusText: ''
}
} as any;
} catch (error) {
@@ -41,7 +41,7 @@ export function RestAPI<T extends AppRoute>(handler: Handler<T>): Endpoint<T> {
// 使用统一的错误处理逻辑
const processedError = processError({
error,
url: `${url} (${duration}ms)`,
url,
defaultCode: 500
});

View File

@@ -0,0 +1,215 @@
import { createFastGPTClient } from '@fastgpt/global/common/tsRest/fastgpt/client';
import { TOKEN_ERROR_CODE } from '@fastgpt/global/common/error/errorCode';
import { getNanoid } from '@fastgpt/global/common/string/tools';
import type { ApiFetcherArgs } from '@ts-rest/core';
import { tsRestFetchApi } from '@ts-rest/core';
import { AnyResponseSchema } from '@fastgpt/global/common/tsRest/type';
import { ZodError } from 'zod';
import { getWebReqUrl, subRoute } from '@fastgpt/web/common/system/utils';
import { i18nT } from '@fastgpt/web/i18n/utils';
import { TeamErrEnum } from '@fastgpt/global/common/error/code/team';
import { useSystemStore } from '../system/useSystemStore';
import { clearToken } from '@/web/support/user/auth';
const queue = new Map<string, { id: string; controller: AbortController }[]>();
type BeforeFetchOptions = ApiFetcherArgs & { max?: number };
const beforeFetch = (
options: BeforeFetchOptions
):
| {
limit: { id: string; url: string; release: () => void };
}
| undefined => {
const checkMaxRequestLimitation = (options: {
url: string;
max: number;
}): {
id: string;
signal: AbortSignal;
release: () => void;
} => {
const { url, max } = options;
const id = getNanoid();
const controller = new AbortController();
const item = queue.get(url);
const current = item ?? [];
if (current.length >= max) {
const first = current.shift()!;
first.controller.abort();
}
current.push({ id, controller });
if (!item) queue.set(url, current);
const release = () => {
const item = queue.get(url);
if (!item) return;
const index = item.findIndex((item) => item.id === id);
if (index !== -1) {
item.splice(index, 1);
}
if (item.length <= 0) {
queue.delete(url);
}
};
return { id, signal: controller.signal, release };
};
const { max, ...args } = options;
if (!max || max <= 0) return;
const { id, signal, release } = checkMaxRequestLimitation({ url: args.path, max });
args.fetchOptions ??= {};
args.fetchOptions.signal = signal;
return {
limit: { id, url: args.path, release }
};
};
const afterFetch = (response: Awaited<ReturnType<typeof tsRestFetchApi>>) => {
if (response.status === 200) {
try {
const data = AnyResponseSchema.parse(response.body);
if (data === undefined) {
console.log('error->', data, 'data is empty');
return Promise.reject('服务器异常');
} else if (data.code < 200 || data.code >= 400) {
return Promise.reject(data);
}
response.body = data.data;
return response;
} catch (error) {
if (error instanceof ZodError) {
return Promise.reject(error.message);
}
return Promise.reject('Unknown error while intercept response');
}
} else {
return Promise.reject(response);
}
};
const requestFinish = (prepare?: ReturnType<typeof beforeFetch>) => {
prepare?.limit?.release?.();
};
const responseError = (err: any) => {
console.log('error->', '请求错误', err);
const isOutlinkPage = {
[`${subRoute}/chat/share`]: true,
[`${subRoute}/chat`]: true,
[`${subRoute}/login`]: true
}[window.location.pathname];
const data = err?.response?.data || err;
if (!err) {
return Promise.reject({ message: '未知错误' });
}
if (typeof err === 'string') {
return Promise.reject({ message: err });
}
if (typeof data === 'string') {
return Promise.reject(data);
}
// 有报错响应
if (data?.code in TOKEN_ERROR_CODE) {
if (!isOutlinkPage) {
clearToken();
window.location.replace(
getWebReqUrl(`/login?lastRoute=${encodeURIComponent(location.pathname + location.search)}`)
);
}
return Promise.reject({ message: i18nT('common:unauth_token') });
}
if (
data?.statusText &&
[
TeamErrEnum.aiPointsNotEnough,
TeamErrEnum.datasetSizeNotEnough,
TeamErrEnum.datasetAmountNotEnough,
TeamErrEnum.appAmountNotEnough,
TeamErrEnum.pluginAmountNotEnough,
TeamErrEnum.websiteSyncNotEnough,
TeamErrEnum.reRankNotEnough
].includes(data?.statusText) &&
!isOutlinkPage
) {
useSystemStore.getState().setNotSufficientModalType(data.statusText);
return Promise.reject(data);
}
return Promise.reject(data);
};
export const client = createFastGPTClient({
baseUrl: getWebReqUrl('/api'),
throwOnUnknownStatus: true,
validateResponse: false,
credentials: 'include',
baseHeaders: {
'Content-Type': 'application/json;charset=utf-8'
},
api: tsRestFetchApi
});
// Simplified types to reduce TS computation overhead
type AnyEndpointFn = (options?: any) => Promise<any>;
// Helper to infer response body type
type InferResponseBody<T extends AnyEndpointFn> =
Awaited<ReturnType<T>> extends { status: 200; body: infer B } ? B : never;
// Helper to infer options type
type InferOptions<T extends AnyEndpointFn> = NonNullable<Parameters<T>[0]>;
// Helper to extract body from options
type InferBody<T extends AnyEndpointFn> = InferOptions<T> extends { body: infer B } ? B : never;
// Helper to extract query from options
type InferQuery<T extends AnyEndpointFn> = InferOptions<T> extends { query: infer Q } ? Q : never;
// Combined params type
type Params<T extends AnyEndpointFn> = (InferBody<T> extends never ? {} : InferBody<T>) &
(InferQuery<T> extends never ? {} : InferQuery<T>);
// Additional options (excluding body and query)
type Options<T extends AnyEndpointFn> = Omit<InferOptions<T>, 'body' | 'query'>;
const call = async <T extends AnyEndpointFn>(
api: T,
options: InferOptions<T>
): Promise<InferResponseBody<T>> => {
const prepare = beforeFetch(options as any);
const res = await api(options)
.then(afterFetch)
.catch(responseError)
.finally(() => requestFinish(prepare));
return res.body as InferResponseBody<T>;
};
export const RestAPI = <T extends AnyEndpointFn>(
endpoint: T,
transform?: (params: Params<T>) => {
body?: InferBody<T> extends never ? any : InferBody<T>;
query?: InferQuery<T> extends never ? any : InferQuery<T>;
}
) => {
return (params?: Params<T>, options?: Options<T>): Promise<InferResponseBody<T>> => {
const transformedData = params && transform ? transform(params) : {};
const finalOptions = { ...options, ...transformedData } as InferOptions<T>;
return call(endpoint, finalOptions);
};
};

View File

@@ -30,7 +30,7 @@ import type {
GetCollectionQuoteProps,
GetCollectionQuoteRes
} from '@/pages/api/core/chat/quote/getCollectionQuote';
import { RestAPI, client } from '@fastgpt/global/common/tsRest/fastgpt/client';
import { RestAPI, client } from '../../common/api/client';
/**
* 获取初始化聊天内容
@@ -109,26 +109,26 @@ export const getCollectionQuote = (data: GetCollectionQuoteProps) =>
POST<GetCollectionQuoteRes>(`/core/chat/quote/getCollectionQuote`, data);
/*---------- chat setting ------------*/
export const getChatSetting = RestAPI(client.chat.setting.detail);
export const updateChatSetting = RestAPI(client.chat.setting.update, (params) => ({
export const getChatSetting = RestAPI(client.core.chat.setting.detail);
export const updateChatSetting = RestAPI(client.core.chat.setting.update, (params) => ({
body: params
}));
export const getFavouriteApps = RestAPI(client.chat.setting.favourite.list);
export const updateFavouriteApps = RestAPI(client.chat.setting.favourite.update, (params) => ({
export const getFavouriteApps = RestAPI(client.core.chat.setting.favourite.list);
export const updateFavouriteApps = RestAPI(client.core.chat.setting.favourite.update, (params) => ({
body: params
}));
export const updateFavouriteAppOrder = RestAPI(client.chat.setting.favourite.order, (params) => ({
body: params
}));
export const updateFavouriteAppTags = RestAPI(client.chat.setting.favourite.tags, (params) => ({
body: params
}));
export const deleteFavouriteApp = RestAPI(client.chat.setting.favourite.delete, (params) => ({
export const updateFavouriteAppOrder = RestAPI(
client.core.chat.setting.favourite.order,
(params) => ({
body: params
})
);
export const updateFavouriteAppTags = RestAPI(
client.core.chat.setting.favourite.tags,
(params) => ({
body: params
})
);
export const deleteFavouriteApp = RestAPI(client.core.chat.setting.favourite.delete, (params) => ({
query: params
}));

View File

@@ -16,6 +16,7 @@ import type {
} from '@fastgpt/global/support/user/login/api.d';
import type { preLoginResponse } from '@/pages/api/support/user/account/preLogin';
import type { WxLoginProps } from '@fastgpt/global/support/user/api.d';
import { client, RestAPI } from '@/web/common/api/client';
export const sendAuthCode = (data: {
username: string;
@@ -94,7 +95,7 @@ export const postLogin = ({ password, ...props }: PostLoginProps) =>
password: hashStr(password)
});
export const loginOut = () => GET('/support/user/account/loginout');
export const loginOut = RestAPI(client.support.user.account.logout);
export const putUserInfo = (data: UserUpdateParams) => PUT('/support/user/account/update', data);