nanoppa commited on
Commit
42e76d2
·
verified ·
1 Parent(s): 37d729e

Create main.ts

Browse files
Files changed (1) hide show
  1. main.ts +263 -0
main.ts ADDED
@@ -0,0 +1,263 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+
9
+ // -------------------- 1. 自定义配置--------------------
10
+ // 路径映射
11
+ // 键名中斜杠后的部分(如`gemini`)将会作为后续变量 provider 的值,用以标记服务商
12
+ const apiMapping: Record<string, string> = {
13
+ "/openai": "https://api.openai.com",
14
+ "/claude": "https://api.anthropic.com",
15
+ "/gemini": "https://generativelanguage.googleapis.com",
16
+ "/xai": "https://api.x.ai",
17
+ "/groq": "https://api.groq.com/openai",
18
+ "/openrouter": "https://openrouter.ai/api",
19
+ "/meta": "https://www.meta.ai/api",
20
+ "/cohere": "https://api.cohere.ai",
21
+ "/huggingface": "https://api-inference.huggingface.co",
22
+ "/together": "https://api.together.xyz",
23
+ "/novita": "https://api.novita.ai",
24
+ "/portkey": "https://api.portkey.ai",
25
+ "/fireworks": "https://api.fireworks.ai",
26
+ "/cerebras": "https://api.cerebras.ai",
27
+ "/discord": "https://discord.com/api",
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],
44
+ };
45
+
46
+ // -------------------- 2. 敏感请求头过滤 --------------------
47
+ const deniedHeaders = ["host", "referer", "cf-", "forward", "cdn"];
48
+ function isAllowedHeader(key: string): boolean {
49
+ const lowerKey = key.toLowerCase();
50
+ for (const deniedHeader of deniedHeaders) {
51
+ if (lowerKey.includes(deniedHeader)) {
52
+ return false;
53
+ }
54
+ }
55
+ return true;
56
+ }
57
+
58
+ // -------------------- 3. parseTarget:尝试匹配 provider 并且提取路径 --------------------
59
+ function parseTarget(urlPath: string): { provider: string; targetUrl: string } {
60
+ const splitIndex = urlPath.indexOf("/", 1);
61
+ if (splitIndex === -1) {
62
+ return { provider: "", targetUrl: "" };
63
+ }
64
+ const prefix = urlPath.substring(0, splitIndex);
65
+ const provider = prefix.replace("/", "");
66
+ const mappedBase = apiMapping[prefix];
67
+ if (!mappedBase) {
68
+ return { provider: "", targetUrl: "" };
69
+ }
70
+ const restPath = urlPath.substring(prefix.length);
71
+ return {
72
+ provider,
73
+ targetUrl: mappedBase + restPath,
74
+ };
75
+ }
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,
121
+ provider: string,
122
+ ): string | undefined {
123
+ const buildAuthHeader = authHeaderMapping[provider];
124
+ if (buildAuthHeader) {
125
+ const [customHeaderKey] = buildAuthHeader("");
126
+ return request.headers.get(customHeaderKey) || undefined;
127
+ } else {
128
+ const incomingAuth = request.headers.get("Authorization");
129
+ if (!incomingAuth || !incomingAuth.startsWith("Bearer ")) {
130
+ return undefined;
131
+ }
132
+ return incomingAuth.substring("Bearer ".length).trim();
133
+ }
134
+ }
135
+
136
+ function checkInboundAuth(request: Request, provider: string): boolean {
137
+ const envAuthKey = Deno.env.get("AUTH_KEY"); // 环境变量名通常大写
138
+ if (!envAuthKey) {
139
+ // 未在环境变量中配置 authKey 者无法执行鉴权流程,视作鉴权失败
140
+ return false;
141
+ }
142
+ const inboundKey = extractInboundAuthKey(request, provider);
143
+ if (inboundKey === envAuthKey) {
144
+ return true;
145
+ }
146
+ // 当 Gemini 有关的请求无法通过 "x-goog-api-key" 头完成鉴权时,尝试通过 url 中的参数 key 鉴权
147
+ if (provider === "gemini") {
148
+ const url = new URL(request.url);
149
+ const keyParam = url.searchParams.get("key");
150
+ return keyParam === envAuthKey;
151
+ }
152
+ return false;
153
+ }
154
+
155
+ // -------------------- 6. 启动服务,处理请求 --------------------
156
+ serve(async (request: Request) => {
157
+ const url = new URL(request.url);
158
+ const pathname = url.pathname + url.search;
159
+
160
+ // 特殊路由:根路径, /robots.txt
161
+ if (pathname === "/" || pathname === "/index.html") {
162
+ return new Response("Service is running!", {
163
+ status: 200,
164
+ headers: { "Content-Type": "text/html" },
165
+ });
166
+ }
167
+ if (pathname === "/robots.txt") {
168
+ return new Response("User-agent: *\nDisallow: /", {
169
+ status: 200,
170
+ headers: { "Content-Type": "text/plain" },
171
+ });
172
+ }
173
+
174
+ // 解析目标地址,判断是否启用密钥轮询
175
+ let { provider, targetUrl } = parseTarget(pathname);
176
+ if (!targetUrl) {
177
+ return new Response("无效路径: " + pathname, { status: 404 });
178
+ }
179
+ const rotationEnabled = rotationConfig[provider] ?? false;
180
+
181
+ // 过滤敏感请求头
182
+ const headers = new Headers();
183
+ for (const [key, value] of request.headers.entries()) {
184
+ if (isAllowedHeader(key)) {
185
+ headers.set(key, value);
186
+ }
187
+ }
188
+
189
+ // -------------------- 无需轮询者直接转发 --------------------
190
+ if (!rotationEnabled) {
191
+ try {
192
+ return await fetch(targetUrl, {
193
+ method: request.method,
194
+ headers,
195
+ body: request.body,
196
+ });
197
+ } catch (error) {
198
+ const message = `转发模式: ${targetUrl}\n\n` +
199
+ `错误消息: ${error.message}\n\n` +
200
+ `堆栈跟踪:\n${error.stack ?? 'No stack trace'}`;
201
+ return new Response(message, { status: 500 });
202
+ }
203
+ }
204
+
205
+ // -------------------- 需轮询者,鉴权后重新构造请求头与转发 --------------------
206
+ if (!checkInboundAuth(request, provider)) {
207
+ return new Response("轮询模式:脚本鉴权未通过", { status: 401 });
208
+ }
209
+
210
+ const keyPool = await getKeyPool(provider);
211
+ if (keyPool.length === 0) {
212
+ return new Response(`轮询模式:${provider} 缺少密钥池`, {
213
+ status: 500,
214
+ });
215
+ }
216
+
217
+ // 移除原有的鉴权请求头
218
+ headers.delete("Authorization");
219
+ headers.delete("x-api-key");
220
+ headers.delete("x-goog-api-key");
221
+
222
+ // 轮询模式代理 Gemini 时,删去 URL 中的参数 key
223
+ if (provider === "gemini") {
224
+ const targetUrlObj = new URL(targetUrl);
225
+ targetUrlObj.searchParams.delete("key");
226
+ targetUrl = targetUrlObj.toString();
227
+ }
228
+
229
+ // 读取并更新内存中的轮询索引,确定使用的密钥
230
+ let currentIndex: number;
231
+ try {
232
+ // 调用改造后的函数
233
+ currentIndex = await getAndIncrementRotationIndex(provider, keyPool.length);
234
+ } catch (err) {
235
+ return new Response(`轮询模式:密钥轮询索引更新失败: ${err.message}`, {
236
+ status: 500,
237
+ });
238
+ }
239
+ const apiKey = keyPool[currentIndex];
240
+
241
+ // 根据 authHeaderMapping 确定目标格式,构造新的鉴权请求头
242
+ const buildAuthHeader = authHeaderMapping[provider];
243
+ let authHeaderKey = "Authorization";
244
+ let authHeaderValue = `Bearer ${apiKey}`;
245
+ if (buildAuthHeader) {
246
+ [authHeaderKey, authHeaderValue] = buildAuthHeader(apiKey);
247
+ }
248
+ headers.set(authHeaderKey, authHeaderValue);
249
+
250
+ // 发起请求
251
+ try {
252
+ return await fetch(targetUrl, {
253
+ method: request.method,
254
+ headers,
255
+ body: request.body,
256
+ });
257
+ } catch (error) {
258
+ const message = `轮询模式 - 请求失败: ${targetUrl}\n\n` +
259
+ `错误消息: ${error.message}\n\n` +
260
+ `堆栈跟踪:\n${error.stack ?? 'No stack trace'}`;
261
+ return new Response(message, { status: 500 });
262
+ }
263
+ },{port:7860});