Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 38 additions & 4 deletions scripts/client/build.js
Original file line number Diff line number Diff line change
Expand Up @@ -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") {
Expand All @@ -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"];
Expand Down Expand Up @@ -300,6 +328,8 @@ class RequestProcessor {
}

_sanitizeHeaders(headers) {
// Debug logging
Logger.output("Sanitizing headers:", JSON.stringify(Object.keys(headers)));
const sanitized = { ...headers };
[
"host",
Expand All @@ -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;
}

Expand Down
34 changes: 34 additions & 0 deletions src/core/BrowserManager.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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.

Expand Down Expand Up @@ -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 },
});
Expand Down Expand Up @@ -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, {
Expand Down
16 changes: 15 additions & 1 deletion src/core/ProxyServerSystem.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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);
});
Expand Down
115 changes: 115 additions & 0 deletions src/core/RequestHandler.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down