From 38111b6abd3b19ba1f60c2d430db963d84ae0dd5 Mon Sep 17 00:00:00 2001 From: BTMuli Date: Thu, 26 Mar 2026 06:00:42 +0800 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20=E6=B7=BB=E5=8A=A0=E5=B8=96?= =?UTF-8?q?=E5=AD=90=E8=AF=A6=E6=83=85=E8=AF=B7=E6=B1=82=E9=99=90=E6=B5=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit close #233 --- src/components/userScripts/tus-mission.vue | 10 ++- src/utils/rateLimiter.ts | 98 ++++++++++++++++++++++ 2 files changed, 106 insertions(+), 2 deletions(-) create mode 100644 src/utils/rateLimiter.ts diff --git a/src/components/userScripts/tus-mission.vue b/src/components/userScripts/tus-mission.vue index 3d0e19c5..9fed59f0 100644 --- a/src/components/userScripts/tus-mission.vue +++ b/src/components/userScripts/tus-mission.vue @@ -50,6 +50,7 @@ import postReq from "@req/postReq.js"; import useAppStore from "@store/app.js"; import TGLogger from "@utils/TGLogger.js"; import TGNotify from "@utils/TGNotify.js"; +import { postDetailRateLimiter } from "@utils/rateLimiter.js"; import { storeToRefs } from "pinia"; import { ref, shallowRef, watch } from "vue"; @@ -216,8 +217,13 @@ async function tryAuto(skip: boolean = false): Promise { } } if (likeCnt < 5 || viewCnt < 3) { - await TGLogger.Script(`[米游币任务]正在浏览帖子${post.post.post_id}`); - const detailResp = await postReq.post(post.post.post_id, ckPost); + const currentCount = postDetailRateLimiter.getRequestCount(); + await TGLogger.Script( + `[米游币任务]正在浏览帖子${post.post.post_id} (当前 1 分钟内请求数:${currentCount}/10)`, + ); + const detailResp = await postDetailRateLimiter.execute(() => + postReq.post(post.post.post_id, ckPost), + ); if ("retcode" in detailResp) { await TGLogger.Script( `[米游币任务]获取帖子${post.post.post_id}失败:${detailResp.retcode} ${detailResp.message}`, diff --git a/src/utils/rateLimiter.ts b/src/utils/rateLimiter.ts new file mode 100644 index 00000000..68103098 --- /dev/null +++ b/src/utils/rateLimiter.ts @@ -0,0 +1,98 @@ +/** + * 请求限流工具 + * @since Beta v0.9.9 + */ + +/** + * 限流器配置 + */ +type RateLimiterConfig = { + /** 时间窗口(毫秒) */ + windowMs: number; + /** 最大请求次数 */ + maxRequests: number; +}; + +/** + * 限流器类 + * @since Beta v0.9.9 + */ +class RateLimiter { + private config: RateLimiterConfig; + private requests: Array; + + constructor(config: RateLimiterConfig) { + this.config = config; + this.requests = []; + } + + /** + * 执行限流请求 + * @since Beta v0.9.9 + * @param fn - 要执行的异步函数 + * @returns 函数执行结果 + */ + async execute(fn: () => Promise): Promise { + await this.wait(); + return fn(); + } + + /** + * 等待直到可以发起请求 + * @since Beta v0.9.9 + */ + private async wait(): Promise { + const now = Date.now(); + const windowStart = now - this.config.windowMs; + + // 移除时间窗口外的请求记录 + this.requests = this.requests.filter((time) => time > windowStart); + + // 如果请求数已达上限,计算需要等待的时间 + if (this.requests.length >= this.config.maxRequests) { + const oldestRequest = this.requests[0]; + const waitTime = oldestRequest + this.config.windowMs - now; + + if (waitTime > 0) { + await new Promise((resolve) => setTimeout(resolve, waitTime)); + // 重新计算时间窗口 + await this.wait(); + return; + } + } + + // 记录当前请求时间 + this.requests.push(Date.now()); + } + + /** + * 获取当前窗口内的请求数 + * @since Beta v0.9.9 + * @returns 请求数 + */ + getRequestCount(): number { + const now = Date.now(); + const windowStart = now - this.config.windowMs; + return this.requests.filter((time) => time > windowStart).length; + } + + /** + * 重置限流器 + * @since Beta v0.9.9 + */ + reset(): void { + this.requests = []; + } +} + +/** + * 帖子详情请求限流器 + * 限制:1 分钟内最多 10 次请求 + * @since Beta v0.9.9 + */ +export const postDetailRateLimiter = new RateLimiter({ + windowMs: 60 * 1000, // 1 分钟 + maxRequests: 10, // 最多 10 次请求 +}); + +export default RateLimiter;