mirror of
https://github.com/Nezumi-2711/9router.git
synced 2026-09-22 13:38:31 +00:00
- Refines the overall structure of the CLI tools and MITM server functionalities.
- Add buildQwenBaseUrl function to construct URLs for Qwen resources. - Update buildProviderUrl to support Qwen model requests. - Enhance token refresh logic to include provider-specific data for Qwen. - Refactor CLI Tools page to exclude MITM tools and streamline model retrieval. - Introduce new components for MITM server management. - Update API routes to handle Qwen-specific resource URLs and improve error handling.
This commit is contained in:
+37
-44
@@ -3,19 +3,22 @@ const fs = require("fs");
|
||||
const path = require("path");
|
||||
const dns = require("dns");
|
||||
const { promisify } = require("util");
|
||||
// Configuration
|
||||
|
||||
const INTERNAL_REQUEST_HEADER = { name: "x-request-source", value: "local" };
|
||||
|
||||
// All intercepted domains across all tools
|
||||
const TARGET_HOSTS = [
|
||||
"daily-cloudcode-pa.googleapis.com",
|
||||
"cloudcode-pa.googleapis.com"
|
||||
"cloudcode-pa.googleapis.com",
|
||||
"api.individual.githubcopilot.com",
|
||||
];
|
||||
|
||||
const LOCAL_PORT = 443;
|
||||
const ROUTER_URL = "http://localhost:20128/v1/chat/completions";
|
||||
const API_KEY = process.env.ROUTER_API_KEY;
|
||||
const { DATA_DIR, MITM_DIR } = require("./paths");
|
||||
const DB_FILE = path.join(DATA_DIR, "db.json");
|
||||
|
||||
// Toggle logging (set true to enable file logging for debugging)
|
||||
const ENABLE_FILE_LOG = false;
|
||||
|
||||
if (!API_KEY) {
|
||||
@@ -23,7 +26,6 @@ if (!API_KEY) {
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Load SSL certificates
|
||||
const certDir = MITM_DIR;
|
||||
let sslOptions;
|
||||
try {
|
||||
@@ -36,10 +38,11 @@ try {
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Chat endpoints that should be intercepted
|
||||
const CHAT_URL_PATTERNS = [":generateContent", ":streamGenerateContent"];
|
||||
// Antigravity: Gemini generateContent endpoints
|
||||
const ANTIGRAVITY_URL_PATTERNS = [":generateContent", ":streamGenerateContent"];
|
||||
// Copilot: OpenAI-compatible + Anthropic endpoints
|
||||
const COPILOT_URL_PATTERNS = ["/chat/completions", "/v1/messages", "/responses"];
|
||||
|
||||
// Log directory for request/response dumps
|
||||
const LOG_DIR = path.join(__dirname, "../../logs/mitm");
|
||||
if (ENABLE_FILE_LOG && !fs.existsSync(LOG_DIR)) fs.mkdirSync(LOG_DIR, { recursive: true });
|
||||
|
||||
@@ -51,26 +54,9 @@ function saveRequestLog(url, bodyBuffer) {
|
||||
const filePath = path.join(LOG_DIR, `${ts}_${urlSlug}.json`);
|
||||
const body = JSON.parse(bodyBuffer.toString());
|
||||
fs.writeFileSync(filePath, JSON.stringify(body, null, 2));
|
||||
console.log(`💾 Saved request: ${filePath}`);
|
||||
} catch {
|
||||
// Ignore
|
||||
}
|
||||
} catch { /* ignore */ }
|
||||
}
|
||||
|
||||
function saveResponseLog(url, data) {
|
||||
if (!ENABLE_FILE_LOG) return;
|
||||
try {
|
||||
const ts = new Date().toISOString().replace(/[:.]/g, "-");
|
||||
const urlSlug = url.replace(/[^a-zA-Z0-9]/g, "_").substring(0, 60);
|
||||
const filePath = path.join(LOG_DIR, `${ts}_${urlSlug}_response.txt`);
|
||||
fs.writeFileSync(filePath, data);
|
||||
console.log(`💾 Saved response: ${filePath}`);
|
||||
} catch {
|
||||
// Ignore
|
||||
}
|
||||
}
|
||||
|
||||
// Resolve real IP of target host (bypass /etc/hosts)
|
||||
const cachedTargetIPs = {};
|
||||
async function resolveTargetIP(hostname) {
|
||||
if (cachedTargetIPs[hostname]) return cachedTargetIPs[hostname];
|
||||
@@ -91,27 +77,36 @@ function collectBodyRaw(req) {
|
||||
});
|
||||
}
|
||||
|
||||
// Extract model from URL path (Gemini format: /v1beta/models/gemini-2.0-flash:generateContent)
|
||||
// Fallback to body.model (OpenAI format)
|
||||
// Extract model from URL path (Gemini) or body (OpenAI/Anthropic)
|
||||
function extractModel(url, body) {
|
||||
const urlMatch = url.match(/\/models\/([^/:]+)/);
|
||||
if (urlMatch) return urlMatch[1];
|
||||
try { return JSON.parse(body.toString()).model || null; } catch { return null; }
|
||||
}
|
||||
|
||||
function getMappedModel(model) {
|
||||
function getMappedModel(tool, model) {
|
||||
if (!model) return null;
|
||||
try {
|
||||
if (!fs.existsSync(DB_FILE)) return null;
|
||||
const db = JSON.parse(fs.readFileSync(DB_FILE, "utf-8"));
|
||||
return db.mitmAlias?.antigravity?.[model] || null;
|
||||
return db.mitmAlias?.[tool]?.[model] || null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine which tool this request belongs to based on hostname
|
||||
*/
|
||||
function getToolForHost(host) {
|
||||
const h = (host || "").split(":")[0];
|
||||
if (h === "api.individual.githubcopilot.com") return "copilot";
|
||||
if (h === "daily-cloudcode-pa.googleapis.com" || h === "cloudcode-pa.googleapis.com") return "antigravity";
|
||||
return null;
|
||||
}
|
||||
|
||||
async function passthrough(req, res, bodyBuffer) {
|
||||
const targetHost = req.headers.host || TARGET_HOSTS[0];
|
||||
const targetHost = (req.headers.host || TARGET_HOSTS[0]).split(":")[0];
|
||||
const targetIP = await resolveTargetIP(targetHost);
|
||||
|
||||
const forwardReq = https.request({
|
||||
@@ -163,7 +158,6 @@ async function intercept(req, res, bodyBuffer, mappedModel) {
|
||||
|
||||
const reader = response.body.getReader();
|
||||
const decoder = new TextDecoder();
|
||||
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) { res.end(); break; }
|
||||
@@ -177,7 +171,6 @@ async function intercept(req, res, bodyBuffer, mappedModel) {
|
||||
}
|
||||
|
||||
const server = https.createServer(sslOptions, async (req, res) => {
|
||||
// Health check endpoint for startup verification
|
||||
if (req.url === "/_mitm_health") {
|
||||
res.writeHead(200, { "Content-Type": "application/json" });
|
||||
res.end(JSON.stringify({ ok: true, pid: process.pid }));
|
||||
@@ -185,27 +178,28 @@ const server = https.createServer(sslOptions, async (req, res) => {
|
||||
}
|
||||
|
||||
const bodyBuffer = await collectBodyRaw(req);
|
||||
|
||||
// Save request log if enabled
|
||||
if (bodyBuffer.length > 0) saveRequestLog(req.url, bodyBuffer);
|
||||
|
||||
// Anti-loop: requests from 9Router bypass interception
|
||||
// Anti-loop: requests originating from 9Router bypass interception
|
||||
if (req.headers[INTERNAL_REQUEST_HEADER.name] === INTERNAL_REQUEST_HEADER.value) {
|
||||
return passthrough(req, res, bodyBuffer);
|
||||
}
|
||||
|
||||
const isChatRequest = CHAT_URL_PATTERNS.some(p => req.url.includes(p));
|
||||
const tool = getToolForHost(req.headers.host);
|
||||
if (!tool) return passthrough(req, res, bodyBuffer);
|
||||
|
||||
if (!isChatRequest) {
|
||||
return passthrough(req, res, bodyBuffer);
|
||||
}
|
||||
// Check if this URL should be intercepted based on tool
|
||||
const isChat = tool === "antigravity"
|
||||
? ANTIGRAVITY_URL_PATTERNS.some(p => req.url.includes(p))
|
||||
: COPILOT_URL_PATTERNS.some(p => req.url.includes(p));
|
||||
|
||||
if (!isChat) return passthrough(req, res, bodyBuffer);
|
||||
|
||||
const model = extractModel(req.url, bodyBuffer);
|
||||
const mappedModel = getMappedModel(model);
|
||||
console.log("Extracted model:", model)
|
||||
const mappedModel = getMappedModel(tool, model);
|
||||
|
||||
if (!mappedModel) {
|
||||
return passthrough(req, res, bodyBuffer);
|
||||
}
|
||||
if (!mappedModel) return passthrough(req, res, bodyBuffer);
|
||||
|
||||
return intercept(req, res, bodyBuffer, mappedModel);
|
||||
});
|
||||
@@ -225,7 +219,6 @@ server.on("error", (error) => {
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
// Graceful shutdown (SIGBREAK for Windows, SIGTERM/SIGINT for Unix)
|
||||
const shutdown = () => { server.close(() => process.exit(0)); };
|
||||
process.on("SIGTERM", shutdown);
|
||||
process.on("SIGINT", shutdown);
|
||||
|
||||
Reference in New Issue
Block a user