Spaces:
Running
Running
Update main.ts
Browse files
main.ts
CHANGED
@@ -1,429 +1,212 @@
|
|
1 |
-
|
2 |
|
3 |
-
|
4 |
-
|
5 |
-
const TARGET_URL = 'https://assistant.on.adaptive.ai/api/sendMessage';
|
6 |
-
const PROXY_MODEL_NAME = "gpt-4o"; // 代理服务返回的模型名称
|
7 |
|
|
|
|
|
8 |
|
9 |
-
|
10 |
-
|
11 |
-
|
12 |
-
|
13 |
-
|
14 |
-
// --- 辅助函数:创建 SSE 数据块 ---
|
15 |
-
function createSSEChunk(id: string, model: string, content: string | null, role: string | null, finish_reason: string | null): string {
|
16 |
-
const now = Math.floor(Date.now() / 1000);
|
17 |
-
const chunk: any = {
|
18 |
-
id: id,
|
19 |
-
object: "chat.completion.chunk",
|
20 |
-
created: now,
|
21 |
-
model: model,
|
22 |
-
choices: [
|
23 |
-
{
|
24 |
-
index: 0,
|
25 |
-
delta: {},
|
26 |
-
finish_reason: finish_reason,
|
27 |
-
logprobs: null,
|
28 |
-
}
|
29 |
-
],
|
30 |
-
// system_fingerprint: null, // 可选
|
31 |
-
};
|
32 |
-
if (role) {
|
33 |
-
chunk.choices[0].delta.role = role;
|
34 |
-
}
|
35 |
-
if (content) {
|
36 |
-
chunk.choices[0].delta.content = content;
|
37 |
-
}
|
38 |
-
// 如果 delta 为空且有 finish_reason,确保 delta 是空对象
|
39 |
-
if (!role && !content && finish_reason) {
|
40 |
-
chunk.choices[0].delta = {};
|
41 |
-
}
|
42 |
-
|
43 |
-
return `data: ${JSON.stringify(chunk)}\n\n`;
|
44 |
}
|
45 |
|
46 |
-
|
47 |
-
|
48 |
-
|
49 |
-
|
50 |
-
|
51 |
-
|
52 |
-
*/
|
53 |
-
async function createChatAndGetId(): Promise<string> {
|
54 |
-
const url = 'https://assistant.on.adaptive.ai/api/createChat';
|
55 |
-
// 定义请求头 (省略了 cookie 和 priority)
|
56 |
-
const headers = {
|
57 |
-
'content-type': 'application/json',
|
58 |
-
};
|
59 |
-
// 构建请求体,使用当前时间戳作为请求 ID
|
60 |
-
const payload = {
|
61 |
-
json: {
|
62 |
-
jsonrpc: "2.0",
|
63 |
-
id: Date.now(), // 使用动态 ID
|
64 |
-
method: "createChat",
|
65 |
-
params: []
|
66 |
-
}
|
67 |
-
};
|
68 |
-
console.log("正在发送创建聊天请求...");
|
69 |
-
try {
|
70 |
-
const response = await fetch(url, {
|
71 |
-
method: 'POST',
|
72 |
-
headers: headers,
|
73 |
-
body: JSON.stringify(payload) // 将 payload 对象转换为 JSON 字符串
|
74 |
-
});
|
75 |
-
// 检查 HTTP 响应状态码是否表示成功
|
76 |
-
if (!response.ok) {
|
77 |
-
let errorBody = "无法读取响应体";
|
78 |
-
try {
|
79 |
-
errorBody = await response.text(); // 尝试读取错误响应体
|
80 |
-
} catch (readError) {
|
81 |
-
console.warn("读取错误响应体失败:", readError);
|
82 |
-
}
|
83 |
-
throw new Error(`创建聊天请求失败,HTTP 状态码: ${response.status}. 响应: ${errorBody}`);
|
84 |
-
}
|
85 |
-
// 解析 JSON 响应体
|
86 |
-
let responseData: any;
|
87 |
-
try {
|
88 |
-
responseData = await response.json();
|
89 |
-
} catch (parseError) {
|
90 |
-
// 如果响应不是有效的 JSON,则抛出错误
|
91 |
-
console.error("解析创建聊天响应 JSON 时出错:", parseError);
|
92 |
-
throw new Error(`无法将响应解析为 JSON: ${parseError.message}`);
|
93 |
-
}
|
94 |
-
// 提取并验证 ID
|
95 |
-
// ID 预期在 responseData.json.result.id
|
96 |
-
// 使用可选链操作符 (?.) 来安全地访问嵌套属性,防止因中间属性不存在而报错
|
97 |
-
const chatId = responseData?.json?.result?.id;
|
98 |
-
// 检查提取到的 ID 是否是一个有效的、非空的字符串
|
99 |
-
if (typeof chatId === 'string' && chatId.length > 0) {
|
100 |
-
console.log(`成功创建聊天,获取到 ID: ${chatId}`);
|
101 |
-
return chatId; // 返回提取到的 ID
|
102 |
-
} else {
|
103 |
-
// 如果 ID 不存在或格式不正确,则抛出错误
|
104 |
-
console.error("从响应中未能提取有效的聊天 ID。响应数据:", JSON.stringify(responseData));
|
105 |
-
throw new Error("创建聊天的响应格式无效或缺少 'json.result.id' 字段。");
|
106 |
-
}
|
107 |
-
} catch (error) {
|
108 |
-
// 捕获 try 块中抛出的任何错误,或 fetch 本身的网络错误
|
109 |
-
const errorMessage = error instanceof Error ? error.message : String(error);
|
110 |
-
console.error("执行 createChatAndGetId 时发生错误:", errorMessage); // 记录详细错误信息
|
111 |
-
// 创建并返回一个 Response 对象
|
112 |
-
// 状态码为 500 (Internal Server Error)
|
113 |
-
// 响应体为 JSON 格式,包含错误信息
|
114 |
-
return new Response(
|
115 |
-
JSON.stringify({ error: `创建聊天会话失败: ${errorMessage}` }), // 将错误信息包装在 JSON 对象中
|
116 |
-
{
|
117 |
-
status: 500, // 设置 HTTP 状态码为 500
|
118 |
-
headers: {
|
119 |
-
"Content-Type": "application/json", // 设置响应内容类型为 JSON
|
120 |
-
"Access-Control-Allow-Origin": "*" // 如果需要跨域,添加此头
|
121 |
-
}
|
122 |
-
}
|
123 |
-
);
|
124 |
-
}
|
125 |
-
|
126 |
}
|
127 |
|
128 |
-
// ---
|
129 |
-
|
130 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
131 |
|
132 |
-
|
133 |
-
|
134 |
-
|
135 |
-
|
136 |
-
|
137 |
-
|
138 |
-
|
139 |
-
|
140 |
-
|
141 |
-
|
142 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
143 |
}
|
144 |
|
145 |
-
|
146 |
-
|
147 |
-
if (url.pathname === "/v1/models" && req.method === "GET") {
|
148 |
-
return new Response(
|
149 |
-
JSON.stringify({
|
150 |
-
object: "list",
|
151 |
-
data: [
|
152 |
-
{
|
153 |
-
id: "gpt-4o",
|
154 |
-
object: "model",
|
155 |
-
created: 0,
|
156 |
-
owned_by: "unlimitedai",
|
157 |
-
permission: [{
|
158 |
-
id: "modelperm-gpt-4o",
|
159 |
-
object: "model_permission",
|
160 |
-
created: 0,
|
161 |
-
allow_create_engine: false,
|
162 |
-
allow_sampling: true,
|
163 |
-
allow_logprobs: false,
|
164 |
-
allow_search_indices: false,
|
165 |
-
allow_view: true,
|
166 |
-
allow_fine_tuning: false,
|
167 |
-
organization: "*",
|
168 |
-
group: null,
|
169 |
-
is_blocking: false,
|
170 |
-
}],
|
171 |
-
root: "gpt-4o",
|
172 |
-
parent: null,
|
173 |
-
},
|
174 |
-
],
|
175 |
-
}),
|
176 |
-
{
|
177 |
-
status: 200,
|
178 |
-
headers: {
|
179 |
-
"Content-Type": "application/json",
|
180 |
-
"Access-Control-Allow-Origin": "*",
|
181 |
-
},
|
182 |
-
}
|
183 |
-
);
|
184 |
}
|
185 |
|
186 |
-
|
187 |
-
|
188 |
-
return new Response(JSON.stringify({ error: "Not Found or Method Not Allowed" }), {
|
189 |
-
status: 404,
|
190 |
-
headers: {
|
191 |
-
"Content-Type": "application/json",
|
192 |
-
"Access-Control-Allow-Origin": "*",
|
193 |
-
},
|
194 |
-
});
|
195 |
-
}
|
196 |
-
// --- 添加认证检查 ---
|
197 |
-
const authHeader = req.headers.get("Authorization");
|
198 |
-
let providedKey = "";
|
199 |
-
// 检查 Authorization header 是否存在且格式正确 (Bearer <key>)
|
200 |
-
if (!authHeader || !authHeader.toLowerCase().startsWith("bearer ")) {
|
201 |
-
console.warn(`认证失败: 缺少或格式错误的 Authorization header`);
|
202 |
-
return new Response(JSON.stringify({
|
203 |
-
error: {
|
204 |
-
message: "Unauthorized: Missing or invalid Authorization header. Use 'Bearer <YOUR_API_KEY>' format.",
|
205 |
-
type: "invalid_request_error",
|
206 |
-
param: null,
|
207 |
-
code: "missing_or_invalid_header"
|
208 |
-
}
|
209 |
-
}), {
|
210 |
-
status: 401, // Unauthorized
|
211 |
-
headers: {
|
212 |
-
"Content-Type": "application/json",
|
213 |
-
"Access-Control-Allow-Origin": "*",
|
214 |
-
"WWW-Authenticate": 'Bearer realm="API Access"'
|
215 |
-
}
|
216 |
-
});
|
217 |
-
}
|
218 |
-
// 提取 key 部分
|
219 |
-
providedKey = authHeader.substring(7); // "Bearer ".length is 7
|
220 |
-
console.log("providedKey:" + providedKey);
|
221 |
-
// 直接比较提供的 key 和硬编码的 key
|
222 |
-
if (providedKey !== AUTH_KEY) {
|
223 |
-
console.warn(`认证失败: 无效的 API Key 提供`);
|
224 |
-
return new Response(JSON.stringify({
|
225 |
-
error: {
|
226 |
-
message: "Unauthorized: Invalid API Key provided.",
|
227 |
-
type: "invalid_request_error",
|
228 |
-
param: null,
|
229 |
-
code: "inAUTH_KEY"
|
230 |
-
}
|
231 |
-
}), {
|
232 |
-
status: 401, // Unauthorized
|
233 |
-
headers: {
|
234 |
-
"Content-Type": "application/json",
|
235 |
-
"Access-Control-Allow-Origin": "*",
|
236 |
-
"WWW-Authenticate": 'Bearer realm="API Access"'
|
237 |
-
}
|
238 |
-
});
|
239 |
-
}
|
240 |
|
241 |
-
// --- 处理 POST 请求 ---
|
242 |
try {
|
243 |
-
|
244 |
-
|
245 |
-
|
246 |
-
|
247 |
-
|
248 |
-
|
249 |
-
|
250 |
-
|
251 |
-
|
252 |
-
|
253 |
-
|
|
|
|
|
|
|
254 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
255 |
|
256 |
-
|
257 |
-
|
258 |
-
|
259 |
-
|
260 |
-
|
261 |
-
|
262 |
-
|
263 |
-
|
264 |
-
|
265 |
-
|
266 |
-
|
267 |
-
|
268 |
-
return new Response(JSON.stringify({ error: "Failed to process 'messages' array." }), {
|
269 |
-
status: 400, headers: { "Content-Type": "application/json", "Access-Control-Allow-Origin": "*" },
|
270 |
-
});
|
271 |
-
}
|
272 |
-
}
|
273 |
-
// 检查 userContent 是否成功生成
|
274 |
-
// 如果 requestBody.messages 不存在、不是数组、为空,或者转换出错,userContent 会是 undefined
|
275 |
-
if (!userContent) {
|
276 |
-
console.error("Request body must contain a non-empty 'messages' array.");
|
277 |
-
return new Response(JSON.stringify({ error: "Request body must contain a non-empty 'messages' array." }), {
|
278 |
-
status: 400, headers: { "Content-Type": "application/json", "Access-Control-Allow-Origin": "*" },
|
279 |
-
});
|
280 |
-
}
|
281 |
-
|
282 |
-
// 现在 userContent 包含了整个对话历史的字符串表示
|
283 |
-
console.log("Formatted user content:", userContent); // 可以取消注释来调试输出
|
284 |
-
|
285 |
-
const CHAT_ID = await createChatAndGetId();//获取新的聊天ID
|
286 |
-
|
287 |
-
|
288 |
-
// 4. 构建目标 API Payload
|
289 |
-
const payload = {
|
290 |
-
json: {
|
291 |
-
jsonrpc: "2.0", id: Date.now(), method: "sendMessage",
|
292 |
-
params: [{ chatId: CHAT_ID, content: userContent, fileId: null, fileIds: [] }]
|
293 |
-
},
|
294 |
-
meta: { values: { "params.0.fileId": ["undefined"] } }
|
295 |
-
};
|
296 |
|
297 |
-
|
298 |
-
|
299 |
-
|
300 |
-
|
301 |
-
|
|
|
|
|
|
|
|
|
|
|
302 |
|
303 |
-
|
304 |
-
if (!targetResponse.ok) {
|
305 |
-
const errorBody = await targetResponse.text();
|
306 |
-
console.error(`Target API Error (${targetResponse.status}):`, errorBody);
|
307 |
-
// 即使是流式请求失败,也返回 JSON 错误
|
308 |
-
return new Response(JSON.stringify({ error: `Upstream API request failed with status ${targetResponse.status}`, details: errorBody }), {
|
309 |
-
status: 502, headers: { "Content-Type": "application/json", "Access-Control-Allow-Origin": "*" },
|
310 |
-
});
|
311 |
-
}
|
312 |
|
313 |
-
|
314 |
-
|
315 |
try {
|
316 |
-
|
317 |
-
|
318 |
-
|
319 |
-
|
320 |
-
|
321 |
-
|
322 |
-
});
|
323 |
-
}
|
324 |
-
|
325 |
-
// 8. 从目标响应中提取内容
|
326 |
-
const assistantContent = targetData?.json?.result?.content;
|
327 |
-
if (typeof assistantContent !== 'string') {
|
328 |
-
console.error("Could not extract 'content' from target API response:", JSON.stringify(targetData));
|
329 |
-
// 即使是流式请求失败,也返回 JSON 错误
|
330 |
-
return new Response(JSON.stringify({ error: "Invalid response format from upstream API" }), {
|
331 |
-
status: 500, headers: { "Content-Type": "application/json", "Access-Control-Allow-Origin": "*" },
|
332 |
-
});
|
333 |
-
}
|
334 |
-
|
335 |
-
// 9. 根据 isStream 决定返回格式
|
336 |
-
const chatCompletionId = `chatcmpl-${crypto.randomUUID()}`; // 为本次交互生成唯一 ID
|
337 |
-
const modelName = requestBody.model || PROXY_MODEL_NAME; // 确定模型名称
|
338 |
-
|
339 |
-
if (isStream) {
|
340 |
-
// --- 返回模拟的流式响应 ---
|
341 |
-
console.log("Simulating stream response...");
|
342 |
-
const encoder = new TextEncoder();
|
343 |
-
const stream = new ReadableStream({
|
344 |
-
async start(controller) {
|
345 |
-
try {
|
346 |
-
// 模拟发送块
|
347 |
-
// 块 1: 发送角色信息
|
348 |
-
controller.enqueue(encoder.encode(
|
349 |
-
createSSEChunk(chatCompletionId, modelName, null, "assistant", null)
|
350 |
-
));
|
351 |
-
await new Promise(resolve => setTimeout(resolve, 10)); // 短暂延迟,模拟处理
|
352 |
-
|
353 |
-
// 块 2: 发送完整内容
|
354 |
-
controller.enqueue(encoder.encode(
|
355 |
-
createSSEChunk(chatCompletionId, modelName, assistantContent, null, null)
|
356 |
-
));
|
357 |
-
await new Promise(resolve => setTimeout(resolve, 10)); // 短暂延迟
|
358 |
-
|
359 |
-
// 块 3: 发送结束信号
|
360 |
-
controller.enqueue(encoder.encode(
|
361 |
-
createSSEChunk(chatCompletionId, modelName, null, null, "stop")
|
362 |
-
));
|
363 |
-
|
364 |
-
// 发送 [DONE] 标记
|
365 |
-
controller.enqueue(encoder.encode("data: [DONE]\n\n"));
|
366 |
-
|
367 |
-
// 关闭流
|
368 |
-
controller.close();
|
369 |
-
} catch (error) {
|
370 |
-
console.error("Error during stream simulation:", error);
|
371 |
-
controller.error(error); // 通知流出错了
|
372 |
-
}
|
373 |
-
}
|
374 |
-
});
|
375 |
-
|
376 |
-
return new Response(stream, {
|
377 |
-
status: 200,
|
378 |
-
headers: {
|
379 |
-
'Content-Type': 'text/event-stream',
|
380 |
-
'Cache-Control': 'no-cache',
|
381 |
-
'Connection': 'keep-alive', // 建议 SSE 使用
|
382 |
-
'Access-Control-Allow-Origin': '*'
|
383 |
-
},
|
384 |
-
});
|
385 |
-
|
386 |
-
} else {
|
387 |
-
// --- 返回完整的 JSON 响应 ---
|
388 |
-
console.log("Returning non-stream response.");
|
389 |
-
const finalResponse = {
|
390 |
-
id: chatCompletionId,
|
391 |
-
object: "chat.completion",
|
392 |
-
created: Math.floor(Date.now() / 1000),
|
393 |
-
model: modelName,
|
394 |
-
choices: [
|
395 |
-
{
|
396 |
-
index: 0,
|
397 |
-
message: {
|
398 |
-
role: "assistant",
|
399 |
-
content: assistantContent,
|
400 |
-
},
|
401 |
-
finish_reason: "stop",
|
402 |
-
logprobs: null,
|
403 |
-
}
|
404 |
-
],
|
405 |
-
usage: { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 },
|
406 |
-
};
|
407 |
-
|
408 |
-
return new Response(JSON.stringify(finalResponse), {
|
409 |
-
status: 200,
|
410 |
-
headers: {
|
411 |
-
'Content-Type': 'application/json',
|
412 |
-
'Access-Control-Allow-Origin': '*'
|
413 |
-
},
|
414 |
-
});
|
415 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
416 |
|
417 |
-
|
418 |
-
|
419 |
-
|
420 |
-
|
421 |
-
|
422 |
-
|
423 |
-
|
424 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
425 |
}
|
|
|
|
|
|
|
|
|
426 |
}
|
427 |
|
|
|
|
|
|
|
|
|
|
|
|
|
428 |
|
429 |
-
serve(handler, { port
|
|
|
1 |
+
// main.ts - Deno 版本的 ish2api 服务 (v1.0.2 - With Sponsor Adblock)
|
2 |
|
3 |
+
import { serve } from "https://deno.land/[email protected]/http/server.ts";
|
4 |
+
import { load } from "https://deno.land/[email protected]/dotenv/mod.ts";
|
|
|
|
|
5 |
|
6 |
+
// 加载环境变量
|
7 |
+
const env = await load();
|
8 |
|
9 |
+
// --- 类型定义 ---
|
10 |
+
interface ChatMessage {
|
11 |
+
role: string;
|
12 |
+
content: string;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
13 |
}
|
14 |
|
15 |
+
interface OpenAIChatRequest {
|
16 |
+
model: string;
|
17 |
+
messages: ChatMessage[];
|
18 |
+
max_tokens?: number;
|
19 |
+
temperature?: number;
|
20 |
+
stream?: boolean;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
21 |
}
|
22 |
|
23 |
+
// --- 配置 ---
|
24 |
+
const POLLINATIONS_HEADERS = {
|
25 |
+
'Accept': '*/*',
|
26 |
+
'Accept-Encoding': 'gzip, deflate, br, zstd',
|
27 |
+
'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8',
|
28 |
+
'Content-Type': 'application/json',
|
29 |
+
'Origin': 'https://ish.junioralive.in',
|
30 |
+
'Referer': 'https://ish.junioralive.in/',
|
31 |
+
'Sec-Ch-Ua': '"Not/A)Brand";v="8", "Chromium";v="126", "Microsoft Edge";v="126"',
|
32 |
+
'Sec-Ch-Ua-Mobile': '?0',
|
33 |
+
'Sec-Ch-Ua-Platform': '"Windows"',
|
34 |
+
'Sec-Fetch-Dest': 'empty',
|
35 |
+
'Sec-Fetch-Mode': 'cors',
|
36 |
+
'Sec-Fetch-Site': 'cross-site',
|
37 |
+
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36 Edg/126.0.0.0',
|
38 |
+
};
|
39 |
|
40 |
+
const TARGET_URL = env.TARGET_URL || Deno.env.get("TARGET_URL") || "https://text.pollinations.ai/openai";
|
41 |
+
|
42 |
+
// --- 核心:流式代理函数 ---
|
43 |
+
async function* streamProxy(requestBody: OpenAIChatRequest): AsyncGenerator<Uint8Array> {
|
44 |
+
try {
|
45 |
+
const response = await fetch(TARGET_URL, {
|
46 |
+
method: "POST",
|
47 |
+
headers: POLLINATIONS_HEADERS,
|
48 |
+
body: JSON.stringify(requestBody),
|
49 |
+
});
|
50 |
+
|
51 |
+
if (!response.ok) {
|
52 |
+
const errorText = await response.text();
|
53 |
+
const errorDetails = {
|
54 |
+
error: {
|
55 |
+
message: `Upstream API error: ${response.status}`,
|
56 |
+
type: "upstream_error",
|
57 |
+
details: errorText
|
58 |
+
}
|
59 |
+
};
|
60 |
+
const errorMessage = `data: ${JSON.stringify(errorDetails)}\n\n`;
|
61 |
+
yield new TextEncoder().encode(errorMessage);
|
62 |
+
console.error(`Error from upstream API: ${response.status} - ${errorText}`);
|
63 |
+
return;
|
64 |
}
|
65 |
|
66 |
+
if (!response.body) {
|
67 |
+
throw new Error("Response body is null");
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
68 |
}
|
69 |
|
70 |
+
const reader = response.body.getReader();
|
71 |
+
const textDecoder = new TextDecoder();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
72 |
|
|
|
73 |
try {
|
74 |
+
while (true) {
|
75 |
+
const { done, value } = await reader.read();
|
76 |
+
|
77 |
+
if (done) break;
|
78 |
+
|
79 |
+
// =================== 广告过滤逻辑开始 ===================
|
80 |
+
// 将二进制块解码为字符串以便检查内容
|
81 |
+
const chunkStr = textDecoder.decode(value, { stream: true });
|
82 |
+
|
83 |
+
// 检查解码后的字符串是否包含 "Sponsor" 关键词
|
84 |
+
if (chunkStr.includes("Sponsor")) {
|
85 |
+
console.log("Sponsor content detected. Stopping the stream to the client.");
|
86 |
+
// 发现广告,立即中断循环,不再向客户端发送任何数据
|
87 |
+
break;
|
88 |
}
|
89 |
+
|
90 |
+
// 如果没有广告,将原始的二进制块转发给客户端
|
91 |
+
yield value;
|
92 |
+
// =================== 广告过滤逻辑结束 ===================
|
93 |
+
}
|
94 |
+
} finally {
|
95 |
+
reader.releaseLock();
|
96 |
+
}
|
97 |
|
98 |
+
} catch (error) {
|
99 |
+
const errorDetails = {
|
100 |
+
error: {
|
101 |
+
message: `An unexpected error occurred: ${error.message}`,
|
102 |
+
type: "proxy_error"
|
103 |
+
}
|
104 |
+
};
|
105 |
+
const errorMessage = `data: ${JSON.stringify(errorDetails)}\n\n`;
|
106 |
+
yield new TextEncoder().encode(errorMessage);
|
107 |
+
console.error(`An unexpected error occurred: ${error}`);
|
108 |
+
}
|
109 |
+
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
110 |
|
111 |
+
// --- 路由处理 ---
|
112 |
+
async function handleChatCompletions(request: Request): Promise<Response> {
|
113 |
+
try {
|
114 |
+
const payload = await request.json() as OpenAIChatRequest;
|
115 |
+
|
116 |
+
// 强制启用流式传输
|
117 |
+
const requestBody = {
|
118 |
+
...payload,
|
119 |
+
stream: true
|
120 |
+
};
|
121 |
|
122 |
+
console.log(`Forwarding request for model '${payload.model}' to ${TARGET_URL}`);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
123 |
|
124 |
+
const stream = new ReadableStream({
|
125 |
+
async start(controller) {
|
126 |
try {
|
127 |
+
for await (const chunk of streamProxy(requestBody)) {
|
128 |
+
controller.enqueue(chunk);
|
129 |
+
}
|
130 |
+
controller.close();
|
131 |
+
} catch (error) {
|
132 |
+
controller.error(error);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
133 |
}
|
134 |
+
}
|
135 |
+
});
|
136 |
+
|
137 |
+
return new Response(stream, {
|
138 |
+
headers: {
|
139 |
+
'Content-Type': 'text/event-stream',
|
140 |
+
'Cache-Control': 'no-cache',
|
141 |
+
'Connection': 'keep-alive',
|
142 |
+
'Access-Control-Allow-Origin': '*',
|
143 |
+
'Access-Control-Allow-Methods': 'POST, GET, OPTIONS',
|
144 |
+
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
|
145 |
+
},
|
146 |
+
});
|
147 |
+
|
148 |
+
} catch (error) {
|
149 |
+
console.error('Error parsing request:', error);
|
150 |
+
return new Response(JSON.stringify({
|
151 |
+
error: {
|
152 |
+
message: 'Invalid request format',
|
153 |
+
type: 'request_error'
|
154 |
+
}
|
155 |
+
}), {
|
156 |
+
status: 400,
|
157 |
+
headers: { 'Content-Type': 'application/json' }
|
158 |
+
});
|
159 |
+
}
|
160 |
+
}
|
161 |
|
162 |
+
// --- 主处理函数 ---
|
163 |
+
async function handler(request: Request): Promise<Response> {
|
164 |
+
const url = new URL(request.url);
|
165 |
+
const { pathname, method } = { pathname: url.pathname, method: request.method };
|
166 |
+
|
167 |
+
// 处理 CORS 预检请求
|
168 |
+
if (method === 'OPTIONS') {
|
169 |
+
return new Response(null, {
|
170 |
+
headers: {
|
171 |
+
'Access-Control-Allow-Origin': '*',
|
172 |
+
'Access-Control-Allow-Methods': 'POST, GET, OPTIONS',
|
173 |
+
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
|
174 |
+
},
|
175 |
+
});
|
176 |
+
}
|
177 |
+
|
178 |
+
// 路由分发
|
179 |
+
if (pathname === '/v1/chat/completions' && method === 'POST') {
|
180 |
+
return handleChatCompletions(request);
|
181 |
+
}
|
182 |
+
|
183 |
+
if (pathname === '/' && method === 'GET') {
|
184 |
+
return new Response(JSON.stringify({
|
185 |
+
message: "Pollinations OpenAI-Compatible Proxy is running. Use the /v1/chat/completions endpoint.",
|
186 |
+
version: "1.0.2",
|
187 |
+
target_url: TARGET_URL
|
188 |
+
}), {
|
189 |
+
headers: { 'Content-Type': 'application/json' }
|
190 |
+
});
|
191 |
+
}
|
192 |
+
|
193 |
+
// 404 处理
|
194 |
+
return new Response(JSON.stringify({
|
195 |
+
error: {
|
196 |
+
message: 'Not Found',
|
197 |
+
type: 'not_found'
|
198 |
}
|
199 |
+
}), {
|
200 |
+
status: 404,
|
201 |
+
headers: { 'Content-Type': 'application/json' }
|
202 |
+
});
|
203 |
}
|
204 |
|
205 |
+
// --- 启动服务器 ---
|
206 |
+
const port = parseInt(Deno.env.get("PORT") || "7860");
|
207 |
+
|
208 |
+
console.log("Starting Deno server...");
|
209 |
+
console.log(`Forwarding requests to: ${TARGET_URL}`);
|
210 |
+
console.log(`OpenAI compatible endpoint available at: http://127.0.0.1:${port}/v1/chat/completions`);
|
211 |
|
212 |
+
await serve(handler, { port });
|