Skip to content

Commit 1b5b47a

Browse files
committed
feat: team rate limitation
1 parent 48c0c15 commit 1b5b47a

File tree

2 files changed

+113
-1
lines changed

2 files changed

+113
-1
lines changed
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
import { type ApiRequestProps } from '../../type/next';
2+
import { type NextApiResponse } from 'next';
3+
import { jsonRes } from '../response';
4+
import { getGlobalRedisConnection } from '../redis';
5+
6+
export interface TeamRateLimitOptions {
7+
seconds: number;
8+
limit: number;
9+
keyPrefix?: string;
10+
}
11+
12+
/**
13+
* 基于团队ID的Redis限流中间件
14+
* 注意:这个中间件会在请求认证前执行,所以需要从请求中提取团队ID
15+
* @param options 限流配置
16+
* @returns 中间件函数
17+
* @example
18+
* export default NextAPI(
19+
* useTeamFrequencyLimit({
20+
* seconds: 60,
21+
* limit: 1000,
22+
* keyPrefix: 'chat-completions-rate-limit'
23+
* }),
24+
* handler
25+
* );
26+
*/
27+
export function useTeamFrequencyLimit(options: TeamRateLimitOptions) {
28+
const { seconds, limit, keyPrefix = 'team-rate-limit' } = options;
29+
30+
return async (req: ApiRequestProps, res: NextApiResponse) => {
31+
// 尝试从请求的不同位置获取团队ID
32+
let teamId: string | undefined;
33+
34+
// 1. 从请求体中获取(最常见的情况)
35+
if (req.body?.teamId) {
36+
teamId = req.body.teamId;
37+
}
38+
// 2. 从查询参数中获取
39+
else if (req.query?.teamId) {
40+
teamId = req.query.teamId as string;
41+
}
42+
// 3. 从Authorization header中解析(如果使用API Key)
43+
else if (req.headers?.authorization) {
44+
// 这里可以添加API Key解析逻辑来获取teamId
45+
// 但为了简单起见,我们暂时跳过这种情况
46+
}
47+
48+
if (!teamId) {
49+
// 如果没有团队ID,跳过限流检查
50+
return;
51+
}
52+
53+
try {
54+
const redis = getGlobalRedisConnection();
55+
const key = `${keyPrefix}:${teamId}`;
56+
57+
// 使用Redis的滑动窗口限流算法
58+
const currentTime = Math.floor(Date.now() / 1000);
59+
const windowStart = currentTime - seconds;
60+
61+
// 使用Redis Pipeline提高性能
62+
const pipeline = redis.pipeline();
63+
64+
// 移除过期的请求记录
65+
pipeline.zremrangebyscore(key, 0, windowStart);
66+
67+
// 添加当前请求记录
68+
pipeline.zadd(key, currentTime, `${currentTime}-${Math.random()}`);
69+
70+
// 获取当前窗口内的请求数量
71+
pipeline.zcard(key);
72+
73+
// 设置key的过期时间
74+
pipeline.expire(key, seconds);
75+
76+
const results = await pipeline.exec();
77+
78+
if (!results) {
79+
throw new Error('Redis pipeline execution failed');
80+
}
81+
82+
// zcard的结果在pipeline的第三个操作中
83+
const currentRequestCount = results[2][1] as number;
84+
85+
if (currentRequestCount > limit) {
86+
// 超出限流,返回429错误
87+
const remainingTime = await redis.ttl(key);
88+
jsonRes(res, {
89+
code: 429,
90+
error: `Rate limit exceeded. Maximum ${limit} requests per ${seconds} seconds for this team. Please try again in ${remainingTime} seconds.`
91+
});
92+
return;
93+
}
94+
95+
// 在响应头中添加限流信息
96+
res.setHeader('X-RateLimit-Limit', limit);
97+
res.setHeader('X-RateLimit-Remaining', Math.max(0, limit - currentRequestCount));
98+
res.setHeader('X-RateLimit-Reset', new Date(Date.now() + seconds * 1000).toISOString());
99+
} catch (error) {
100+
console.error('Rate limit check failed:', error);
101+
// Redis错误时不阻断请求,继续处理
102+
}
103+
};
104+
}

projects/app/src/pages/api/v1/chat/completions.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ import { DispatchNodeResponseKeyEnum } from '@fastgpt/global/core/workflow/runti
4848
import { NextAPI } from '@/service/middleware/entry';
4949
import { getAppLatestVersion } from '@fastgpt/service/core/app/version/controller';
5050
import { ReadPermissionVal } from '@fastgpt/global/support/permission/constant';
51+
import { useTeamFrequencyLimit } from '@fastgpt/service/common/middle/teamRateLimit';
5152
import { AppTypeEnum } from '@fastgpt/global/core/app/constants';
5253
import {
5354
serverGetWorkflowToolRunUserQuery,
@@ -449,7 +450,14 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
449450
}
450451
}
451452
}
452-
export default NextAPI(handler);
453+
export default NextAPI(
454+
useTeamFrequencyLimit({
455+
seconds: 60,
456+
limit: 1000,
457+
keyPrefix: 'chat-completions-rate-limit'
458+
}),
459+
handler
460+
);
453461

454462
const authShareChat = async ({
455463
chatId,

0 commit comments

Comments
 (0)