Files
WenwuMap/apps/api/src/common/rate-limit.guard.ts
T
selfrelease 4a9397bccc feat(tts): 新增通用流式 TTS 引擎并接入 AI 对话
- 新增 @wenwumap/tts 独立包:边流式边合成、按句排队顺序播放、
  专业 TTS 失败自动降级浏览器朗读,含 README 使用说明
- AI 后端新增 /ai/tts 接口,改用 DashScope CosyVoice(cosyvoice-v3-flash)
  输出 mp3,串行+退避重试规避 429 限流
- web 对话面板接入 SpeechQueue,按角色配音色,加语音开关与朗读按钮
- admin 支持 /admin/ 基路径部署
- 地图页移除大面积 backdrop-blur,降低 GPU 占用
2026-06-14 23:13:26 +08:00

46 lines
1.3 KiB
TypeScript

import {
CanActivate,
ExecutionContext,
HttpException,
HttpStatus,
Injectable,
} from "@nestjs/common";
import { Request } from "express";
/**
* 简单的内存级 IP 限流守卫:滑动窗口。
* 用于保护成本敏感的 AI 接口,防止被刷爆额度。
*/
@Injectable()
export class RateLimitGuard implements CanActivate {
private static readonly WINDOW_MS = 60_000;
// 单条回答会按句拆分为多次 TTS 调用,故放宽阈值(仍可防刷)。
private static readonly MAX_REQUESTS = 120;
private static readonly buckets = new Map<string, number[]>();
canActivate(context: ExecutionContext): boolean {
const req = context.switchToHttp().getRequest<Request>();
const forwarded = req.headers["x-forwarded-for"];
const ip =
(Array.isArray(forwarded) ? forwarded[0] : forwarded?.split(",")[0])?.trim() ||
req.ip ||
"unknown";
const now = Date.now();
const recent = (RateLimitGuard.buckets.get(ip) ?? []).filter(
(t) => now - t < RateLimitGuard.WINDOW_MS
);
if (recent.length >= RateLimitGuard.MAX_REQUESTS) {
throw new HttpException(
"请求过于频繁,请稍后再试",
HttpStatus.TOO_MANY_REQUESTS
);
}
recent.push(now);
RateLimitGuard.buckets.set(ip, recent);
return true;
}
}