aiclient-2-api / src /auth /gemini-oauth.js
Jaasomn
Initial deployment
ceb3821
import { OAuth2Client } from 'google-auth-library';
import http from 'http';
import fs from 'fs';
import path from 'path';
import os from 'os';
import { broadcastEvent } from '../services/ui-manager.js';
import { autoLinkProviderConfigs } from '../services/service-manager.js';
import { CONFIG } from '../core/config-manager.js';
import { getGoogleAuthProxyConfig } from '../utils/proxy-utils.js';
/**
* OAuth 提供商配置
*/
const OAUTH_PROVIDERS = {
'gemini-cli-oauth': {
clientId: '681255809395-oo8ft2oprdrnp9e3aqf6av3hmdib135j.apps.googleusercontent.com',
clientSecret: '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',
port: 8085,
credentialsDir: '.gemini',
credentialsFile: 'oauth_creds.json',
scope: ['https://www.googleapis.com/auth/cloud-platform'],
logPrefix: '[Gemini Auth]'
},
'gemini-antigravity': {
clientId: '1071006060591-tmhssin2h21lcre235vtolojh4g403ep.apps.googleusercontent.com',
clientSecret: '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',
port: 8086,
credentialsDir: '.antigravity',
credentialsFile: 'oauth_creds.json',
scope: ['https://www.googleapis.com/auth/cloud-platform'],
logPrefix: '[Antigravity Auth]'
}
};
/**
* 活动的服务器实例管理
*/
const activeServers = new Map();
/**
* 生成 HTML 响应页面
* @param {boolean} isSuccess - 是否成功
* @param {string} message - 显示消息
* @returns {string} HTML 内容
*/
function generateResponsePage(isSuccess, message) {
const title = isSuccess ? '授权成功!' : '授权失败';
return `<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>${title}</title>
</head>
<body>
<div class="container">
<h1>${title}</h1>
<p>${message}</p>
</div>
</body>
</html>`;
}
/**
* 关闭指定端口的活动服务器
* @param {number} port - 端口号
* @returns {Promise<void>}
*/
async function closeActiveServer(provider, port = null) {
// 1. 关闭该提供商之前的所有服务器
const existing = activeServers.get(provider);
if (existing) {
await new Promise((resolve) => {
existing.server.close(() => {
activeServers.delete(provider);
console.log(`[OAuth] 已关闭提供商 ${provider} 在端口 ${existing.port} 上的旧服务器`);
resolve();
});
});
}
// 2. 如果指定了端口,检查是否有其他提供商占用了该端口
if (port) {
for (const [p, info] of activeServers.entries()) {
if (info.port === port) {
await new Promise((resolve) => {
info.server.close(() => {
activeServers.delete(p);
console.log(`[OAuth] 已关闭端口 ${port} 上被占用(提供商: ${p})的旧服务器`);
resolve();
});
});
}
}
}
}
/**
* 创建 OAuth 回调服务器
* @param {Object} config - OAuth 提供商配置
* @param {string} redirectUri - 重定向 URI
* @param {OAuth2Client} authClient - OAuth2 客户端
* @param {string} credPath - 凭据保存路径
* @param {string} provider - 提供商标识
* @returns {Promise<http.Server>} HTTP 服务器实例
*/
async function createOAuthCallbackServer(config, redirectUri, authClient, credPath, provider, options = {}) {
const port = parseInt(options.port) || config.port;
// 先关闭该提供商之前可能运行的所有服务器,或该端口上的旧服务器
await closeActiveServer(provider, port);
return new Promise((resolve, reject) => {
const server = http.createServer(async (req, res) => {
try {
const url = new URL(req.url, redirectUri);
const code = url.searchParams.get('code');
const errorParam = url.searchParams.get('error');
if (code) {
console.log(`${config.logPrefix} 收到来自 Google 的成功回调: ${req.url}`);
try {
const { tokens } = await authClient.getToken(code);
let finalCredPath = credPath;
// 如果指定了保存到 configs 目录
if (options.saveToConfigs) {
const providerDir = options.providerDir;
const targetDir = path.join(process.cwd(), 'configs', providerDir);
await fs.promises.mkdir(targetDir, { recursive: true });
const timestamp = Date.now();
const filename = `${timestamp}_oauth_creds.json`;
finalCredPath = path.join(targetDir, filename);
}
await fs.promises.mkdir(path.dirname(finalCredPath), { recursive: true });
await fs.promises.writeFile(finalCredPath, JSON.stringify(tokens, null, 2));
console.log(`${config.logPrefix} 新令牌已接收并保存到文件: ${finalCredPath}`);
const relativePath = path.relative(process.cwd(), finalCredPath);
// 广播授权成功事件
broadcastEvent('oauth_success', {
provider: provider,
credPath: finalCredPath,
relativePath: relativePath,
timestamp: new Date().toISOString()
});
// 自动关联新生成的凭据到 Pools
await autoLinkProviderConfigs(CONFIG);
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
res.end(generateResponsePage(true, '您可以关闭此页面'));
} catch (tokenError) {
console.error(`${config.logPrefix} 获取令牌失败:`, tokenError);
res.writeHead(500, { 'Content-Type': 'text/html; charset=utf-8' });
res.end(generateResponsePage(false, `获取令牌失败: ${tokenError.message}`));
} finally {
server.close(() => {
activeServers.delete(provider);
});
}
} else if (errorParam) {
const errorMessage = `授权失败。Google 返回错误: ${errorParam}`;
console.error(`${config.logPrefix}`, errorMessage);
res.writeHead(400, { 'Content-Type': 'text/html; charset=utf-8' });
res.end(generateResponsePage(false, errorMessage));
server.close(() => {
activeServers.delete(provider);
});
} else {
console.log(`${config.logPrefix} 忽略无关请求: ${req.url}`);
res.writeHead(204);
res.end();
}
} catch (error) {
console.error(`${config.logPrefix} 处理回调时出错:`, error);
res.writeHead(500, { 'Content-Type': 'text/html; charset=utf-8' });
res.end(generateResponsePage(false, `服务器错误: ${error.message}`));
if (server.listening) {
server.close(() => {
activeServers.delete(provider);
});
}
}
});
server.on('error', (err) => {
if (err.code === 'EADDRINUSE') {
console.error(`${config.logPrefix} 端口 ${port} 已被占用`);
reject(new Error(`端口 ${port} 已被占用`));
} else {
console.error(`${config.logPrefix} 服务器错误:`, err);
reject(err);
}
});
const host = '0.0.0.0';
server.listen(port, host, () => {
console.log(`${config.logPrefix} OAuth 回调服务器已启动于 ${host}:${port}`);
activeServers.set(provider, { server, port });
resolve(server);
});
});
}
/**
* 处理 Google OAuth 授权(通用函数)
* @param {string} providerKey - 提供商键名
* @param {Object} currentConfig - 当前配置对象
* @param {Object} options - 额外选项
* @returns {Promise<Object>} 返回授权URL和相关信息
*/
async function handleGoogleOAuth(providerKey, currentConfig, options = {}) {
const config = OAUTH_PROVIDERS[providerKey];
if (!config) {
throw new Error(`未知的提供商: ${providerKey}`);
}
const port = parseInt(options.port) || config.port;
const host = 'localhost';
const redirectUri = `http://${host}:${port}`;
// 获取代理配置
const proxyConfig = getGoogleAuthProxyConfig(currentConfig, providerKey);
// 构建 OAuth2Client 选项
const oauth2Options = {
clientId: config.clientId,
clientSecret: config.clientSecret,
};
if (proxyConfig) {
oauth2Options.transporterOptions = proxyConfig;
console.log(`${config.logPrefix} Using proxy for OAuth token exchange`);
}
const authClient = new OAuth2Client(oauth2Options);
authClient.redirectUri = redirectUri;
const authUrl = authClient.generateAuthUrl({
access_type: 'offline',
prompt: 'select_account',
scope: config.scope
});
// 启动回调服务器
const credPath = path.join(os.homedir(), config.credentialsDir, config.credentialsFile);
try {
await createOAuthCallbackServer(config, redirectUri, authClient, credPath, providerKey, options);
} catch (error) {
throw new Error(`启动回调服务器失败: ${error.message}`);
}
return {
authUrl,
authInfo: {
provider: providerKey,
redirectUri: redirectUri,
port: port,
...options
}
};
}
/**
* 处理 Gemini CLI OAuth 授权
* @param {Object} currentConfig - 当前配置对象
* @param {Object} options - 额外选项
* @returns {Promise<Object>} 返回授权URL和相关信息
*/
export async function handleGeminiCliOAuth(currentConfig, options = {}) {
return handleGoogleOAuth('gemini-cli-oauth', currentConfig, options);
}
/**
* 处理 Gemini Antigravity OAuth 授权
* @param {Object} currentConfig - 当前配置对象
* @param {Object} options - 额外选项
* @returns {Promise<Object>} 返回授权URL和相关信息
*/
export async function handleGeminiAntigravityOAuth(currentConfig, options = {}) {
return handleGoogleOAuth('gemini-antigravity', currentConfig, options);
}