mirror of
https://github.com/labring/FastGPT.git
synced 2025-10-13 22:56:28 +00:00
@@ -17,4 +17,8 @@ description: 'FastGPT V4.13.1 更新说明'
|
||||
|
||||
## 🔨 插件更新
|
||||
|
||||
1. Perplexity search 工具。
|
||||
1. Perplexity search 工具。
|
||||
2. Base64转文件工具。
|
||||
3. MiniMax TTS 文件生成工具。
|
||||
4. Openrouter nano banana 绘图工具。
|
||||
5. 系统工具支持配置是否需要在 Worker 中运行。
|
@@ -1,172 +1,13 @@
|
||||
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';
|
||||
import { initClient } from '@ts-rest/core';
|
||||
import { fastgptContract } from './contracts';
|
||||
|
||||
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 createFastGPTClient(options: {
|
||||
baseUrl: string;
|
||||
baseHeaders?: Record<string, string>;
|
||||
api?: any;
|
||||
credentials?: RequestCredentials;
|
||||
throwOnUnknownStatus?: boolean;
|
||||
validateResponse?: boolean;
|
||||
}) {
|
||||
return initClient(fastgptContract, options);
|
||||
}
|
||||
|
||||
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);
|
||||
};
|
||||
};
|
||||
|
@@ -1,37 +0,0 @@
|
||||
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: '更新聊天设置'
|
||||
}
|
||||
});
|
@@ -1,5 +1,6 @@
|
||||
import { settingContract } from './setting';
|
||||
import { c } from '../../../init';
|
||||
import { initContract } from '@ts-rest/core';
|
||||
const c = initContract();
|
||||
|
||||
export const chatContract = c.router({
|
||||
setting: settingContract
|
@@ -1,12 +1,14 @@
|
||||
import { ObjectIdSchema } from '../../../../../../type';
|
||||
import z from 'zod';
|
||||
import {
|
||||
ChatFavouriteAppResponseItemSchema,
|
||||
ChatFavouriteAppUpdateSchema
|
||||
} from '../../../../../../../core/chat/favouriteApp/type';
|
||||
import { c } from '../../../../../init';
|
||||
import { z } from 'zod';
|
||||
ChatSettingResponseSchema,
|
||||
ChatSettingSchema
|
||||
} from '../../../../../../core/chat/setting/type';
|
||||
import { ChatFavouriteAppResponseItemSchema } from '../../../../../../core/chat/favouriteApp/type';
|
||||
import { ObjectIdSchema } from '../../../../../type';
|
||||
import { initContract } from '@ts-rest/core';
|
||||
|
||||
export const favouriteContract = c.router({
|
||||
const c = initContract();
|
||||
const favouriteContract = c.router({
|
||||
list: {
|
||||
path: '/proApi/core/chat/setting/favourite/list',
|
||||
method: 'GET',
|
||||
@@ -23,13 +25,17 @@ export const favouriteContract = c.router({
|
||||
description: '获取精选应用列表',
|
||||
summary: '获取精选应用列表'
|
||||
},
|
||||
|
||||
update: {
|
||||
path: '/proApi/core/chat/setting/favourite/update',
|
||||
method: 'PUT',
|
||||
body: ChatFavouriteAppUpdateSchema,
|
||||
body: z.array(
|
||||
z.object({
|
||||
appId: z.string(),
|
||||
order: z.number()
|
||||
})
|
||||
),
|
||||
responses: {
|
||||
200: c.type<void>()
|
||||
200: z.void()
|
||||
},
|
||||
metadata: {
|
||||
tags: ['chat']
|
||||
@@ -37,7 +43,6 @@ export const favouriteContract = c.router({
|
||||
description: '更新精选应用',
|
||||
summary: '更新精选应用'
|
||||
},
|
||||
|
||||
delete: {
|
||||
path: '/proApi/core/chat/setting/favourite/delete',
|
||||
method: 'DELETE',
|
||||
@@ -45,7 +50,7 @@ export const favouriteContract = c.router({
|
||||
id: ObjectIdSchema
|
||||
}),
|
||||
responses: {
|
||||
200: c.type<void>()
|
||||
200: z.void()
|
||||
},
|
||||
metadata: {
|
||||
tags: ['chat']
|
||||
@@ -53,7 +58,6 @@ export const favouriteContract = c.router({
|
||||
description: '删除精选应用',
|
||||
summary: '删除精选应用'
|
||||
},
|
||||
|
||||
order: {
|
||||
path: '/proApi/core/chat/setting/favourite/order',
|
||||
method: 'PUT',
|
||||
@@ -64,7 +68,7 @@ export const favouriteContract = c.router({
|
||||
})
|
||||
),
|
||||
responses: {
|
||||
200: c.type<void>()
|
||||
200: z.void()
|
||||
},
|
||||
metadata: {
|
||||
tags: ['chat']
|
||||
@@ -72,7 +76,6 @@ export const favouriteContract = c.router({
|
||||
description: '更新精选应用顺序',
|
||||
summary: '更新精选应用顺序'
|
||||
},
|
||||
|
||||
tags: {
|
||||
path: '/proApi/core/chat/setting/favourite/tags',
|
||||
method: 'PUT',
|
||||
@@ -83,7 +86,7 @@ export const favouriteContract = c.router({
|
||||
})
|
||||
),
|
||||
responses: {
|
||||
200: c.type<void>()
|
||||
200: z.void()
|
||||
},
|
||||
metadata: {
|
||||
tags: ['chat']
|
||||
@@ -92,3 +95,33 @@ export const favouriteContract = c.router({
|
||||
summary: '更新精选应用标签'
|
||||
}
|
||||
});
|
||||
|
||||
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: z.void()
|
||||
},
|
||||
metadata: {
|
||||
tags: ['chat']
|
||||
},
|
||||
description: '更新聊天设置',
|
||||
summary: '更新聊天设置'
|
||||
}
|
||||
});
|
@@ -0,0 +1,7 @@
|
||||
import { initContract } from '@ts-rest/core';
|
||||
import { chatContract } from './chat';
|
||||
const c = initContract();
|
||||
|
||||
export const coreContract = c.router({
|
||||
chat: chatContract
|
||||
});
|
@@ -1,14 +1,12 @@
|
||||
import { initContract } from '@ts-rest/core';
|
||||
import { coreContract } from './core';
|
||||
import { supportContract } from './support';
|
||||
import { chatContract } from './chat';
|
||||
import { c } from '../../init';
|
||||
|
||||
// 前端使用的完整合约(开源 + Pro)
|
||||
// FastGPT 后端使用的合约
|
||||
export const contract = c.router({
|
||||
chat: {
|
||||
...chatContract
|
||||
},
|
||||
support: {
|
||||
...supportContract
|
||||
}
|
||||
const c = initContract();
|
||||
|
||||
export const fastgptContract = c.router({
|
||||
core: coreContract,
|
||||
support: supportContract
|
||||
});
|
||||
|
||||
export type FadtGPTContractType = typeof fastgptContract;
|
||||
|
@@ -1,5 +1,6 @@
|
||||
import { userContract } from './user';
|
||||
import { c } from '../../../init';
|
||||
import { initContract } from '@ts-rest/core';
|
||||
const c = initContract();
|
||||
|
||||
export const supportContract = c.router({
|
||||
user: userContract
|
||||
|
@@ -1,12 +1,14 @@
|
||||
import { c } from '../../../../../init';
|
||||
import { initContract } from '@ts-rest/core';
|
||||
import z from 'zod';
|
||||
const c = initContract();
|
||||
|
||||
export const accountContract = c.router({
|
||||
logout: {
|
||||
path: '/support/user/account/login',
|
||||
method: 'POST',
|
||||
body: c.type<undefined>(),
|
||||
body: z.undefined(),
|
||||
responses: {
|
||||
200: c.type<void>()
|
||||
200: z.void()
|
||||
},
|
||||
metadata: {
|
||||
tags: ['support']
|
@@ -1,5 +1,6 @@
|
||||
import { accountContract } from '../../../../fastgpt/contracts/support/user/account';
|
||||
import { c } from '../../../../init';
|
||||
import { accountContract } from './account';
|
||||
import { initContract } from '@ts-rest/core';
|
||||
const c = initContract();
|
||||
|
||||
export const userContract = c.router({
|
||||
account: accountContract
|
||||
|
@@ -1,18 +1,18 @@
|
||||
import type { contract } from './fastgpt/contracts';
|
||||
import { fastgptContract, type FadtGPTContractType } from './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 {
|
||||
type OpenAPIObject = ReturnType<typeof generateOpenApi>;
|
||||
function generateOpenApiDocument(c: FadtGPTContractType): OpenAPIObject {
|
||||
return generateOpenApi(
|
||||
c,
|
||||
{
|
||||
info: {
|
||||
title: 'FastGPT OpenAPI',
|
||||
version: '4.12.4',
|
||||
version: '4.13.2',
|
||||
description: 'FastGPT OpenAPI'
|
||||
},
|
||||
servers: [{ url: '/api' }]
|
||||
@@ -32,3 +32,5 @@ export function generateOpenApiDocument(c: typeof contract): OpenAPIObject {
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export const fastgptOpenApiDocument = generateOpenApiDocument(fastgptContract);
|
20
packages/global/common/tsRest/fastgpt/router.ts
Normal file
20
packages/global/common/tsRest/fastgpt/router.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { fastgptContract } from './contracts';
|
||||
import { createNextRoute, createNextRouter } from '@ts-rest/next';
|
||||
|
||||
/**
|
||||
* 创建 FastGPT 单个路由
|
||||
*/
|
||||
export function createServerRoute(
|
||||
implementation: Parameters<typeof createNextRoute<typeof fastgptContract>>[1]
|
||||
) {
|
||||
return createNextRoute(fastgptContract, implementation);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建 FastGPT 路由器([...ts-rest])
|
||||
*/
|
||||
export function createServerRouter(
|
||||
router: Parameters<typeof createNextRouter<typeof fastgptContract>>[1]
|
||||
) {
|
||||
return createNextRouter(fastgptContract, router);
|
||||
}
|
@@ -1,21 +0,0 @@
|
||||
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);
|
||||
}
|
@@ -1,23 +1,6 @@
|
||||
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
|
||||
});
|
||||
import { chatContract } from '../../fastgpt/contracts/core/chat';
|
||||
import { initContract } from '@ts-rest/core';
|
||||
const c = initContract();
|
||||
|
||||
/**
|
||||
* 转换路径前缀
|
||||
@@ -53,3 +36,21 @@ function transformPaths<T extends Record<string, any>>(
|
||||
|
||||
return transform(router) as T;
|
||||
}
|
||||
|
||||
// 通过 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
|
||||
});
|
||||
|
@@ -1,3 +0,0 @@
|
||||
import { initContract } from '@ts-rest/core';
|
||||
|
||||
export const c = initContract();
|
@@ -1,5 +1,6 @@
|
||||
import type { AppRoute } from '@ts-rest/core';
|
||||
import type { createSingleRouteHandler } from '@ts-rest/next';
|
||||
import z from 'zod';
|
||||
|
||||
export type { AppRoute } from '@ts-rest/core';
|
||||
|
||||
@@ -12,3 +13,23 @@ 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>>;
|
||||
|
||||
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 AnyResponseSchema = createCommonResponseSchema(z.any());
|
||||
|
@@ -3,24 +3,6 @@ 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)
|
||||
@@ -40,6 +22,3 @@ 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());
|
||||
|
@@ -27,8 +27,3 @@ export const ChatFavouriteAppResponseItemSchema = z.object({
|
||||
});
|
||||
|
||||
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>;
|
||||
|
58
projects/app/src/apiRouters/proApi.ts
Normal file
58
projects/app/src/apiRouters/proApi.ts
Normal 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);
|
@@ -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);
|
31
projects/app/src/pages/api/[...ts-rest].ts
Normal file
31
projects/app/src/pages/api/[...ts-rest].ts
Normal 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);
|
@@ -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);
|
@@ -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 (
|
||||
|
@@ -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
|
||||
});
|
||||
|
||||
|
215
projects/app/src/web/common/api/client.ts
Normal file
215
projects/app/src/web/common/api/client.ts
Normal 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);
|
||||
};
|
||||
};
|
@@ -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
|
||||
}));
|
||||
|
@@ -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);
|
||||
|
||||
|
Reference in New Issue
Block a user