nanoppa commited on
Commit
e1ac4a8
·
verified ·
1 Parent(s): 99e3cc9

Update main.ts

Browse files
Files changed (1) hide show
  1. main.ts +50 -54
main.ts CHANGED
@@ -1,8 +1,53 @@
1
  import { serve } from "https://deno.land/std@0.178.0/http/server.ts";
2
 
3
- // -------------------- 改造部分:使用内存 Map 和 Web Locks API 替代 Deno KV --------------------
4
  // 使用一个 Map 在内存中存储每个 provider 的轮询索引
5
  const rotationIndexes = new Map<string, number>();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6
  // -------------------- 改造部分结束 --------------------
7
 
8
 
@@ -28,16 +73,14 @@ const apiMapping: Record<string, string> = {
28
  "/telegram": "https://api.telegram.org",
29
  };
30
 
31
- // 密钥轮询配置:是否对各 provider 启用密钥轮询
32
- // 如果没有在这里配置,则默认过滤敏感请求头后直接转发请求
33
  const rotationConfig: Record<string, boolean> = {
34
  gemini: true,
35
  groq: true,
36
  openrouter: true
37
  };
38
 
39
- // 鉴权请求头格式声明:请在此处声明不同 provider 的鉴权请求头格式
40
- // 如果没有在这里声明,则默认使用 "Authorization: Bearer ${apiKey}"
41
  const authHeaderMapping: Record<string, (apiKey: string) => [string, string]> = {
42
  gemini: (apiKey: string) => ["x-goog-api-key", apiKey],
43
  claude: (apiKey: string) => ["x-api-key", apiKey],
@@ -76,45 +119,12 @@ function parseTarget(urlPath: string): { provider: string; targetUrl: string } {
76
 
77
  // -------------------- 4. 处理密钥池与轮询索引 --------------------
78
  async function getKeyPool(provider: string): Promise<string[]> {
79
- const envKeyName = `${provider.toUpperCase()}_KEYS`; // 环境变量名通常大写,这里统一为大写
80
  const keyPoolStr = Deno.env.get(envKeyName);
81
  if (!keyPoolStr) return [];
82
  return keyPoolStr.split(",").map((k) => k.trim()).filter((k) => k.length > 0);
83
  }
84
 
85
- /**
86
- * 改造后的函数:
87
- * 使用 Web Locks API 原子性地获取并递增轮询索引。
88
- * 这可以防止并发请求时多个请求拿到同一个索引。
89
- * @param provider - 服务商名称
90
- * @param length - 密钥池的长度
91
- * @returns 将用于当前请求的索引
92
- */
93
- async function getAndIncrementRotationIndex(
94
- provider: string,
95
- length: number,
96
- ): Promise<number> {
97
- // 使用 provider 名称作为锁的唯一标识符
98
- const lockName = `key-rotation-lock-${provider}`;
99
-
100
- // navigator.locks.request 会确保回调函数中的代码以原子方式执行
101
- // 对于同一个 lockName,一次只有一个回调可以运行
102
- return await navigator.locks.request(lockName, () => {
103
- // 从内存 Map 中获取当前索引,如果不存在则默认为 0
104
- const currentIndex = rotationIndexes.get(provider) ?? 0;
105
-
106
- // 计算下一个索引
107
- const nextIndex = (currentIndex + 1) % length;
108
-
109
- // 将新索引存回 Map
110
- rotationIndexes.set(provider, nextIndex);
111
-
112
- // 返回当前请求应该使用的索引
113
- return currentIndex;
114
- });
115
- }
116
-
117
-
118
  // -------------------- 5. 传入请求头鉴权 --------------------
119
  function extractInboundAuthKey(
120
  request: Request,
@@ -134,17 +144,14 @@ function extractInboundAuthKey(
134
  }
135
 
136
  function checkInboundAuth(request: Request, provider: string): boolean {
137
- const envAuthKey = Deno.env.get("AUTH_KEY"); // 环境变量名通常大写
138
- console.log(envAuthKey)
139
  if (!envAuthKey) {
140
- // 未在环境变量中配置 authKey 者无法执行鉴权流程,视作鉴权失败
141
  return false;
142
  }
143
  const inboundKey = extractInboundAuthKey(request, provider);
144
  if (inboundKey === envAuthKey) {
145
  return true;
146
  }
147
- // 当 Gemini 有关的请求无法通过 "x-goog-api-key" 头完成鉴权时,尝试通过 url 中的参数 key 鉴权
148
  if (provider === "gemini") {
149
  const url = new URL(request.url);
150
  const keyParam = url.searchParams.get("key");
@@ -158,7 +165,6 @@ serve(async (request: Request) => {
158
  const url = new URL(request.url);
159
  const pathname = url.pathname + url.search;
160
 
161
- // 特殊路由:根路径, /robots.txt
162
  if (pathname === "/" || pathname === "/index.html") {
163
  return new Response("Service is running!", {
164
  status: 200,
@@ -172,14 +178,12 @@ serve(async (request: Request) => {
172
  });
173
  }
174
 
175
- // 解析目标地址,判断是否启用密钥轮询
176
  let { provider, targetUrl } = parseTarget(pathname);
177
  if (!targetUrl) {
178
  return new Response("无效路径: " + pathname, { status: 404 });
179
  }
180
  const rotationEnabled = rotationConfig[provider] ?? false;
181
 
182
- // 过滤敏感请求头
183
  const headers = new Headers();
184
  for (const [key, value] of request.headers.entries()) {
185
  if (isAllowedHeader(key)) {
@@ -187,7 +191,6 @@ serve(async (request: Request) => {
187
  }
188
  }
189
 
190
- // -------------------- 无需轮询者直接转发 --------------------
191
  if (!rotationEnabled) {
192
  try {
193
  return await fetch(targetUrl, {
@@ -203,7 +206,6 @@ serve(async (request: Request) => {
203
  }
204
  }
205
 
206
- // -------------------- 需轮询者,鉴权后重新构造请求头与转发 --------------------
207
  if (!checkInboundAuth(request, provider)) {
208
  return new Response("轮询模式:脚本鉴权未通过", { status: 401 });
209
  }
@@ -215,22 +217,18 @@ serve(async (request: Request) => {
215
  });
216
  }
217
 
218
- // 移除原有的鉴权请求头
219
  headers.delete("Authorization");
220
  headers.delete("x-api-key");
221
  headers.delete("x-goog-api-key");
222
 
223
- // 轮询模式代理 Gemini 时,删去 URL 中的参数 key
224
  if (provider === "gemini") {
225
  const targetUrlObj = new URL(targetUrl);
226
  targetUrlObj.searchParams.delete("key");
227
  targetUrl = targetUrlObj.toString();
228
  }
229
 
230
- // 读取并更新内存中的轮询索引,确定使用的密钥
231
  let currentIndex: number;
232
  try {
233
- // 调用改造后的函数
234
  currentIndex = await getAndIncrementRotationIndex(provider, keyPool.length);
235
  } catch (err) {
236
  return new Response(`轮询模式:密钥轮询索引更新失败: ${err.message}`, {
@@ -239,7 +237,6 @@ serve(async (request: Request) => {
239
  }
240
  const apiKey = keyPool[currentIndex];
241
 
242
- // 根据 authHeaderMapping 确定目标格式,构造新的鉴权请求头
243
  const buildAuthHeader = authHeaderMapping[provider];
244
  let authHeaderKey = "Authorization";
245
  let authHeaderValue = `Bearer ${apiKey}`;
@@ -248,7 +245,6 @@ serve(async (request: Request) => {
248
  }
249
  headers.set(authHeaderKey, authHeaderValue);
250
 
251
- // 发起请求
252
  try {
253
  return await fetch(targetUrl, {
254
  method: request.method,
 
1
  import { serve } from "https://deno.land/std@0.178.0/http/server.ts";
2
 
3
+ // -------------------- 改造部分:使用内存 Map 和自定义的异步锁 --------------------
4
  // 使用一个 Map 在内存中存储每个 provider 的轮询索引
5
  const rotationIndexes = new Map<string, number>();
6
+ // 使用一个 Set 来实现一个简单的 provider 锁,防止并发更新时的竞态条件
7
+ const providerLocks = new Set<string>();
8
+
9
+ /**
10
+ * 延时函数
11
+ * @param ms 毫秒数
12
+ */
13
+ const delay = (ms: number) => new Promise(res => setTimeout(res, ms));
14
+
15
+ /**
16
+ * 改造后的函数 v2:
17
+ * 使用自定义的异步锁来原子性地获取并递增轮询索引。
18
+ * 这可以防止并发请求时多个请求拿到同一个索引。
19
+ * @param provider - 服务商名称
20
+ * @param length - 密钥池的长度
21
+ * @returns 将用于当前请求的索引
22
+ */
23
+ async function getAndIncrementRotationIndex(
24
+ provider: string,
25
+ length: number,
26
+ ): Promise<number> {
27
+ // 1. 等待获取锁
28
+ // 当 provider 在锁集合中时,持续等待
29
+ while (providerLocks.has(provider)) {
30
+ await delay(10); // 等待10毫秒后重试,避免CPU空转
31
+ }
32
+
33
+ try {
34
+ // 2. 成功获取锁
35
+ providerLocks.add(provider);
36
+
37
+ // --- 临界区开始 (Critical Section) ---
38
+ // 这部分代码在同一时间只允许一个请求为同一个 provider 执行
39
+ const currentIndex = rotationIndexes.get(provider) ?? 0;
40
+ const nextIndex = (currentIndex + 1) % length;
41
+ rotationIndexes.set(provider, nextIndex);
42
+ // --- 临界区结束 ---
43
+
44
+ return currentIndex;
45
+ } finally {
46
+ // 3. 释放锁
47
+ // 无论 try 块中是否发生错误,都必须释放锁,否则会造成死锁
48
+ providerLocks.delete(provider);
49
+ }
50
+ }
51
  // -------------------- 改造部分结束 --------------------
52
 
53
 
 
73
  "/telegram": "https://api.telegram.org",
74
  };
75
 
76
+ // 密钥轮询配置
 
77
  const rotationConfig: Record<string, boolean> = {
78
  gemini: true,
79
  groq: true,
80
  openrouter: true
81
  };
82
 
83
+ // 鉴权请求头格式声明
 
84
  const authHeaderMapping: Record<string, (apiKey: string) => [string, string]> = {
85
  gemini: (apiKey: string) => ["x-goog-api-key", apiKey],
86
  claude: (apiKey: string) => ["x-api-key", apiKey],
 
119
 
120
  // -------------------- 4. 处理密钥池与轮询索引 --------------------
121
  async function getKeyPool(provider: string): Promise<string[]> {
122
+ const envKeyName = `${provider.toUpperCase()}_KEYS`;
123
  const keyPoolStr = Deno.env.get(envKeyName);
124
  if (!keyPoolStr) return [];
125
  return keyPoolStr.split(",").map((k) => k.trim()).filter((k) => k.length > 0);
126
  }
127
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
128
  // -------------------- 5. 传入请求头鉴权 --------------------
129
  function extractInboundAuthKey(
130
  request: Request,
 
144
  }
145
 
146
  function checkInboundAuth(request: Request, provider: string): boolean {
147
+ const envAuthKey = Deno.env.get("AUTH_KEY");
 
148
  if (!envAuthKey) {
 
149
  return false;
150
  }
151
  const inboundKey = extractInboundAuthKey(request, provider);
152
  if (inboundKey === envAuthKey) {
153
  return true;
154
  }
 
155
  if (provider === "gemini") {
156
  const url = new URL(request.url);
157
  const keyParam = url.searchParams.get("key");
 
165
  const url = new URL(request.url);
166
  const pathname = url.pathname + url.search;
167
 
 
168
  if (pathname === "/" || pathname === "/index.html") {
169
  return new Response("Service is running!", {
170
  status: 200,
 
178
  });
179
  }
180
 
 
181
  let { provider, targetUrl } = parseTarget(pathname);
182
  if (!targetUrl) {
183
  return new Response("无效路径: " + pathname, { status: 404 });
184
  }
185
  const rotationEnabled = rotationConfig[provider] ?? false;
186
 
 
187
  const headers = new Headers();
188
  for (const [key, value] of request.headers.entries()) {
189
  if (isAllowedHeader(key)) {
 
191
  }
192
  }
193
 
 
194
  if (!rotationEnabled) {
195
  try {
196
  return await fetch(targetUrl, {
 
206
  }
207
  }
208
 
 
209
  if (!checkInboundAuth(request, provider)) {
210
  return new Response("轮询模式:脚本鉴权未通过", { status: 401 });
211
  }
 
217
  });
218
  }
219
 
 
220
  headers.delete("Authorization");
221
  headers.delete("x-api-key");
222
  headers.delete("x-goog-api-key");
223
 
 
224
  if (provider === "gemini") {
225
  const targetUrlObj = new URL(targetUrl);
226
  targetUrlObj.searchParams.delete("key");
227
  targetUrl = targetUrlObj.toString();
228
  }
229
 
 
230
  let currentIndex: number;
231
  try {
 
232
  currentIndex = await getAndIncrementRotationIndex(provider, keyPool.length);
233
  } catch (err) {
234
  return new Response(`轮询模式:密钥轮询索引更新失败: ${err.message}`, {
 
237
  }
238
  const apiKey = keyPool[currentIndex];
239
 
 
240
  const buildAuthHeader = authHeaderMapping[provider];
241
  let authHeaderKey = "Authorization";
242
  let authHeaderValue = `Bearer ${apiKey}`;
 
245
  }
246
  headers.set(authHeaderKey, authHeaderValue);
247
 
 
248
  try {
249
  return await fetch(targetUrl, {
250
  method: request.method,