feat: integrate ts-rest (#5741)

* feat: integrate ts-rest

* chore: classify core contract and pro contract

* chore: update lockfile

* chore: tweak dir structure

* chore: tweak dir structure
This commit is contained in:
伍闲犬
2025-10-11 20:49:59 +08:00
committed by archer
parent 45038dd732
commit bd33873fcf
38 changed files with 2627 additions and 177 deletions

View File

@@ -0,0 +1,172 @@
import { contract } from './contracts';
import { initClient, tsRestFetchApi } from '@ts-rest/core';
import { TOKEN_ERROR_CODE } from '../../error/errorCode';
import { getNanoid } from '../../string/tools';
import { type ApiFetcherArgs } from '@ts-rest/core';
import { AnyResponseSchema } from '../../type';
import { ZodError } from 'zod';
import { getWebReqUrl } from '../../../../web/common/system/utils';
export const client = initClient(contract, {
baseUrl: getWebReqUrl('/api'),
throwOnUnknownStatus: true,
validateResponse: false,
credentials: 'include',
baseHeaders: {
'Content-Type': 'application/json;charset=utf-8'
},
api: async (args: BeforeFetchOptions) => {
const prepare = beforeFetch(args);
const response = await tsRestFetchApi(args);
return afterFetch(response, prepare);
}
});
const WHITE_LIST = ['/chat/share', '/chat', '/login'];
async function isTokenExpired() {
if (WHITE_LIST.includes(window.location.pathname)) return;
await client.support.user.account.logout();
const lastRoute = encodeURIComponent(location.pathname + location.search);
window.location.replace(getWebReqUrl(`/login?lastRoute=${lastRoute}`));
}
export function checkBusinessCode(code: number) {
if (code in TOKEN_ERROR_CODE) {
isTokenExpired();
return;
}
}
type Item = { id: string; controller: AbortController };
const queue = new Map<string, Item[]>();
function 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 };
}
function checkHttpStatus(status: number): status is 200 {
if (status !== 200) return false;
return true;
}
type BeforeFetchOptions = ApiFetcherArgs & { max?: number };
function beforeFetch(options: BeforeFetchOptions):
| {
limit: { id: string; url: string; release: () => void };
}
| undefined {
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 }
};
}
function afterFetch(
response: Awaited<ReturnType<typeof tsRestFetchApi>>,
prepare?: ReturnType<typeof beforeFetch>
) {
if (checkHttpStatus(response.status)) {
try {
const body = AnyResponseSchema.parse(response.body);
response.body = body.data;
if (prepare?.limit) {
prepare.limit.release();
}
return response;
} catch (error) {
if (error instanceof ZodError) {
throw new Error(error.message);
}
throw new Error('Unknown error while intercept response');
}
} else {
throw new Error(`HTTP error, status: ${response.status}`);
}
}
type Client = typeof client;
type U<T> = { [K in keyof T]: T[K] extends (...args: any[]) => any ? T[K] : U<T[K]> }[keyof T];
export type Endpoints = U<Client>;
type _Options<T extends Endpoints> = NonNullable<Parameters<T>[0]>;
type ExtractBodySchema<T extends Endpoints> = 'body' extends keyof _Options<T>
? _Options<T>['body']
: never;
type ExtractQuerySchema<T extends Endpoints> = 'query' extends keyof _Options<T>
? _Options<T>['query']
: never;
export type Params<T extends Endpoints> = (ExtractBodySchema<T> extends never
? {}
: ExtractBodySchema<T>) &
(ExtractQuerySchema<T> extends never ? {} : ExtractQuerySchema<T>);
export type Options<T extends Endpoints> = Omit<_Options<T>, 'body' | 'query'>;
type Body<T extends Endpoints> = Extract<Awaited<ReturnType<T>>, { status: 200 }>['body'];
type RestAPIResult<T extends Endpoints> = Body<T>;
const call = async <T extends Endpoints>(
api: T,
options: _Options<T>
): Promise<RestAPIResult<T>> => {
const res = await api(options as any);
if (res.status !== 200) {
throw new Error(`Unexpected status: ${res.status}`);
}
return res.body as RestAPIResult<T>;
};
export const RestAPI = <T extends Endpoints>(
endpoint: T,
transform?: (params: Params<T>) => {
body?: ExtractBodySchema<T> extends never ? any : ExtractBodySchema<T>;
query?: ExtractQuerySchema<T> extends never ? any : ExtractQuerySchema<T>;
}
) => {
return (params?: Params<T>, options?: Options<T>) => {
const transformedData = params && transform ? transform(params) : {};
const finalOptions = { ...options, ...transformedData } as _Options<T>;
return call(endpoint, finalOptions);
};
};

View File

@@ -0,0 +1,6 @@
import { settingContract } from './setting';
import { c } from '../../../init';
export const chatContract = c.router({
setting: settingContract
});

View File

@@ -0,0 +1,94 @@
import { ObjectIdSchema } from '../../../../../../type';
import {
ChatFavouriteAppResponseItemSchema,
ChatFavouriteAppUpdateSchema
} from '../../../../../../../core/chat/favouriteApp/type';
import { c } from '../../../../../init';
import { z } from 'zod';
export const favouriteContract = c.router({
list: {
path: '/proApi/core/chat/setting/favourite/list',
method: 'GET',
query: z.object({
name: z.string().optional().openapi({ example: 'FastGPT' }),
tag: z.string().optional().openapi({ example: 'i7Ege2W2' })
}),
responses: {
200: z.array(ChatFavouriteAppResponseItemSchema)
},
metadata: {
tags: ['chat']
},
description: '获取精选应用列表',
summary: '获取精选应用列表'
},
update: {
path: '/proApi/core/chat/setting/favourite/update',
method: 'PUT',
body: ChatFavouriteAppUpdateSchema,
responses: {
200: c.type<void>()
},
metadata: {
tags: ['chat']
},
description: '更新精选应用',
summary: '更新精选应用'
},
delete: {
path: '/proApi/core/chat/setting/favourite/delete',
method: 'DELETE',
query: z.object({
id: ObjectIdSchema
}),
responses: {
200: c.type<void>()
},
metadata: {
tags: ['chat']
},
description: '删除精选应用',
summary: '删除精选应用'
},
order: {
path: '/proApi/core/chat/setting/favourite/order',
method: 'PUT',
body: z.array(
z.object({
id: ObjectIdSchema,
order: z.number()
})
),
responses: {
200: c.type<void>()
},
metadata: {
tags: ['chat']
},
description: '更新精选应用顺序',
summary: '更新精选应用顺序'
},
tags: {
path: '/proApi/core/chat/setting/favourite/tags',
method: 'PUT',
body: z.array(
z.object({
id: z.string(),
tags: z.array(z.string())
})
),
responses: {
200: c.type<void>()
},
metadata: {
tags: ['chat']
},
description: '更新精选应用标签',
summary: '更新精选应用标签'
}
});

View File

@@ -0,0 +1,37 @@
import {
ChatSettingResponseSchema,
ChatSettingSchema
} from '../../../../../../core/chat/setting/type';
import { c } from '../../../../init';
import { favouriteContract } from './favourite';
export const settingContract = c.router({
favourite: favouriteContract,
detail: {
path: '/proApi/core/chat/setting/detail',
method: 'GET',
responses: {
200: ChatSettingResponseSchema
},
metadata: {
tags: ['chat']
},
description: '获取聊天设置',
summary: '获取聊天设置'
},
update: {
path: '/proApi/core/chat/setting/update',
method: 'PUT',
body: ChatSettingSchema.partial(),
responses: {
200: c.type<void>()
},
metadata: {
tags: ['chat']
},
description: '更新聊天设置',
summary: '更新聊天设置'
}
});

View File

@@ -0,0 +1,14 @@
import { supportContract } from './support';
import { chatContract } from './chat';
import { c } from '../../init';
// 前端使用的完整合约(开源 + Pro
// FastGPT 后端使用的合约
export const contract = c.router({
chat: {
...chatContract
},
support: {
...supportContract
}
});

View File

@@ -0,0 +1,6 @@
import { userContract } from './user';
import { c } from '../../../init';
export const supportContract = c.router({
user: userContract
});

View File

@@ -0,0 +1,17 @@
import { c } from '../../../../../init';
export const accountContract = c.router({
logout: {
path: '/support/user/account/login',
method: 'POST',
body: c.type<undefined>(),
responses: {
200: c.type<void>()
},
metadata: {
tags: ['support']
},
description: '退出登录',
summary: '退出登录'
}
});

View File

@@ -0,0 +1,6 @@
import { accountContract } from '../../../../fastgpt/contracts/support/user/account';
import { c } from '../../../../init';
export const userContract = c.router({
account: accountContract
});

View File

@@ -0,0 +1,21 @@
import { createNextRouter } from '@ts-rest/next';
import { createNextRoute } from '@ts-rest/next';
import { contract } from './contracts';
/**
* 创建 FastGPT 单个路由
*/
export function createServerRoute(
implementation: Parameters<typeof createNextRoute<typeof contract>>[1]
) {
return createNextRoute(contract, implementation);
}
/**
* 创建 FastGPT 路由器
*/
export function createServerRouter(
router: Parameters<typeof createNextRouter<typeof contract>>[1]
) {
return createNextRouter(contract, router);
}

View File

@@ -0,0 +1,55 @@
import { chatContract } from '../../fastgpt/contracts/chat';
import { c } from '../../init';
// 通过 FastGPT 后端转发到 Pro 后端使用的合约
const transformedProContract = c.router({
chat: transformPaths(chatContract)
});
// Pro 后端独有的接口
const proOnlyContract = c.router({
// TODO
// admin: adminContract,
});
// 最终的 Pro 合约 = 转换后的 Pro 接口 + Pro 后端独有的接口
// Pro 后端使用的合约
export const proContract = c.router({
...transformedProContract,
...proOnlyContract
});
/**
* 转换路径前缀
* 将 /proApi 替换为空字符串,用于 Pro 后端
*/
function transformPaths<T extends Record<string, any>>(
router: T,
removePrefix: string = '/proApi',
replaceWith: string = ''
): T {
const transform = (obj: any): any => {
if (typeof obj !== 'object' || obj === null) return obj;
// 如果是路由定义(有 path 属性)
if ('path' in obj && typeof obj.path === 'string') {
return {
...obj,
path: obj.path.replace(removePrefix, replaceWith),
metadata: {
...obj.metadata,
originalPath: obj.path
}
};
}
// 递归处理嵌套的路由
const result: any = {};
for (const key in obj) {
result[key] = transform(obj[key]);
}
return result;
};
return transform(router) as T;
}

View File

@@ -0,0 +1,21 @@
import { proContract } from './contracts';
import { createNextRoute, createNextRouter } from '@ts-rest/next';
/**
* 创建 Pro 单个路由
*/
export function createProServerRoute(
implementation: Parameters<typeof createNextRoute<typeof proContract>>[1]
) {
return createNextRoute(proContract, implementation);
}
/**
* 创建 Pro 路由器
* 只需实现 Pro 接口(路径已自动转换 /proApi → 空)
*/
export function createProServerRouter(
router: Parameters<typeof createNextRouter<typeof proContract>>[1]
) {
return createNextRouter(proContract, router);
}

View File

@@ -0,0 +1,3 @@
import { initContract } from '@ts-rest/core';
export const c = initContract();

View File

@@ -0,0 +1,34 @@
import type { contract } from './fastgpt/contracts';
import { generateOpenApi } from '@ts-rest/open-api';
const hasCustomTags = (metadata: unknown): metadata is { tags: string[] } => {
return !!metadata && typeof metadata === 'object' && 'tags' in metadata;
};
export type OpenAPIObject = ReturnType<typeof generateOpenApi>;
export function generateOpenApiDocument(c: typeof contract): OpenAPIObject {
return generateOpenApi(
c,
{
info: {
title: 'FastGPT OpenAPI',
version: '4.12.4',
description: 'FastGPT OpenAPI'
},
servers: [{ url: '/api' }]
},
{
operationMapper(operation, appRoute) {
return {
...operation,
...(hasCustomTags(appRoute.metadata)
? {
tags: appRoute.metadata.tags
}
: {})
};
},
setOperationId: false
}
);
}

View File

@@ -0,0 +1,14 @@
import type { AppRoute } from '@ts-rest/core';
import type { createSingleRouteHandler } from '@ts-rest/next';
export type { AppRoute } from '@ts-rest/core';
export type Endpoint<T extends AppRoute> = Parameters<typeof createSingleRouteHandler<T>>[1];
export type Args<T extends AppRoute> = Parameters<Endpoint<T>>[0];
type Result<T extends AppRoute> = Awaited<ReturnType<Endpoint<T>>>;
type Ok<T extends AppRoute> = Extract<Result<T>, { status: 200 }>;
type Response<T extends AppRoute> =
Ok<T> extends { body: infer B } ? (B extends { data: infer D } ? D : B) : never;
export type Handler<T extends AppRoute> = (args: Args<T>) => Promise<Response<T>>;

View File

@@ -0,0 +1,45 @@
import { extendZodWithOpenApi } from '@anatine/zod-openapi';
import { z } from 'zod';
extendZodWithOpenApi(z);
export const createCommonResponseSchema = <T extends z.ZodTypeAny>(schema: T) =>
z
.object({
code: z.number().describe('状态码'),
message: z.string().describe('消息'),
data: schema.describe('数据'),
statusText: z.string().describe('状态文本')
})
.describe('通用响应')
.openapi({
example: {
code: 200,
data: null,
message: 'Success',
statusText: 'Success'
}
});
export const ObjectIdSchema = z
.string()
.length(24)
.describe('MongoDB ObjectId 格式的字符串')
.openapi({ example: '6894728240dc458ece573294' });
export type ObjectIdType = z.infer<typeof ObjectIdSchema>;
export const ParentIdSchema = ObjectIdSchema.nullish()
.openapi({ example: null })
.describe('父级ID, 可以是 ObjectId、null 或 undefined(表示根级)');
export type ParentIdType = z.infer<typeof ParentIdSchema>;
export const DateTimeSchema = z.date().describe('ISO 8601 格式的时间字符串');
export type DateTimeType = z.infer<typeof DateTimeSchema>;
export const OptionalDateTimeSchema = DateTimeSchema.nullish()
.openapi({ example: null })
.describe('ISO 8601 格式的时间字符串, 可以是 null 或 undefined');
export type OptionalDateTimeType = z.infer<typeof OptionalDateTimeSchema>;
// 任意类型数据的响应
export const AnyResponseSchema = createCommonResponseSchema(z.any());

View File

@@ -1,18 +0,0 @@
export type ChatFavouriteAppSchema = {
_id: string;
teamId: string;
appId: string;
favouriteTags: string[]; // tag id list
order: number;
};
export type ChatFavouriteAppUpdateParams = {
appId: string;
order: number;
};
export type ChatFavouriteApp = ChatFavouriteAppSchema & {
name: string;
avatar: string;
intro: string;
};

View File

@@ -0,0 +1,34 @@
import { z } from 'zod';
import { extendZodWithOpenApi } from '@anatine/zod-openapi';
import { ObjectIdSchema } from '../../../common/type';
extendZodWithOpenApi(z);
export const ChatFavouriteTagSchema = z.object({
id: z.string().openapi({ example: 'i7Ege2W2' }),
name: z.string().openapi({ example: '游戏' })
});
export type ChatFavouriteTagType = z.infer<typeof ChatFavouriteTagSchema>;
export const ChatFavouriteAppSchema = z.object({
_id: ObjectIdSchema,
teamId: ObjectIdSchema,
appId: ObjectIdSchema,
favouriteTags: z.array(z.string()).openapi({ example: ['i7Ege2W2', 'i7Ege2W3'] }),
order: z.number().openapi({ example: 1 })
});
export type ChatFavouriteAppType = z.infer<typeof ChatFavouriteAppSchema>;
export const ChatFavouriteAppResponseItemSchema = z.object({
...ChatFavouriteAppSchema.shape,
name: z.string().openapi({ example: 'FastGPT' }),
intro: z.string().openapi({ example: 'FastGPT' }),
avatar: z.string().openapi({ example: 'https://fastgpt.com/avatar.png' })
});
export type ChatFavouriteAppResponseItemType = z.infer<typeof ChatFavouriteAppResponseItemSchema>;
export const ChatFavouriteAppUpdateSchema = z.array(
ChatFavouriteAppSchema.pick({ appId: true, order: true })
);
export type ChatFavouriteAppUpdateType = z.infer<typeof ChatFavouriteAppUpdateSchema>;

View File

@@ -1,37 +0,0 @@
export type ChatSettingSchema = {
_id: string;
appId: string;
teamId: string;
slogan: string;
dialogTips: string;
enableHome: boolean;
homeTabTitle: string;
wideLogoUrl?: string;
squareLogoUrl?: string;
selectedTools: {
pluginId: string;
inputs?: Record<`${NodeInputKeyEnum}` | string, any>;
}[];
quickAppIds: string[];
favouriteTags: {
id: string;
name: string;
}[];
};
export type ChatSettingUpdateParams = Partial<Omit<ChatSettingSchema, '_id' | 'appId' | 'teamId'>>;
export type QuickAppType = { _id: string; name: string; avatar: string };
export type ChatFavouriteTagType = ChatSettingSchema['favouriteTags'][number];
export type SelectedToolType = ChatSettingSchema['selectedTools'][number] & {
name: string;
avatar: string;
};
export type ChatSettingReturnType =
| (Omit<ChatSettingSchema, 'quickAppIds' | 'selectedTools'> & {
quickAppList: QuickAppType[];
selectedTools: SelectedToolType[];
})
| undefined;

View File

@@ -0,0 +1,58 @@
import { z } from 'zod';
import { extendZodWithOpenApi } from '@anatine/zod-openapi';
import { ObjectIdSchema } from '../../../common/type';
import { ChatFavouriteTagSchema } from '../favouriteApp/type';
extendZodWithOpenApi(z);
export const ChatQuickAppSchema = z.object({
_id: ObjectIdSchema,
name: z.string().openapi({ example: 'FastGPT' }),
avatar: z.string().openapi({ example: 'https://fastgpt.com/avatar.png' })
});
export type ChatQuickAppType = z.infer<typeof ChatQuickAppSchema>;
export const ChatSelectedToolSchema = z.object({
pluginId: z.string().openapi({ example: 'systemTool-getTime' }),
inputs: z.record(z.string(), z.any()).optional(),
name: z.string().openapi({ example: 'FastGPT' }),
avatar: z.string().openapi({ example: 'https://fastgpt.com/avatar.png' })
});
export type ChatSelectedToolType = z.infer<typeof ChatSelectedToolSchema>;
export const ChatSettingSchema = z.object({
_id: ObjectIdSchema,
appId: ObjectIdSchema,
teamId: ObjectIdSchema,
slogan: z.string().openapi({ example: '你好👋,我是 FastGPT ! 请问有什么可以帮你?' }),
dialogTips: z.string().openapi({ example: '你可以问我任何问题' }),
enableHome: z.boolean().openapi({ example: true }),
homeTabTitle: z.string().openapi({ example: '首页' }),
wideLogoUrl: z.string().optional().openapi({
example:
'/api/system/img/avatar/686f319cbb6eea8840884338/2025_09_30/9f77bd74be89c71cd3b97bd2fb8b2c05_DeviconMysql.png'
}),
squareLogoUrl: z.string().optional().openapi({
example:
'/api/system/img/avatar/686f319cbb6eea8840884338/2025_09_30/9f77bd74be89c71cd3b97bd2fb8b2c05_DeviconMysql.png'
}),
selectedTools: z
.array(ChatSelectedToolSchema.pick({ pluginId: true, inputs: true }))
.openapi({ example: [{ pluginId: 'systemTool-getTime', inputs: {} }] }),
quickAppIds: z
.array(ObjectIdSchema)
.openapi({ example: ['68ac3dc6a717b3bcacc749bb', '68ac2da6a717b3bcacc73032'] }),
favouriteTags: z
.array(ChatFavouriteTagSchema)
.openapi({ example: [{ id: 'i7Ege2W2', name: '游戏' }] })
});
export type ChatSettingType = z.infer<typeof ChatSettingSchema>;
export const ChatSettingResponseSchema = z.object({
...ChatSettingSchema.omit({ quickAppIds: true, selectedTools: true }).shape,
quickAppList: z.array(ChatQuickAppSchema),
selectedTools: z.array(ChatSelectedToolSchema)
});
export type ChatSettingResponseType = z.infer<typeof ChatSettingResponseSchema>;
export type ChatSettingUpdateType = Partial<z.infer<typeof ChatSettingSchema>>;

View File

@@ -3,9 +3,13 @@
"version": "1.0.0",
"dependencies": {
"@fastgpt-sdk/plugin": "^0.1.19",
"@anatine/zod-openapi": "^2.2.8",
"@apidevtools/swagger-parser": "^10.1.0",
"@bany/curl-to-json": "^1.2.8",
"axios": "^1.12.1",
"@ts-rest/core": "^3.52.1",
"@ts-rest/next": "^3.52.1",
"@ts-rest/open-api": "^3.52.1",
"cron-parser": "^4.9.0",
"dayjs": "^1.11.7",
"encoding": "^0.1.13",
@@ -17,7 +21,8 @@
"openai": "4.61.0",
"openapi-types": "^12.1.3",
"timezones-list": "^3.0.2",
"lodash": "^4.17.21"
"lodash": "^4.17.21",
"zod": "^3.24.2"
},
"devDependencies": {
"@types/lodash": "^4.14.191",

View File

@@ -12,6 +12,72 @@ export interface ResponseType<T = any> {
data: T;
}
export interface ProcessedError {
code: number;
statusText: string;
message: string;
data?: any;
shouldClearCookie: boolean;
}
/**
* 通用错误处理函数,提取错误信息并分类记录日志
* @param params - 包含错误对象、URL和默认状态码的参数
* @returns 处理后的错误对象
*/
export function processError(params: {
error: any;
url?: string;
defaultCode?: number;
}): ProcessedError {
const { error, url, defaultCode = 500 } = params;
const errResponseKey = typeof error === 'string' ? error : error?.message;
// 1. 处理特定的业务错误ERROR_RESPONSE
if (ERROR_RESPONSE[errResponseKey]) {
const shouldClearCookie = errResponseKey === ERROR_ENUM.unAuthorization;
// 记录业务侧错误日志
addLog.info(`Api response error: ${url}`, ERROR_RESPONSE[errResponseKey]);
return {
code: ERROR_RESPONSE[errResponseKey].code || defaultCode,
statusText: ERROR_RESPONSE[errResponseKey].statusText || 'error',
message: ERROR_RESPONSE[errResponseKey].message,
data: ERROR_RESPONSE[errResponseKey].data,
shouldClearCookie
};
}
// 2. 提取通用错误消息
let msg = error?.response?.statusText || error?.message || '请求错误';
if (typeof error === 'string') {
msg = error;
} else if (proxyError[error?.code]) {
msg = '网络连接异常';
} else if (error?.response?.data?.error?.message) {
msg = error?.response?.data?.error?.message;
} else if (error?.error?.message) {
msg = error?.error?.message;
}
// 3. 根据错误类型记录不同级别的日志
if (error instanceof UserError) {
addLog.info(`Request error: ${url}, ${msg}`);
} else {
addLog.error(`System unexpected error: ${url}, ${msg}`, error);
}
// 4. 返回处理后的错误信息
return {
code: defaultCode,
statusText: 'error',
message: replaceSensitiveText(msg),
shouldClearCookie: false
};
}
export const jsonRes = <T = any>(
res: NextApiResponse,
props?: {
@@ -24,53 +90,37 @@ export const jsonRes = <T = any>(
) => {
const { code = 200, message = '', data = null, error, url } = props || {};
const errResponseKey = typeof error === 'string' ? error : error?.message;
// Specified error
if (ERROR_RESPONSE[errResponseKey]) {
// login is expired
if (errResponseKey === ERROR_ENUM.unAuthorization) {
// 如果有错误,使用统一的错误处理逻辑
if (error) {
const processedError = processError({ error, url, defaultCode: code });
// 如果需要清除 cookie
if (processedError.shouldClearCookie) {
clearCookie(res);
}
// Bussiness Side Error
addLog.info(`Api response error: ${url}`, ERROR_RESPONSE[errResponseKey]);
res.status(code);
res.status(processedError.code);
// 如果有自定义消息,直接发送
if (message) {
res.send(message);
} else {
res.json(ERROR_RESPONSE[errResponseKey]);
res.json({
code: processedError.code,
statusText: processedError.statusText,
message: processedError.message,
data: processedError.data !== undefined ? processedError.data : null
});
}
return;
}
// another error
let msg = '';
if ((code < 200 || code >= 400) && !message) {
msg = error?.response?.statusText || error?.message || '请求错误';
if (typeof error === 'string') {
msg = error;
} else if (proxyError[error?.code]) {
msg = '网络连接异常';
} else if (error?.response?.data?.error?.message) {
msg = error?.response?.data?.error?.message;
} else if (error?.error?.message) {
msg = error?.error?.message;
}
if (error instanceof UserError) {
addLog.info(`Request error: ${url}, ${msg}`);
} else {
addLog.error(`System unexpected error: ${url}, ${msg}`, error);
}
}
// 成功响应
res.status(code).json({
code,
statusText: '',
message: replaceSensitiveText(message || msg),
message: replaceSensitiveText(message),
data: data !== undefined ? data : null
});
};

View File

@@ -1,5 +1,5 @@
import { connectionMongo, getMongoModel } from '../../../common/mongo';
import { type ChatFavouriteAppSchema as ChatFavouriteAppType } from '@fastgpt/global/core/chat/favouriteApp/type';
import { type ChatFavouriteAppType } from '@fastgpt/global/core/chat/favouriteApp/type';
import { TeamCollectionName } from '@fastgpt/global/support/user/team/constant';
import { AppCollectionName } from '../../app/schema';

View File

@@ -1,5 +1,5 @@
import { connectionMongo, getMongoModel } from '../../../common/mongo';
import { type ChatSettingSchema as ChatSettingType } from '@fastgpt/global/core/chat/setting/type';
import { type ChatSettingType } from '@fastgpt/global/core/chat/setting/type';
import { TeamCollectionName } from '@fastgpt/global/support/user/team/constant';
import { AppCollectionName } from '../../app/schema';