@8-/gemini-web-api 1.0.0 → 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/router.js ADDED
@@ -0,0 +1,302 @@
1
+ import {
2
+ API_KEY,
3
+ CORS_HEADERS,
4
+ DEFAULT_MODEL_LI,
5
+ ENABLE_THINKING,
6
+ ERR_MISSING_URL,
7
+ ERR_NOT_FOUND,
8
+ ERR_UNAUTHORIZED,
9
+ GEN_URL,
10
+ ROLE_ASSISTANT,
11
+ STATUS_BAD_REQUEST,
12
+ STATUS_NO_CONTENT,
13
+ STATUS_NOT_FOUND,
14
+ STATUS_OK,
15
+ STATUS_UNAUTHORIZED,
16
+ STOP_REASON_STOP,
17
+ STOP_REASON_TOOL_CALLS,
18
+ USER_AGENT,
19
+ } from "./constant.js";
20
+ import { modelMap } from "./modelDiscover.js";
21
+ import { conversationFormat, payloadBuild } from "./payloadBuild.js";
22
+ import { sessionInit, session_state } from "./sessionState.js";
23
+ import { sseStreamCreate } from "./sseResponse.js";
24
+ import { fullResponseCollect } from "./streamExtract.js";
25
+ import { jsonFormat, toolCallExtract } from "./toolHandle.js";
26
+
27
+ export const authVerify = (req) => {
28
+ if (!API_KEY) return true;
29
+ const auth_header = req.headers.get("authorization") ?? "",
30
+ key = auth_header.replace(/^Bearer\s+/i, "");
31
+ return key === API_KEY;
32
+ },
33
+
34
+ healthHandle = () => {
35
+ const res_obj = {
36
+ status: "healthy",
37
+ service: "Gemini API 代理 (Node/Bun)",
38
+ version: "1.0.0",
39
+ endpoints: ["/v1/models", "/v1/chat/completions"],
40
+ },
41
+ res_body = JSON.stringify(res_obj);
42
+ console.log("<-- 响应: [200 OK]\n" + jsonFormat(res_obj));
43
+ return new Response(res_body, {
44
+ status: STATUS_OK,
45
+ headers: { ...CORS_HEADERS, "Content-Type": "application/json" },
46
+ });
47
+ },
48
+
49
+ modelListHandle = (now) => {
50
+ const available_model_li =
51
+ session_state.model_li.length > 0 ? session_state.model_li : DEFAULT_MODEL_LI,
52
+ model_data_li = available_model_li.map((model_item) => ({
53
+ id: model_item.id,
54
+ object: "model",
55
+ created: now,
56
+ owned_by: "google-gemini-web",
57
+ })),
58
+ res_obj = { object: "list", data: model_data_li },
59
+ res_body = JSON.stringify(res_obj);
60
+ console.log("<-- 响应: [200 OK] 模型列表:\n" + jsonFormat(res_obj));
61
+ return new Response(res_body, {
62
+ status: STATUS_OK,
63
+ headers: { ...CORS_HEADERS, "Content-Type": "application/json" },
64
+ });
65
+ },
66
+
67
+ modelDetailHandle = (req_model_id, now) => {
68
+ const target_model = modelMap(req_model_id),
69
+ res_obj = {
70
+ id: target_model.id,
71
+ object: "model",
72
+ created: now,
73
+ owned_by: "google-gemini-web",
74
+ },
75
+ res_body = JSON.stringify(res_obj);
76
+ console.log("<-- 响应: [200 OK] 模型详情:\n" + jsonFormat(res_obj));
77
+ return new Response(res_body, {
78
+ status: STATUS_OK,
79
+ headers: { ...CORS_HEADERS, "Content-Type": "application/json" },
80
+ });
81
+ },
82
+
83
+ imageProxyHandle = async (req_url) => {
84
+ const target_url = req_url.searchParams.get("url");
85
+ if (!target_url) {
86
+ return new Response(
87
+ JSON.stringify({ code: ERR_MISSING_URL, error: "缺少 url 参数" }),
88
+ {
89
+ status: STATUS_BAD_REQUEST,
90
+ headers: { ...CORS_HEADERS, "Content-Type": "application/json" },
91
+ },
92
+ );
93
+ }
94
+ const img_res = await fetch(target_url, {
95
+ headers: {
96
+ "User-Agent": USER_AGENT,
97
+ Referer: "https://gemini.google.com/",
98
+ },
99
+ }),
100
+ img_bytes = new Uint8Array(await img_res.arrayBuffer()),
101
+ headers = {
102
+ ...CORS_HEADERS,
103
+ "Content-Type": img_res.headers.get("content-type") ?? "image/png",
104
+ };
105
+ return new Response(img_bytes, { status: STATUS_OK, headers });
106
+ },
107
+
108
+ chatCompletionsHandle = async (req, body) => {
109
+ if (!authVerify(req)) {
110
+ const res_obj = { code: ERR_UNAUTHORIZED, error: "未授权" },
111
+ res_body = JSON.stringify(res_obj);
112
+ console.log("<-- 响应: [401 Unauthorized]\n" + jsonFormat(res_obj));
113
+ return new Response(res_body, {
114
+ status: STATUS_UNAUTHORIZED,
115
+ headers: { ...CORS_HEADERS, "Content-Type": "application/json" },
116
+ });
117
+ }
118
+
119
+ if (!body.messages && body.prompt) {
120
+ body.messages = [{ role: "user", content: body.prompt }];
121
+ }
122
+
123
+ const model_info = modelMap(body.model),
124
+ has_tools = Array.isArray(body.tools) && body.tools.length > 0 && body.tool_choice !== "none",
125
+ conversation = conversationFormat(body.messages, body.tools, body.tool_choice);
126
+
127
+ if (!session_state.at) {
128
+ await sessionInit();
129
+ }
130
+
131
+ ++session_state.req_id;
132
+ const [inner_req, model_header, uuid_val] = payloadBuild(conversation, model_info),
133
+ form_data = new URLSearchParams();
134
+ form_data.set("at", session_state.at);
135
+ form_data.set("f.req", JSON.stringify([null, JSON.stringify(inner_req)]));
136
+
137
+ const gen_url = new URL(GEN_URL);
138
+ gen_url.searchParams.set("hl", "en");
139
+ gen_url.searchParams.set("_reqid", String(session_state.req_id));
140
+ gen_url.searchParams.set("rt", "c");
141
+ if (session_state.bl) gen_url.searchParams.set("bl", session_state.bl);
142
+ if (session_state.fsid) gen_url.searchParams.set("f.sid", session_state.fsid);
143
+
144
+ const gen_res = await fetch(gen_url.toString(), {
145
+ method: "POST",
146
+ headers: {
147
+ "Content-Type": "application/x-www-form-urlencoded;charset=utf-8",
148
+ Origin: "https://gemini.google.com",
149
+ Referer: "https://gemini.google.com/",
150
+ "X-Same-Domain": "1",
151
+ "User-Agent": USER_AGENT,
152
+ Cookie: session_state.cookie_header,
153
+ "x-goog-ext-525001261-jspb": JSON.stringify(model_header),
154
+ "x-goog-ext-73010989-jspb": "[0]",
155
+ "x-goog-ext-73010990-jspb": "[0,0,0]",
156
+ "x-goog-ext-525005358-jspb": JSON.stringify([uuid_val, 1]),
157
+ },
158
+ body: form_data.toString(),
159
+ }),
160
+ completion_id = "chatcmpl-" + crypto.randomUUID(),
161
+ created_time = Math.floor(Date.now() / 1000);
162
+
163
+ if (body.stream) {
164
+ console.log("<-- 响应: [200 OK] 开始流式传输 (SSE)...");
165
+ const sse_stream = sseStreamCreate(
166
+ gen_res.body,
167
+ completion_id,
168
+ model_info.id,
169
+ created_time,
170
+ has_tools,
171
+ );
172
+ return new Response(sse_stream, {
173
+ status: STATUS_OK,
174
+ headers: {
175
+ ...CORS_HEADERS,
176
+ "Content-Type": "text/event-stream",
177
+ "Cache-Control": "no-cache",
178
+ Connection: "keep-alive",
179
+ },
180
+ });
181
+ }
182
+
183
+ const [full_content, final_text, final_thought] = await fullResponseCollect(gen_res.body),
184
+ prompt_tokens = conversation.split(/\s+/).length;
185
+
186
+ let message = {
187
+ role: ROLE_ASSISTANT,
188
+ content: full_content,
189
+ },
190
+ finish_reason = STOP_REASON_STOP;
191
+
192
+ if (ENABLE_THINKING && final_thought) {
193
+ message.reasoning_content = final_thought;
194
+ }
195
+
196
+ if (has_tools) {
197
+ const [, tool_call_li] = toolCallExtract(final_text);
198
+ if (tool_call_li.length > 0) {
199
+ message.content = null;
200
+ message.tool_calls = tool_call_li;
201
+ finish_reason = STOP_REASON_TOOL_CALLS;
202
+ }
203
+ }
204
+
205
+ const completion_tokens = (message.content ?? "").split(/\s+/).length,
206
+ res_json = {
207
+ id: completion_id,
208
+ object: "chat.completion",
209
+ created: created_time,
210
+ model: model_info.id,
211
+ choices: [
212
+ {
213
+ index: 0,
214
+ message,
215
+ finish_reason,
216
+ },
217
+ ],
218
+ usage: {
219
+ prompt_tokens,
220
+ completion_tokens,
221
+ total_tokens: prompt_tokens + completion_tokens,
222
+ },
223
+ },
224
+ res_body = JSON.stringify(res_json);
225
+ console.log("<-- 响应: [200 OK]\n" + jsonFormat(res_json));
226
+ return new Response(res_body, {
227
+ status: STATUS_OK,
228
+ headers: { ...CORS_HEADERS, "Content-Type": "application/json" },
229
+ });
230
+ },
231
+
232
+ reqHandle = async (req) => {
233
+ const req_url = new URL(req.url),
234
+ pathname = req_url.pathname,
235
+ norm_path = pathname.replace(/\/+$/, "") || "/",
236
+ is_body_method = req.method === "POST" || req.method === "PUT" || req.method === "PATCH";
237
+ let body = {},
238
+ req_text = "";
239
+
240
+ if (is_body_method) {
241
+ req_text = await req.text();
242
+ const trimmed = req_text.trim();
243
+ if (trimmed && (trimmed.startsWith("{") || trimmed.startsWith("["))) {
244
+ body = JSON.parse(trimmed);
245
+ }
246
+ }
247
+
248
+ console.log("\n--> [" + req.method + "] " + pathname);
249
+ console.log("--> 请求头:", JSON.stringify(Object.fromEntries(req.headers.entries())));
250
+ if (req_text.trim()) {
251
+ console.log("--> 请求体:\n" + jsonFormat(body || req_text));
252
+ } else {
253
+ console.log("--> 请求体: (无)");
254
+ }
255
+
256
+ if (req.method === "OPTIONS") {
257
+ console.log("<-- 响应: [204 No Content]");
258
+ return new Response(null, { status: STATUS_NO_CONTENT, headers: CORS_HEADERS });
259
+ }
260
+
261
+ if (norm_path === "/" || norm_path === "/v1") {
262
+ return healthHandle();
263
+ }
264
+
265
+ const now = Math.floor(Date.now() / 1000);
266
+ if ((norm_path === "/models" || norm_path.endsWith("/models")) && req.method === "GET") {
267
+ return modelListHandle(now);
268
+ }
269
+
270
+ if (norm_path.includes("/models/") && req.method === "GET") {
271
+ const req_model_id = norm_path.split("/").pop();
272
+ return modelDetailHandle(req_model_id, now);
273
+ }
274
+
275
+ if (norm_path.endsWith("/gemini-proxy/image") && req.method === "GET") {
276
+ console.log("<-- 响应: [200 OK] 代理图片");
277
+ return imageProxyHandle(req_url);
278
+ }
279
+
280
+ if (
281
+ (norm_path === "/chat/completions" ||
282
+ norm_path.endsWith("/chat/completions") ||
283
+ norm_path === "/completions" ||
284
+ norm_path.endsWith("/completions")) &&
285
+ req.method === "POST"
286
+ ) {
287
+ return chatCompletionsHandle(req, body);
288
+ }
289
+
290
+ const res_obj = {
291
+ code: ERR_NOT_FOUND,
292
+ error: "未找到接口",
293
+ path: pathname,
294
+ method: req.method,
295
+ },
296
+ res_body = JSON.stringify(res_obj);
297
+ console.log("<-- 响应: [404 Not Found] 未找到接口: " + req.method + " " + pathname + "\n" + jsonFormat(res_obj));
298
+ return new Response(res_body, {
299
+ status: STATUS_NOT_FOUND,
300
+ headers: { ...CORS_HEADERS, "Content-Type": "application/json" },
301
+ });
302
+ };
@@ -0,0 +1,96 @@
1
+ import { createServer } from "node:http";
2
+ import { Readable } from "node:stream";
3
+ import { HOST, PORT, STATUS_SERVER_ERR } from "./constant.js";
4
+ import { modelListPrint } from "./modelDiscover.js";
5
+ import { reqHandle } from "./router.js";
6
+
7
+ export const serverStart = (custom_handler = null) => {
8
+ const handler = custom_handler ?? reqHandle,
9
+ is_tty = Boolean(process.stdout?.isTTY || process.env.FORCE_COLOR),
10
+ color = {
11
+ reset: is_tty ? "\x1b[0m" : "",
12
+ bold: is_tty ? "\x1b[1m" : "",
13
+ dim: is_tty ? "\x1b[2m" : "",
14
+ cyan: is_tty ? "\x1b[36m" : "",
15
+ green: is_tty ? "\x1b[32m" : "",
16
+ },
17
+ serverBannerPrint = () => {
18
+ const url = "http://" + (HOST === "0.0.0.0" ? "127.0.0.1" : HOST) + ":" + PORT;
19
+ console.log(
20
+ "\n" +
21
+ color.bold +
22
+ color.green +
23
+ "服务已启动" +
24
+ color.reset +
25
+ "\n" +
26
+ color.cyan +
27
+ "本地服务" +
28
+ color.reset +
29
+ " " +
30
+ url +
31
+ "\n" +
32
+ color.cyan +
33
+ "对话接口" +
34
+ color.reset +
35
+ " " +
36
+ url +
37
+ "/v1/chat/completions" +
38
+ "\n" +
39
+ color.cyan +
40
+ "模型接口" +
41
+ color.reset +
42
+ " " +
43
+ url +
44
+ "/v1/models\n",
45
+ );
46
+ modelListPrint();
47
+ };
48
+
49
+ if (typeof Bun !== "undefined" && Bun.serve) {
50
+ const bun_server = Bun.serve({
51
+ port: PORT,
52
+ hostname: HOST,
53
+ fetch: handler,
54
+ });
55
+ serverBannerPrint("Bun");
56
+ return bun_server;
57
+ }
58
+
59
+ const node_server = createServer(async (node_req, node_res) => {
60
+ try {
61
+ const url =
62
+ "http://" + (node_req.headers.host ?? (HOST + ":" + PORT)) + node_req.url,
63
+ has_body = node_req.method !== "GET" && node_req.method !== "HEAD",
64
+ web_req = new Request(url, {
65
+ method: node_req.method,
66
+ headers: node_req.headers,
67
+ body: has_body ? Readable.toWeb(node_req) : null,
68
+ duplex: "half",
69
+ }),
70
+ web_res = await handler(web_req);
71
+
72
+ node_res.statusCode = web_res.status;
73
+ web_res.headers.forEach((header_val, header_key) => {
74
+ node_res.setHeader(header_key, header_val);
75
+ });
76
+ if (web_res.body) {
77
+ Readable.fromWeb(web_res.body).pipe(node_res);
78
+ } else {
79
+ node_res.end();
80
+ }
81
+ } catch (err) {
82
+ if (!node_res.headersSent) {
83
+ node_res.statusCode = STATUS_SERVER_ERR;
84
+ node_res.end(
85
+ JSON.stringify({ code: STATUS_SERVER_ERR, error: "Internal Server Error" }),
86
+ );
87
+ }
88
+ console.error("服务器异常:", err);
89
+ }
90
+ });
91
+
92
+ node_server.listen(PORT, HOST, () => {
93
+ serverBannerPrint("Node");
94
+ });
95
+ return node_server;
96
+ };
@@ -0,0 +1,70 @@
1
+ import { DEFAULT_MODEL_LI, INIT_URL, STATUS_OK, USER_AGENT } from "./constant.js";
2
+ import { cookieRead } from "./cookieRead.js";
3
+ import { modelFetch } from "./modelDiscover.js";
4
+
5
+ export let session_state = {
6
+ cookie_header: "",
7
+ at: "",
8
+ bl: "",
9
+ fsid: "",
10
+ req_id: Math.floor(Math.random() * 90000) + 10000,
11
+ model_li: [],
12
+ default_model: null,
13
+ };
14
+
15
+ export const sessionInit = async () => {
16
+ try {
17
+ if (!session_state.cookie_header) {
18
+ session_state.cookie_header = await cookieRead();
19
+ }
20
+ const res = await fetch(INIT_URL, {
21
+ headers: {
22
+ "User-Agent": USER_AGENT,
23
+ Cookie: session_state.cookie_header,
24
+ },
25
+ redirect: "manual",
26
+ });
27
+
28
+ let html = "";
29
+ if (res.status === STATUS_OK) {
30
+ html = await res.text();
31
+ } else if (res.status >= 300 && res.status < 400) {
32
+ const location = res.headers.get("location") ?? "";
33
+ if (location.includes("/sorry/")) {
34
+ console.warn("Gemini 会话初始化提示: 触发了 Google 验证码重定向 (sorry/index)");
35
+ } else if (location) {
36
+ const redirect_res = await fetch(location, {
37
+ headers: {
38
+ "User-Agent": USER_AGENT,
39
+ Cookie: session_state.cookie_header,
40
+ },
41
+ });
42
+ if (redirect_res.ok) html = await redirect_res.text();
43
+ }
44
+ }
45
+
46
+ const at_match = html.match(/"SNlM0e":\s*"([^"]+)"/),
47
+ bl_match = html.match(/"cfb2h":\s*"([^"]+)"/),
48
+ fsid_match = html.match(/"FdrFJe":\s*"([^"]+)"/);
49
+ session_state.at = at_match ? at_match[1] : "";
50
+ session_state.bl = bl_match ? bl_match[1] : "";
51
+ session_state.fsid = fsid_match ? fsid_match[1] : "";
52
+ if (session_state.at) {
53
+ session_state.model_li = await modelFetch(
54
+ session_state.at,
55
+ session_state.bl,
56
+ session_state.fsid,
57
+ session_state.cookie_header,
58
+ );
59
+ }
60
+ if (!session_state.default_model) {
61
+ session_state.default_model = session_state.model_li[0] ?? DEFAULT_MODEL_LI[0];
62
+ }
63
+ } catch (err) {
64
+ if (!session_state.default_model) {
65
+ session_state.default_model = session_state.model_li[0] ?? DEFAULT_MODEL_LI[0];
66
+ }
67
+ console.warn("会话初始化异常:", err.message);
68
+ }
69
+ return session_state;
70
+ };
@@ -0,0 +1,166 @@
1
+ import {
2
+ ENABLE_THINKING,
3
+ ROLE_ASSISTANT,
4
+ STOP_REASON_STOP,
5
+ STOP_REASON_TOOL_CALLS,
6
+ } from "./constant.js";
7
+ import { streamChunkExtract } from "./streamExtract.js";
8
+ import { jsonFormat, toolCallExtract } from "./toolHandle.js";
9
+
10
+ export const sseStreamCreate = (
11
+ body,
12
+ completion_id,
13
+ model,
14
+ created_time,
15
+ has_tools = false,
16
+ ) => {
17
+ const enc = new TextEncoder();
18
+ return new ReadableStream({
19
+ async start(controller) {
20
+ const chunkSend = (delta, finish_reason = null) => {
21
+ try {
22
+ const payload = JSON.stringify({
23
+ id: completion_id,
24
+ object: "chat.completion.chunk",
25
+ created: created_time,
26
+ model,
27
+ choices: [
28
+ {
29
+ index: 0,
30
+ delta,
31
+ finish_reason,
32
+ },
33
+ ],
34
+ });
35
+ console.log("<-- [SSE 块]:", JSON.stringify(delta));
36
+ controller.enqueue(enc.encode("data: " + payload + "\n\n"));
37
+ } catch {
38
+ // Client disconnected
39
+ }
40
+ };
41
+
42
+ chunkSend({ role: ROLE_ASSISTANT });
43
+
44
+ const reader = body.getReader(),
45
+ decoder = new TextDecoder();
46
+
47
+ let buf = "",
48
+ last_text = "",
49
+ last_thought = "",
50
+ thinking_started = false,
51
+ thinking_ended = false;
52
+
53
+ const itemProcess = ({ thoughts, text }) => {
54
+ if (ENABLE_THINKING && thoughts) {
55
+ if (thoughts.startsWith(last_thought)) {
56
+ const thought_delta = thoughts.slice(last_thought.length);
57
+ if (thought_delta) {
58
+ if (!thinking_started) {
59
+ chunkSend({ content: "<think>\n" });
60
+ thinking_started = true;
61
+ }
62
+ chunkSend({
63
+ content: thought_delta,
64
+ reasoning_content: thought_delta,
65
+ });
66
+ last_thought = thoughts;
67
+ }
68
+ } else {
69
+ if (!thinking_started) {
70
+ chunkSend({ content: "<think>\n" });
71
+ thinking_started = true;
72
+ }
73
+ chunkSend({
74
+ content: thoughts,
75
+ reasoning_content: thoughts,
76
+ });
77
+ last_thought = thoughts;
78
+ }
79
+ }
80
+
81
+ if (text) {
82
+ if (!has_tools) {
83
+ if (ENABLE_THINKING && thinking_started && !thinking_ended) {
84
+ chunkSend({ content: "</think>\n\n" });
85
+ thinking_ended = true;
86
+ }
87
+ if (text.startsWith(last_text)) {
88
+ const text_delta = text.slice(last_text.length);
89
+ if (text_delta) {
90
+ chunkSend({ content: text_delta });
91
+ }
92
+ } else {
93
+ chunkSend({ content: text });
94
+ }
95
+ }
96
+ last_text = text;
97
+ }
98
+ };
99
+
100
+ while (true) {
101
+ const read_res = await reader.read();
102
+ if (read_res.done) {
103
+ if (buf.trim()) {
104
+ const [remaining_li] = streamChunkExtract(buf + "\n");
105
+ remaining_li.forEach(itemProcess);
106
+ }
107
+ break;
108
+ }
109
+ buf += decoder.decode(read_res.value, { stream: true });
110
+
111
+ const [extracted_li, next_buf] = streamChunkExtract(buf);
112
+ buf = next_buf;
113
+ extracted_li.forEach(itemProcess);
114
+ }
115
+
116
+ if (ENABLE_THINKING && thinking_started && !thinking_ended) {
117
+ chunkSend({ content: "</think>\n\n" });
118
+ thinking_ended = true;
119
+ }
120
+
121
+ let tool_call_li = [];
122
+
123
+ if (has_tools) {
124
+ const [clean_text, parsed_calls_li] = toolCallExtract(last_text);
125
+ tool_call_li = parsed_calls_li;
126
+ if (tool_call_li.length > 0) {
127
+ const delta_calls_li = tool_call_li.map((tc, call_idx) => ({
128
+ index: call_idx,
129
+ id: tc.id,
130
+ type: "function",
131
+ function: tc.function,
132
+ }));
133
+ chunkSend({ tool_calls: delta_calls_li });
134
+ chunkSend({}, STOP_REASON_TOOL_CALLS);
135
+ } else {
136
+ const chunk_size = 24,
137
+ emit_text = clean_text || last_text;
138
+ for (let i = 0; i < emit_text.length; i += chunk_size) {
139
+ chunkSend({ content: emit_text.slice(i, i + chunk_size) });
140
+ }
141
+ chunkSend({}, STOP_REASON_STOP);
142
+ }
143
+ } else {
144
+ chunkSend({}, STOP_REASON_STOP);
145
+ }
146
+
147
+ try {
148
+ controller.enqueue(enc.encode("data: [DONE]\n\n"));
149
+ controller.close();
150
+ } catch {}
151
+ if (tool_call_li.length > 0) {
152
+ console.log(
153
+ "<-- [SSE 完成] 工具调用:\n" +
154
+ (last_thought ? "<think>\n" + last_thought + "\n</think>\n\n" : "") +
155
+ jsonFormat(tool_call_li),
156
+ );
157
+ } else {
158
+ console.log(
159
+ "<-- [SSE 完成] 完整输出:\n" +
160
+ (last_thought ? "<think>\n" + last_thought + "\n</think>\n\n" : "") +
161
+ last_text,
162
+ );
163
+ }
164
+ },
165
+ });
166
+ };
@@ -0,0 +1,75 @@
1
+ import { ENABLE_THINKING } from "./constant.js";
2
+
3
+ export const streamChunkExtract = (buf) => {
4
+ const extracted_li = [],
5
+ line_li = buf.split("\n"),
6
+ next_buf = line_li.pop() ?? "";
7
+
8
+ line_li.forEach((raw_line) => {
9
+ const line = raw_line.trim();
10
+ if (!line || !line.includes('"wrb.fr"')) return;
11
+ try {
12
+ const item_li = JSON.parse(line);
13
+ if (Array.isArray(item_li)) {
14
+ item_li.forEach((item) => {
15
+ if (Array.isArray(item) && item[2]) {
16
+ const inner = JSON.parse(item[2]),
17
+ cand_li = inner[4];
18
+ if (Array.isArray(cand_li) && cand_li.length > 0) {
19
+ const cand = cand_li[0],
20
+ thoughts = cand[37]?.[0]?.[0] ?? "",
21
+ raw_text = cand[1]?.[0] ?? "",
22
+ card_text = raw_text.startsWith("http://googleusercontent.com/card_content/")
23
+ ? (cand[22]?.[0] ?? raw_text)
24
+ : raw_text,
25
+ text = card_text.replaceAll(
26
+ /https?:\/\/googleusercontent\.com\/(?:\w+\/)+\d+\n*/g,
27
+ "",
28
+ );
29
+ extracted_li.push({ thoughts, text });
30
+ }
31
+ }
32
+ });
33
+ }
34
+ } catch {
35
+ // ignore non-json or malformed lines
36
+ }
37
+ });
38
+
39
+ return [extracted_li, next_buf];
40
+ },
41
+
42
+ fullResponseCollect = async (body) => {
43
+ const reader = body.getReader(),
44
+ decoder = new TextDecoder();
45
+ let buf = "",
46
+ final_text = "",
47
+ final_thought = "";
48
+
49
+ const textUpdate = ({ thoughts, text }) => {
50
+ if (text) final_text = text;
51
+ if (thoughts) final_thought = thoughts;
52
+ };
53
+
54
+ while (true) {
55
+ const read_res = await reader.read();
56
+ if (read_res.done) {
57
+ if (buf.trim()) {
58
+ const [remaining_li] = streamChunkExtract(buf + "\n");
59
+ remaining_li.forEach(textUpdate);
60
+ }
61
+ break;
62
+ }
63
+ buf += decoder.decode(read_res.value, { stream: true });
64
+
65
+ const [extracted_li, next_buf] = streamChunkExtract(buf);
66
+ buf = next_buf;
67
+ extracted_li.forEach(textUpdate);
68
+ }
69
+
70
+ let content = final_text;
71
+ if (ENABLE_THINKING && final_thought) {
72
+ content = "<think>\n" + final_thought + "\n</think>\n\n" + final_text;
73
+ }
74
+ return [content, final_text, final_thought];
75
+ };