4a9397bccc
- 新增 @wenwumap/tts 独立包:边流式边合成、按句排队顺序播放、 专业 TTS 失败自动降级浏览器朗读,含 README 使用说明 - AI 后端新增 /ai/tts 接口,改用 DashScope CosyVoice(cosyvoice-v3-flash) 输出 mp3,串行+退避重试规避 429 限流 - web 对话面板接入 SpeechQueue,按角色配音色,加语音开关与朗读按钮 - admin 支持 /admin/ 基路径部署 - 地图页移除大面积 backdrop-blur,降低 GPU 占用
46 lines
1.3 KiB
TypeScript
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;
|
|
}
|
|
}
|