diff --git a/Dockerfile b/Dockerfile index 04c436b..517d0a4 100644 --- a/Dockerfile +++ b/Dockerfile @@ -26,7 +26,7 @@ USER root RUN mkdir -p /app/logs # 暴露端口 -EXPOSE 3000 +EXPOSE 3000 8085 8086 19876-19880 # 添加健康检查 HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ diff --git a/README-JA.md b/README-JA.md index 6ad768f..76b5842 100644 --- a/README-JA.md +++ b/README-JA.md @@ -93,12 +93,12 @@ AIClient-2-APIを使い始める最も推奨される方法は、自動起動ス #### 🐳 Docker クイックスタート (推奨) ```bash -docker run -d -p 3000:3000 --restart=always -v "指定パス:/app/configs" --name aiclient2api justlikemaki/aiclient-2-api +docker run -d -p 3000:3000 -p 8085:8085 -p 8086:8086 -p 19876-19880:19876-19880 --restart=always -v "指定パス:/app/configs" --name aiclient2api justlikemaki/aiclient-2-api ``` **パラメータ説明**: - `-d`:バックグラウンドでコンテナを実行 -- `-p 3000:3000`:コンテナ内の3000ポートをホストの3000ポートにマッピング +- `-p 3000:3000 ...`:ポートマッピング。3000はWeb UI用、その他はOAuthコールバック用(Gemini: 8085, Antigravity: 8086, Kiro: 19876-19880) - `--restart=always`:コンテナ自動再起動ポリシー - `-v "指定パス:/app/configs"`:設定ディレクトリをマウント(「指定パス」を実際のパスに置き換えてください、例:`/home/user/aiclient-configs`) - `--name aiclient2api`:コンテナ名 diff --git a/README-ZH.md b/README-ZH.md index faf2ecf..14a703d 100644 --- a/README-ZH.md +++ b/README-ZH.md @@ -92,12 +92,12 @@ #### 🐳 Docker 快捷启动 (推荐) ```bash -docker run -d -p 3000:3000 --restart=always -v "指定路径:/app/configs" --name aiclient2api justlikemaki/aiclient-2-api +docker run -d -p 3000:3000 -p 8085:8085 -p 8086:8086 -p 19876-19880:19876-19880 --restart=always -v "指定路径:/app/configs" --name aiclient2api justlikemaki/aiclient-2-api ``` **参数说明**: - `-d`:后台运行容器 -- `-p 3000:3000`:将容器内 3000 端口映射到主机 3000 端口 +- `-p 3000:3000 ...`:端口映射。3000 为 Web UI,其余为 OAuth 回调端口(Gemini: 8085, Antigravity: 8086, Kiro: 19876-19880) - `--restart=always`:容器自动重启策略 - `-v "指定路径:/app/configs"`:挂载配置目录(请将"指定路径"替换为实际路径,如 `/home/user/aiclient-configs`) - `--name aiclient2api`:容器名称 diff --git a/README.md b/README.md index d0fbcf4..4150d03 100644 --- a/README.md +++ b/README.md @@ -93,12 +93,12 @@ The most recommended way to use AIClient-2-API is to start it through an automat #### 🐳 Docker Quick Start (Recommended) ```bash -docker run -d -p 3000:3000 --restart=always -v "your_path:/app/configs" --name aiclient2api justlikemaki/aiclient-2-api +docker run -d -p 3000:3000 -p 8085:8085 -p 8086:8086 -p 19876-19880:19876-19880 --restart=always -v "your_path:/app/configs" --name aiclient2api justlikemaki/aiclient-2-api ``` **Parameter Description**: - `-d`: Run container in background -- `-p 3000:3000`: Map container port 3000 to host port 3000 +- `-p 3000:3000 ...`: Port mapping. 3000 is for Web UI, others are for OAuth callbacks (Gemini: 8085, Antigravity: 8086, Kiro: 19876-19880) - `--restart=always`: Container auto-restart policy - `-v "your_path:/app/configs"`: Mount configuration directory (replace "your_path" with actual path, e.g., `/home/user/aiclient-configs`) - `--name aiclient2api`: Container name diff --git a/src/gemini/antigravity-core.js b/src/gemini/antigravity-core.js index 2ec805d..e7f4cfc 100644 --- a/src/gemini/antigravity-core.js +++ b/src/gemini/antigravity-core.js @@ -26,7 +26,6 @@ const httpsAgent = new https.Agent({ }); // --- Constants --- -const AUTH_REDIRECT_PORT = 8086; const CREDENTIALS_DIR = '.antigravity'; const CREDENTIALS_FILE = 'oauth_creds.json'; const DEFAULT_ANTIGRAVITY_BASE_URL_DAILY = 'https://daily-cloudcode-pa.sandbox.googleapis.com'; @@ -240,7 +239,6 @@ export class AntigravityApiService { this.config = config; this.host = config.HOST; this.oauthCredsFilePath = config.ANTIGRAVITY_OAUTH_CREDS_FILE_PATH; - this.baseURL = DEFAULT_ANTIGRAVITY_BASE_URL_DAILY; // 使用通用 GEMINI_BASE_URL 配置 this.userAgent = DEFAULT_USER_AGENT; // 支持通用 USER_AGENT 配置 this.projectId = config.PROJECT_ID; @@ -249,7 +247,7 @@ export class AntigravityApiService { this.baseUrlAutopush = config.ANTIGRAVITY_BASE_URL_AUTOPUSH || DEFAULT_ANTIGRAVITY_BASE_URL_AUTOPUSH; // 多环境降级顺序 - this.baseURLs = this.baseURL ? [this.baseURL] : [ + this.baseURLs = [ this.baseUrlDaily, this.baseUrlAutopush // ANTIGRAVITY_BASE_URL_PROD // 生产环境已注释 diff --git a/src/oauth-handlers.js b/src/oauth-handlers.js index aee4bca..5bc8d60 100644 --- a/src/oauth-handlers.js +++ b/src/oauth-handlers.js @@ -135,17 +135,33 @@ function generateResponsePage(isSuccess, message) { * @param {number} port - 端口号 * @returns {Promise} */ -async function closeActiveServer(port) { - const existingServer = activeServers.get(port); - if (existingServer && existingServer.listening) { - return new Promise((resolve) => { - existingServer.close(() => { - activeServers.delete(port); - console.log(`[OAuth] 已关闭端口 ${port} 上的旧服务器`); +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(); + }); + }); + } + } + } } /** @@ -158,8 +174,9 @@ async function closeActiveServer(port) { * @returns {Promise} HTTP 服务器实例 */ async function createOAuthCallbackServer(config, redirectUri, authClient, credPath, provider, options = {}) { - // 先关闭该端口上的旧服务器 - await closeActiveServer(config.port); + const port = parseInt(options.port) || config.port; + // 先关闭该提供商之前可能运行的所有服务器,或该端口上的旧服务器 + await closeActiveServer(provider, port); return new Promise((resolve, reject) => { const server = http.createServer(async (req, res) => { @@ -210,7 +227,7 @@ async function createOAuthCallbackServer(config, redirectUri, authClient, credPa res.end(generateResponsePage(false, `获取令牌失败: ${tokenError.message}`)); } finally { server.close(() => { - activeServers.delete(config.port); + activeServers.delete(provider); }); } } else if (errorParam) { @@ -220,7 +237,7 @@ async function createOAuthCallbackServer(config, redirectUri, authClient, credPa res.writeHead(400, { 'Content-Type': 'text/html; charset=utf-8' }); res.end(generateResponsePage(false, errorMessage)); server.close(() => { - activeServers.delete(config.port); + activeServers.delete(provider); }); } else { console.log(`${config.logPrefix} 忽略无关请求: ${req.url}`); @@ -234,7 +251,7 @@ async function createOAuthCallbackServer(config, redirectUri, authClient, credPa if (server.listening) { server.close(() => { - activeServers.delete(config.port); + activeServers.delete(provider); }); } } @@ -242,8 +259,8 @@ async function createOAuthCallbackServer(config, redirectUri, authClient, credPa server.on('error', (err) => { if (err.code === 'EADDRINUSE') { - console.error(`${config.logPrefix} 端口 ${config.port} 已被占用`); - reject(new Error(`端口 ${config.port} 已被占用`)); + console.error(`${config.logPrefix} 端口 ${port} 已被占用`); + reject(new Error(`端口 ${port} 已被占用`)); } else { console.error(`${config.logPrefix} 服务器错误:`, err); reject(err); @@ -251,9 +268,9 @@ async function createOAuthCallbackServer(config, redirectUri, authClient, credPa }); const host = '0.0.0.0'; - server.listen(config.port, host, () => { - console.log(`${config.logPrefix} OAuth 回调服务器已启动于 ${host}:${config.port}`); - activeServers.set(config.port, server); + server.listen(port, host, () => { + console.log(`${config.logPrefix} OAuth 回调服务器已启动于 ${host}:${port}`); + activeServers.set(provider, { server, port }); resolve(server); }); }); @@ -272,8 +289,9 @@ async function handleGoogleOAuth(providerKey, currentConfig, options = {}) { throw new Error(`未知的提供商: ${providerKey}`); } + const port = parseInt(options.port) || config.port; const host = 'localhost'; - const redirectUri = `http://${host}:${config.port}`; + const redirectUri = `http://${host}:${port}`; const authClient = new OAuth2Client(config.clientId, config.clientSecret); authClient.redirectUri = redirectUri; @@ -298,7 +316,8 @@ async function handleGoogleOAuth(providerKey, currentConfig, options = {}) { authInfo: { provider: providerKey, redirectUri: redirectUri, - port: config.port + port: port, + ...options } }; } @@ -607,7 +626,17 @@ async function handleKiroSocialAuth(provider, currentConfig, options = {}) { const state = crypto.randomBytes(16).toString('base64url'); // 启动本地回调服务器并获取端口 - const handlerPort = await startKiroCallbackServer(codeVerifier, state, options); + let handlerPort; + const providerKey = 'claude-kiro-oauth'; + if (options.port) { + const port = parseInt(options.port); + await closeKiroServer(providerKey, port); + const server = await createKiroHttpCallbackServer(port, codeVerifier, state, options); + activeKiroServers.set(providerKey, { server, port }); + handlerPort = port; + } else { + handlerPort = await startKiroCallbackServer(codeVerifier, state, options); + } // 使用 HTTP localhost 作为 redirect_uri const redirectUri = `http://127.0.0.1:${handlerPort}/oauth/callback`; @@ -629,7 +658,8 @@ async function handleKiroSocialAuth(provider, currentConfig, options = {}) { socialProvider: provider, port: handlerPort, redirectUri: redirectUri, - state: state + state: state, + ...options } }; } @@ -718,7 +748,8 @@ async function handleKiroBuilderIDDeviceCode(currentConfig, options = {}) { verificationUri: deviceAuth.verificationUri, verificationUriComplete: deviceAuth.verificationUriComplete, expiresIn: deviceAuth.expiresIn, - interval: deviceAuth.interval + interval: deviceAuth.interval, + ...options } }; } @@ -855,7 +886,7 @@ async function startKiroCallbackServer(codeVerifier, expectedState, options = {} try { const server = await createKiroHttpCallbackServer(port, codeVerifier, expectedState, options); - activeKiroServers.set(port, server); + activeKiroServers.set('claude-kiro-oauth', { server, port }); console.log(`${KIRO_OAUTH_CONFIG.logPrefix} 回调服务器已启动于端口 ${port}`); return port; } catch (err) { @@ -869,17 +900,31 @@ async function startKiroCallbackServer(codeVerifier, expectedState, options = {} /** * 关闭 Kiro 服务器 */ -async function closeKiroServer(port) { - const existingServer = activeKiroServers.get(port); - if (existingServer && existingServer.listening) { - return new Promise((resolve) => { - existingServer.close(() => { - activeKiroServers.delete(port); - console.log(`${KIRO_OAUTH_CONFIG.logPrefix} 已关闭端口 ${port} 上的旧服务器`); +async function closeKiroServer(provider, port = null) { + const existing = activeKiroServers.get(provider); + if (existing) { + await new Promise((resolve) => { + existing.server.close(() => { + activeKiroServers.delete(provider); + console.log(`${KIRO_OAUTH_CONFIG.logPrefix} 已关闭提供商 ${provider} 在端口 ${existing.port} 上的旧服务器`); resolve(); }); }); } + + if (port) { + for (const [p, info] of activeKiroServers.entries()) { + if (info.port === port) { + await new Promise((resolve) => { + info.server.close(() => { + activeKiroServers.delete(p); + console.log(`${KIRO_OAUTH_CONFIG.logPrefix} 已关闭端口 ${port} 上的旧服务器`); + resolve(); + }); + }); + } + } + } } /** @@ -975,7 +1020,7 @@ function createKiroHttpCallbackServer(port, codeVerifier, expectedState, options // 关闭服务器 server.close(() => { - activeKiroServers.delete(port); + activeKiroServers.delete('claude-kiro-oauth'); }); } else { @@ -996,7 +1041,7 @@ function createKiroHttpCallbackServer(port, codeVerifier, expectedState, options setTimeout(() => { if (server.listening) { server.close(() => { - activeKiroServers.delete(port); + activeKiroServers.delete('claude-kiro-oauth'); }); } }, KIRO_OAUTH_CONFIG.authTimeout); diff --git a/static/app/app.js b/static/app/app.js index 06d6483..e12226d 100644 --- a/static/app/app.js +++ b/static/app/app.js @@ -39,7 +39,8 @@ import { updateTimeDisplay, loadProviders, openProviderManager, - showAuthModal + showAuthModal, + executeGenerateAuthUrl } from './provider-manager.js'; import { @@ -150,6 +151,7 @@ window.showProviderManagerModal = showProviderManagerModal; window.refreshProviderConfig = refreshProviderConfig; window.fileUploadHandler = fileUploadHandler; window.showAuthModal = showAuthModal; +window.executeGenerateAuthUrl = executeGenerateAuthUrl; // 配置管理相关全局函数 window.viewConfig = viewConfig; diff --git a/static/app/event-handlers.js b/static/app/event-handlers.js index 5482306..94ca3f9 100644 --- a/static/app/event-handlers.js +++ b/static/app/event-handlers.js @@ -261,52 +261,13 @@ async function handleGenerateCreds(event) { * 实际执行授权逻辑 */ async function proceedWithAuth(providerType, targetInputId, extraOptions = {}) { - try { - showToast(t('common.info'), t('modal.provider.auth.initializing'), 'info'); - - // 使用 fileUploadHandler 中的 getProviderKey 获取目录名称 - const providerDir = fileUploadHandler.getProviderKey(providerType); - - const response = await window.apiClient.post( - `/providers/${encodeURIComponent(providerType)}/generate-auth-url`, - { - saveToConfigs: true, - providerDir: providerDir, - ...extraOptions - } - ); - - if (response.success && response.authUrl) { - // 使用自定义事件监听授权成功,以便自动填充路径 - const handleSuccess = (e) => { - const data = e.detail; - if (data.provider === providerType && data.relativePath) { - const input = document.getElementById(targetInputId); - if (input) { - input.value = data.relativePath; - input.dispatchEvent(new Event('input', { bubbles: true })); - showToast(t('common.success'), t('modal.provider.auth.success'), 'success'); - } - window.removeEventListener('oauth_success_event', handleSuccess); - } - }; - window.addEventListener('oauth_success_event', handleSuccess); - - // 调用 provider-manager.js 中的 showAuthModal (假设已在全局作用域或通过某种方式可用) - // 如果不可用,我们需要在 app.js 中导出它 - if (window.showAuthModal) { - window.showAuthModal(response.authUrl, response.authInfo); - } else { - // 降级处理:如果在 app.js 中没导出,尝试直接打开 - window.open(response.authUrl, '_blank'); - showToast(t('common.info'), t('modal.provider.auth.window'), 'info'); - } - } else { - showToast(t('common.error'), t('modal.provider.auth.failed'), 'error'); - } - } catch (error) { - console.error('生成凭据失败:', error); - showToast(t('common.error'), t('modal.provider.auth.failed') + `: ${error.message}`, 'error'); + if (window.executeGenerateAuthUrl) { + await window.executeGenerateAuthUrl(providerType, { + targetInputId, + ...extraOptions + }); + } else { + console.error('executeGenerateAuthUrl not found'); } } diff --git a/static/app/provider-manager.js b/static/app/provider-manager.js index 928d716..c9b5167 100644 --- a/static/app/provider-manager.js +++ b/static/app/provider-manager.js @@ -451,6 +451,24 @@ async function executeGenerateAuthUrl(providerType, extraOptions = {}) { ); if (response.success && response.authUrl) { + // 如果提供了 targetInputId,设置成功监听器 + if (extraOptions.targetInputId) { + const targetInputId = extraOptions.targetInputId; + const handleSuccess = (e) => { + const data = e.detail; + if (data.provider === providerType && data.relativePath) { + const input = document.getElementById(targetInputId); + if (input) { + input.value = data.relativePath; + input.dispatchEvent(new Event('input', { bubbles: true })); + showToast(t('common.success'), t('modal.provider.auth.success'), 'success'); + } + window.removeEventListener('oauth_success_event', handleSuccess); + } + }; + window.addEventListener('oauth_success_event', handleSuccess); + } + // 显示授权信息模态框 showAuthModal(response.authUrl, response.authInfo); } else { @@ -492,7 +510,8 @@ function showAuthModal(authUrl, authInfo) { // 获取需要开放的端口号(从 authInfo 或当前页面 URL) const requiredPort = authInfo.callbackPort || authInfo.port || window.location.port || '3000'; - + const isDeviceFlow = authInfo.provider === 'openai-qwen-oauth' || (authInfo.provider === 'claude-kiro-oauth' && authInfo.authMethod === 'builder-id'); + let instructionsHtml = ''; if (authInfo.provider === 'openai-qwen-oauth') { instructionsHtml = ` @@ -545,11 +564,19 @@ function showAuthModal(authUrl, authInfo) {

${t('oauth.modal.provider')} ${authInfo.provider}

-

+

${t('oauth.modal.requiredPort')} - ${requiredPort} -

+ ${isDeviceFlow ? + `${requiredPort}` : + `
+ + +
` + } +

${t('oauth.modal.portNote')}

${instructionsHtml} @@ -585,6 +612,25 @@ function showAuthModal(authUrl, authInfo) { }); }); + // 重新生成按钮事件 + const regenerateBtn = modal.querySelector('.regenerate-port-btn'); + if (regenerateBtn) { + regenerateBtn.onclick = async () => { + const newPort = modal.querySelector('.auth-port-input').value; + if (newPort && newPort !== requiredPort) { + modal.remove(); + // 构造重新请求的参数 + const options = { ...authInfo, port: newPort }; + // 移除不需要传递回后端的字段 + delete options.provider; + delete options.redirectUri; + delete options.callbackPort; + + await executeGenerateAuthUrl(authInfo.provider, options); + } + }; + } + // 复制链接按钮 const copyBtn = modal.querySelector('.copy-btn'); copyBtn.addEventListener('click', () => { @@ -715,5 +761,6 @@ export { renderProviders, updateProviderStatsDisplay, openProviderManager, - showAuthModal + showAuthModal, + executeGenerateAuthUrl }; \ No newline at end of file