From 40b7f0106750b93b84179eae199391283941fd2f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=B0=8F=E6=B5=B7?= <7836246@qq.com> Date: Wed, 4 Mar 2026 16:17:06 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=BC=95=E5=85=A5=20x-is-human=20Token?= =?UTF-8?q?=20=E8=B5=84=E6=BA=90=E6=B1=A0=E4=BB=A5=E5=AF=B9=E6=8A=97?= =?UTF-8?q?=E5=B9=B6=E5=8F=91=E9=A3=8E=E6=8E=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 移除了原有的单例 `cachedToken` 设计。 - 引入最大容量为 5 的数组 `tokenPool`。 - 每个发出的并发请求都会从池中随机抽取有效 token,有效打散请求特征,降低被墙概率。 - 引入异步补充机制:一旦库存不足满载,将会在后台默默生成并补全缓存,且不阻塞当前请求。 --- src/cursor-client.ts | 75 +++++++++++++++++++++++++++++++++++--------- 1 file changed, 61 insertions(+), 14 deletions(-) diff --git a/src/cursor-client.ts b/src/cursor-client.ts index e7c865e..9a55f38 100644 --- a/src/cursor-client.ts +++ b/src/cursor-client.ts @@ -44,10 +44,17 @@ function getChromeHeaders(xIsHuman: string): Record { // ==================== Token 管理 ==================== -let cachedToken: { value: string; createdAt: number } | null = null; let envJS: string = ''; let mainJS: string = ''; +interface TokenEntry { + value: string; + createdAt: number; +} +const tokenPool: TokenEntry[] = []; +const MAX_POOL_SIZE = 5; +let isGenerating = false; + const TOKEN_EXPIRY_MS = 25 * 60 * 1000; // 25 分钟 const TOKEN_REFRESH_MS = 20 * 60 * 1000; // 20 分钟时刷新 @@ -138,7 +145,25 @@ async function generateToken(): Promise { } /** - * 获取有效的 x-is-human token(带缓存) + * 异步补充 Token 池 + */ +async function replenishPool(): Promise { + if (isGenerating) return; + isGenerating = true; + try { + console.log(`[Token] 补充 token 池 (当前有效: ${tokenPool.length}/${MAX_POOL_SIZE})...`); + const token = await generateToken(); + tokenPool.push({ value: token, createdAt: Date.now() }); + console.log(`[Token] ✓ 成功添加到资源池 (当前大小: ${tokenPool.length})`); + } catch (e) { + console.error('[Token] ✗ 补充池失败:', e); + } finally { + isGenerating = false; + } +} + +/** + * 获取有效的 x-is-human token(从池中随机抽取以分散风控特征) */ export async function getXIsHumanToken(): Promise { // 如果没有脚本,返回空(某些场景可能不需要 token) @@ -147,24 +172,46 @@ export async function getXIsHumanToken(): Promise { return ''; } - // 检查缓存是否有效 - if (cachedToken && (Date.now() - cachedToken.createdAt) < TOKEN_REFRESH_MS) { - return cachedToken.value; + const now = Date.now(); + + // 1. 清理过期 Token (>=25分钟) + for (let i = tokenPool.length - 1; i >= 0; i--) { + if (now - tokenPool[i].createdAt >= TOKEN_EXPIRY_MS) { + tokenPool.splice(i, 1); + } } + // 2. 筛选仍然新鲜的 Token (<20分钟) + const freshTokens = tokenPool.filter(t => (now - t.createdAt) < TOKEN_REFRESH_MS); + + // 3. 如果池子没满,且当前没有在生成,则异步触发补充机制 + if (freshTokens.length < MAX_POOL_SIZE && !isGenerating) { + // 后台异步生成,不阻塞请求 + replenishPool().catch(console.error); + } + + // 4. 如果有新鲜 token,从中随机挑选一个(分散风控特征) + if (freshTokens.length > 0) { + const randomIndex = Math.floor(Math.random() * freshTokens.length); + return freshTokens[randomIndex].value; + } + + // 5. 如果没有新鲜的,但有即将过期的 (20-25分钟内),作为过渡先使用最近生成的一个 + if (tokenPool.length > 0) { + const lastValid = [...tokenPool].sort((a, b) => b.createdAt - a.createdAt)[0]; + console.warn('[Token] 暂无新鲜 token,使用临近过期的就近 token 作为过渡'); + return lastValid.value; + } + + // 6. 连过期的都没有,只能同步阻塞等待生成一个 try { - console.log('[Token] 生成新 token...'); + console.log('[Token] 资源池为空,同步等待生成新 token...'); const token = await generateToken(); - cachedToken = { value: token, createdAt: Date.now() }; - console.log('[Token] ✓ 生成成功'); + tokenPool.push({ value: token, createdAt: Date.now() }); + console.log('[Token] ✓ 同步生成成功'); return token; } catch (e) { - console.error('[Token] ✗ 生成失败:', e); - // 如果有旧 token 且未过期,继续使用 - if (cachedToken && (Date.now() - cachedToken.createdAt) < TOKEN_EXPIRY_MS) { - console.warn('[Token] 使用旧 token(仍在有效期内)'); - return cachedToken.value; - } + console.error('[Token] ✗ 同步生成失败:', e); return ''; } }