From 2fe9feecb2c2d34fababfae28b1d517e12c0a921 Mon Sep 17 00:00:00 2001 From: bbbugg Date: Sat, 17 Jan 2026 22:37:32 +0800 Subject: [PATCH] feat: implement API Key capture from network traffic and enhance file upload handling --- scripts/client/build.js | 42 +++++++++++-- src/core/BrowserManager.js | 34 ++++++++++ src/core/ProxyServerSystem.js | 16 ++++- src/core/RequestHandler.js | 115 ++++++++++++++++++++++++++++++++++ 4 files changed, 202 insertions(+), 5 deletions(-) diff --git a/scripts/client/build.js b/scripts/client/build.js index 05a9ee85..fdd61e42 100644 --- a/scripts/client/build.js +++ b/scripts/client/build.js @@ -158,6 +158,11 @@ class RequestProcessor { } _constructUrl(requestSpec) { + if (requestSpec.absoluteUrl) { + Logger.output(`Using absolute URL for proxy: ${requestSpec.absoluteUrl}`); + return requestSpec.absoluteUrl; + } + let pathSegment = requestSpec.path.startsWith("/") ? requestSpec.path.substring(1) : requestSpec.path; const queryParams = new URLSearchParams(requestSpec.query_params); if (requestSpec.streaming_mode === "fake") { @@ -184,20 +189,43 @@ class RequestProcessor { _buildRequestConfig(requestSpec, signal) { const config = { + credentials: "include", headers: this._sanitizeHeaders(requestSpec.headers), - method: requestSpec.method, + method: requestSpec.method, // Critical for cross-origin cookie transmission signal, }; if (["POST", "PUT", "PATCH"].includes(requestSpec.method) && requestSpec.body) { try { + // Handle Binary/Base64 Body + if (requestSpec.isBase64) { + // Convert Base64 string to Uint8Array for fetch + const binaryString = atob(requestSpec.body); + const bytes = new Uint8Array(binaryString.length); + for (let i = 0; i < binaryString.length; i++) { + bytes[i] = binaryString.charCodeAt(i); + } + config.body = bytes; + Logger.output("Converted Base64 body to binary Uint8Array."); + return config; // Skip all JSON logic + } + + // If it looks like an upload path but not marked as base64 (maybe text file), pass it through + const isUpload = requestSpec.path && requestSpec.path.includes("/upload/"); + if (isUpload) { + config.body = requestSpec.body; + Logger.output("Upload path detected, passing body as-is (skipping JSON parsing)."); + return config; + } + const bodyObj = JSON.parse(requestSpec.body); // --- Module 1: Image/Embedding/TTS Model Filtering --- // These models do NOT support: tools, thinkingConfig, systemInstruction, response_mime_type - const isImageModel = requestSpec.path.includes("-image") || requestSpec.path.includes("imagen"); - const isEmbeddingModel = requestSpec.path.includes("embedding"); - const isTtsModel = requestSpec.path.includes("tts"); + const isImageModel = + requestSpec.path && (requestSpec.path.includes("-image") || requestSpec.path.includes("imagen")); + const isEmbeddingModel = requestSpec.path && requestSpec.path.includes("embedding"); + const isTtsModel = requestSpec.path && requestSpec.path.includes("tts"); if (isImageModel || isEmbeddingModel || isTtsModel) { // Remove tools const incompatibleKeys = ["toolConfig", "tool_config", "toolChoice", "tools"]; @@ -300,6 +328,8 @@ class RequestProcessor { } _sanitizeHeaders(headers) { + // Debug logging + Logger.output("Sanitizing headers:", JSON.stringify(Object.keys(headers))); const sanitized = { ...headers }; [ "host", @@ -312,6 +342,10 @@ class RequestProcessor { "sec-fetch-site", "sec-fetch-dest", ].forEach(h => delete sanitized[h]); + + // Whitelist x-goog-upload headers + // These are often case-sensitive or critical for the resumable upload flow + // The default behavior preserves them if they are in the input, but we ensure we don't accidentally delete them. return sanitized; } diff --git a/src/core/BrowserManager.js b/src/core/BrowserManager.js index 803a288b..9c2f1bfd 100644 --- a/src/core/BrowserManager.js +++ b/src/core/BrowserManager.js @@ -27,6 +27,7 @@ class BrowserManager { // -1 means no account is currently active (invalid/error state) this._currentAuthIndex = -1; this.scriptFileName = "build.js"; + this.capturedApiKey = null; // Store captured API Key // Added for background wakeup logic from new core this.noButtonCount = 0; @@ -129,6 +130,12 @@ class BrowserManager { authData.cookies = storageState.cookies; authData.origins = storageState.origins; + // Feature: Auto-save captured API Key - REMOVED per user request (In-Memory only) + // if (this.capturedApiKey) { + // authData.apiKey = this.capturedApiKey; + // this.logger.info(`[Auth Update] 🔑 Persisting captured API Key for account #${authIndex}`); + // } + // Note: We do NOT force-set accountName. If it was there, it stays; if not, it remains missing. // This preserves the "missing state" as requested. @@ -718,7 +725,11 @@ class BrowserManager { const randomHeight = 1080 + Math.floor(Math.random() * 50); this.context = await this.browser.newContext({ + bypassCSP: true, deviceScaleFactor: 1, + // Enable CSP bypass to potentially see more headers + ignoreHTTPSErrors: true, + storageState: storageStateObject, viewport: { height: randomHeight, width: randomWidth }, }); @@ -753,6 +764,29 @@ class BrowserManager { }); this.logger.info(`[Browser] Navigating to target page...`); + + // Feature: Capture API Key from network traffic + this.page.on("request", request => { + try { + const url = request.url(); + if (url.includes("generativelanguage.googleapis.com") || url.includes("alkalimakersuite")) { + const headers = request.headers(); + // Look for standard API Key headers + const key = headers["x-goog-api-key"] || headers["x-api-key"]; + // Filter out default/placeholder keys if any + if (key && key !== "123456" && key !== this.capturedApiKey) { + this.capturedApiKey = key; + this.logger.info( + `[Browser] 🔑 Captured new API Key from traffic: ${key.substring(0, 8)}...` + ); + // Optional: Trigger immediate save? Maybe wait for periodic update to avoid IO spam + } + } + } catch (e) { + // Ignore + } + }); + const targetUrl = "https://aistudio.google.com/u/0/apps/bundled/blank?showPreview=true&showCode=true&showAssistant=true"; await this.page.goto(targetUrl, { diff --git a/src/core/ProxyServerSystem.js b/src/core/ProxyServerSystem.js index 1316e4c8..fe1c6818 100644 --- a/src/core/ProxyServerSystem.js +++ b/src/core/ProxyServerSystem.js @@ -289,7 +289,16 @@ class ProxyServerSystem extends EventEmitter { "Content-Type, Authorization, x-requested-with, x-api-key, x-goog-api-key, x-goog-api-client, x-user-agent," + " origin, accept, baggage, sentry-trace, openai-organization, openai-project, openai-beta, x-stainless-lang, " + "x-stainless-package-version, x-stainless-os, x-stainless-arch, x-stainless-runtime, x-stainless-runtime-version, " + - "x-stainless-retry-count, x-stainless-timeout, sec-ch-ua, sec-ch-ua-mobile, sec-ch-ua-platform" + "x-stainless-retry-count, x-stainless-timeout, sec-ch-ua, sec-ch-ua-mobile, sec-ch-ua-platform, " + + "x-goog-upload-command, x-goog-upload-protocol, x-goog-upload-header-content-length, x-goog-upload-header-content-type, " + + "x-goog-upload-offset, x-goog-upload-file-name" + ); + // 暴露响应头给前端(Files API 断点续传需要) + res.header( + "Access-Control-Expose-Headers", + "Content-Length, X-Goog-Upload-URL, X-Goog-Upload-Status, X-Goog-Upload-Chunk-Granularity, " + + "X-Goog-Upload-Control-URL, X-GUploader-UploadID, X-Goog-Upload-Header-Content-Type, " + + "X-Goog-Upload-Header-Access-Control-Allow-Origin, X-Goog-Upload-Header-Access-Control-Expose-Headers" ); if (req.method === "OPTIONS") { return res.sendStatus(204); @@ -366,6 +375,11 @@ class ProxyServerSystem extends EventEmitter { ); }); + // Files API 专用路由(上传) + app.all(["/upload/*", "/proxy_absolute"], (req, res) => { + this.requestHandler.processFilesApiRequest(req, res); + }); + app.all(/(.*)/, (req, res) => { this.requestHandler.processRequest(req, res); }); diff --git a/src/core/RequestHandler.js b/src/core/RequestHandler.js index f9239ea5..4cbacfad 100644 --- a/src/core/RequestHandler.js +++ b/src/core/RequestHandler.js @@ -343,6 +343,121 @@ class RequestHandler { } } + /** + * 专门处理 Files API 请求(上传) + * 不调用 _buildProxyRequest,极简透传 + */ + async processFilesApiRequest(req, res) { + const requestId = this._generateRequestId(); + + // 连接检查 + if (!this.connectionRegistry.hasActiveConnections()) { + const recovered = await this._handleBrowserRecovery(res); + if (!recovered) return; + } + if (this.authSwitcher.isSystemBusy) { + const ready = await this._waitForSystemReady(); + if (!ready) { + return this._sendErrorResponse(res, 503, "System busy, please try again later."); + } + } + if (this.browserManager) this.browserManager.notifyUserActivity(); + + // 极简构建请求对象 + const cleanPath = req.path.replace(/^\/proxy/, ""); + const proxyRequest = { + headers: { ...req.headers }, + is_generative: false, + method: req.method, + path: cleanPath, + query_params: req.query || {}, + request_id: requestId, + streaming_mode: "fake", + }; + + // 处理绝对URL代理(断点续传) + if (req.query.url) { + proxyRequest.absoluteUrl = req.query.url; + } + + // 处理 Body + if (Buffer.isBuffer(req.body)) { + proxyRequest.body = req.body.toString("base64"); + proxyRequest.isBase64 = true; + } else if (req.body) { + proxyRequest.body = typeof req.body === "string" ? req.body : JSON.stringify(req.body); + } + + // 注入内存中的 API Key + if (this.browserManager.capturedApiKey) { + proxyRequest.headers["x-goog-api-key"] = this.browserManager.capturedApiKey; + this.logger.info( + `[FilesAPI] 🔑 Injecting API Key: ${this.browserManager.capturedApiKey.substring(0, 8)}...` + ); + } else { + this.logger.warn(`[FilesAPI] ⚠️ No API Key in memory, upload may fail.`); + } + + // 发送请求并等待响应 + const messageQueue = this.connectionRegistry.createMessageQueue(requestId); + try { + this._forwardRequest(proxyRequest); + + // 等待响应头 + const headerMessage = await messageQueue.dequeue(this.config.timeout || 120000); + if (headerMessage.event_type === "error") { + return this._sendErrorResponse(res, headerMessage.status || 500, headerMessage.message); + } + + // 收集响应体 + let fullBody = ""; + let receiving = true; + while (receiving) { + const message = await messageQueue.dequeue(300000); + if (message.type === "STREAM_END") { + receiving = false; + break; + } + if (message.event_type === "chunk" && message.data) { + fullBody += message.data; + } + } + + // 设置响应头 + res.status(headerMessage.status || 200); + const headers = headerMessage.headers || {}; + for (const [key, value] of Object.entries(headers)) { + const lowerKey = key.toLowerCase(); + if (lowerKey === "content-length" || lowerKey === "content-encoding") continue; + + // URL 重写(断点续传) + if (lowerKey === "x-goog-upload-url") { + let myHost = this.serverSystem.config.host || "127.0.0.1"; + if (myHost === "0.0.0.0") myHost = "127.0.0.1"; + const myPort = this.serverSystem.config.httpPort; + const newUrl = `http://${myHost}:${myPort}/proxy_absolute?url=${encodeURIComponent(value)}`; + this.logger.info(`[FilesAPI] Rewriting upload URL: ${newUrl}`); + res.set(key, newUrl); + continue; + } + + res.set(key, value); + } + + // 发送响应体(如果有的话) + if (fullBody) { + res.send(fullBody); + } else { + res.end(); // 空响应体,只返回头 + } + this.logger.info(`[FilesAPI] ✅ Response sent for request #${requestId}`); + } catch (error) { + this._handleRequestError(error, res); + } finally { + this.connectionRegistry.removeMessageQueue(requestId); + } + } + // Process OpenAI format requests async processOpenAIRequest(req, res) { const requestId = this._generateRequestId();