feat(oauth): 重构OAuth授权流程并支持自定义端口

- 重构OAuth回调服务器管理,改为按提供商而非端口存储
- 在授权模态框中添加端口自定义功能
- 支持在生成授权URL时指定自定义端口
- 更新Dockerfile和文档以反映新增的OAuth端口需求
- 将授权逻辑从event-handlers.js移至provider-manager.js
- 优化服务器关闭逻辑,避免端口冲突
This commit is contained in:
hex2077 2025-12-27 17:11:19 +08:00
parent 16b7ee454b
commit fc3eef0b3d
9 changed files with 147 additions and 94 deletions

View file

@ -26,7 +26,7 @@ USER root
RUN mkdir -p /app/logs RUN mkdir -p /app/logs
# 暴露端口 # 暴露端口
EXPOSE 3000 EXPOSE 3000 8085 8086 19876-19880
# 添加健康检查 # 添加健康检查
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \

View file

@ -93,12 +93,12 @@ AIClient-2-APIを使い始める最も推奨される方法は、自動起動ス
#### 🐳 Docker クイックスタート (推奨) #### 🐳 Docker クイックスタート (推奨)
```bash ```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`:バックグラウンドでコンテナを実行 - `-d`:バックグラウンドでコンテナを実行
- `-p 3000:3000`コンテナ内の3000ポートをホストの3000ポートにマッピング - `-p 3000:3000 ...`ポートマッピング。3000はWeb UI用、その他はOAuthコールバック用Gemini: 8085, Antigravity: 8086, Kiro: 19876-19880
- `--restart=always`:コンテナ自動再起動ポリシー - `--restart=always`:コンテナ自動再起動ポリシー
- `-v "指定パス:/app/configs"`:設定ディレクトリをマウント(「指定パス」を実際のパスに置き換えてください、例:`/home/user/aiclient-configs` - `-v "指定パス:/app/configs"`:設定ディレクトリをマウント(「指定パス」を実際のパスに置き換えてください、例:`/home/user/aiclient-configs`
- `--name aiclient2api`:コンテナ名 - `--name aiclient2api`:コンテナ名

View file

@ -92,12 +92,12 @@
#### 🐳 Docker 快捷启动 (推荐) #### 🐳 Docker 快捷启动 (推荐)
```bash ```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`:后台运行容器 - `-d`:后台运行容器
- `-p 3000:3000`:将容器内 3000 端口映射到主机 3000 端口 - `-p 3000:3000 ...`端口映射。3000 为 Web UI其余为 OAuth 回调端口Gemini: 8085, Antigravity: 8086, Kiro: 19876-19880
- `--restart=always`:容器自动重启策略 - `--restart=always`:容器自动重启策略
- `-v "指定路径:/app/configs"`:挂载配置目录(请将"指定路径"替换为实际路径,如 `/home/user/aiclient-configs` - `-v "指定路径:/app/configs"`:挂载配置目录(请将"指定路径"替换为实际路径,如 `/home/user/aiclient-configs`
- `--name aiclient2api`:容器名称 - `--name aiclient2api`:容器名称

View file

@ -93,12 +93,12 @@ The most recommended way to use AIClient-2-API is to start it through an automat
#### 🐳 Docker Quick Start (Recommended) #### 🐳 Docker Quick Start (Recommended)
```bash ```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**: **Parameter Description**:
- `-d`: Run container in background - `-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 - `--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`) - `-v "your_path:/app/configs"`: Mount configuration directory (replace "your_path" with actual path, e.g., `/home/user/aiclient-configs`)
- `--name aiclient2api`: Container name - `--name aiclient2api`: Container name

View file

@ -26,7 +26,6 @@ const httpsAgent = new https.Agent({
}); });
// --- Constants --- // --- Constants ---
const AUTH_REDIRECT_PORT = 8086;
const CREDENTIALS_DIR = '.antigravity'; const CREDENTIALS_DIR = '.antigravity';
const CREDENTIALS_FILE = 'oauth_creds.json'; const CREDENTIALS_FILE = 'oauth_creds.json';
const DEFAULT_ANTIGRAVITY_BASE_URL_DAILY = 'https://daily-cloudcode-pa.sandbox.googleapis.com'; const DEFAULT_ANTIGRAVITY_BASE_URL_DAILY = 'https://daily-cloudcode-pa.sandbox.googleapis.com';
@ -240,7 +239,6 @@ export class AntigravityApiService {
this.config = config; this.config = config;
this.host = config.HOST; this.host = config.HOST;
this.oauthCredsFilePath = config.ANTIGRAVITY_OAUTH_CREDS_FILE_PATH; 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.userAgent = DEFAULT_USER_AGENT; // 支持通用 USER_AGENT 配置
this.projectId = config.PROJECT_ID; 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.baseUrlAutopush = config.ANTIGRAVITY_BASE_URL_AUTOPUSH || DEFAULT_ANTIGRAVITY_BASE_URL_AUTOPUSH;
// 多环境降级顺序 // 多环境降级顺序
this.baseURLs = this.baseURL ? [this.baseURL] : [ this.baseURLs = [
this.baseUrlDaily, this.baseUrlDaily,
this.baseUrlAutopush this.baseUrlAutopush
// ANTIGRAVITY_BASE_URL_PROD // 生产环境已注释 // ANTIGRAVITY_BASE_URL_PROD // 生产环境已注释

View file

@ -135,17 +135,33 @@ function generateResponsePage(isSuccess, message) {
* @param {number} port - 端口号 * @param {number} port - 端口号
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
async function closeActiveServer(port) { async function closeActiveServer(provider, port = null) {
const existingServer = activeServers.get(port); // 1. 关闭该提供商之前的所有服务器
if (existingServer && existingServer.listening) { const existing = activeServers.get(provider);
return new Promise((resolve) => { if (existing) {
existingServer.close(() => { await new Promise((resolve) => {
activeServers.delete(port); existing.server.close(() => {
console.log(`[OAuth] 已关闭端口 ${port} 上的旧服务器`); activeServers.delete(provider);
console.log(`[OAuth] 已关闭提供商 ${provider} 在端口 ${existing.port} 上的旧服务器`);
resolve(); 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.Server>} HTTP 服务器实例 * @returns {Promise<http.Server>} HTTP 服务器实例
*/ */
async function createOAuthCallbackServer(config, redirectUri, authClient, credPath, provider, options = {}) { async function createOAuthCallbackServer(config, redirectUri, authClient, credPath, provider, options = {}) {
// 先关闭该端口上的旧服务器 const port = parseInt(options.port) || config.port;
await closeActiveServer(config.port); // 先关闭该提供商之前可能运行的所有服务器,或该端口上的旧服务器
await closeActiveServer(provider, port);
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const server = http.createServer(async (req, res) => { const server = http.createServer(async (req, res) => {
@ -210,7 +227,7 @@ async function createOAuthCallbackServer(config, redirectUri, authClient, credPa
res.end(generateResponsePage(false, `获取令牌失败: ${tokenError.message}`)); res.end(generateResponsePage(false, `获取令牌失败: ${tokenError.message}`));
} finally { } finally {
server.close(() => { server.close(() => {
activeServers.delete(config.port); activeServers.delete(provider);
}); });
} }
} else if (errorParam) { } 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.writeHead(400, { 'Content-Type': 'text/html; charset=utf-8' });
res.end(generateResponsePage(false, errorMessage)); res.end(generateResponsePage(false, errorMessage));
server.close(() => { server.close(() => {
activeServers.delete(config.port); activeServers.delete(provider);
}); });
} else { } else {
console.log(`${config.logPrefix} 忽略无关请求: ${req.url}`); console.log(`${config.logPrefix} 忽略无关请求: ${req.url}`);
@ -234,7 +251,7 @@ async function createOAuthCallbackServer(config, redirectUri, authClient, credPa
if (server.listening) { if (server.listening) {
server.close(() => { server.close(() => {
activeServers.delete(config.port); activeServers.delete(provider);
}); });
} }
} }
@ -242,8 +259,8 @@ async function createOAuthCallbackServer(config, redirectUri, authClient, credPa
server.on('error', (err) => { server.on('error', (err) => {
if (err.code === 'EADDRINUSE') { if (err.code === 'EADDRINUSE') {
console.error(`${config.logPrefix} 端口 ${config.port} 已被占用`); console.error(`${config.logPrefix} 端口 ${port} 已被占用`);
reject(new Error(`端口 ${config.port} 已被占用`)); reject(new Error(`端口 ${port} 已被占用`));
} else { } else {
console.error(`${config.logPrefix} 服务器错误:`, err); console.error(`${config.logPrefix} 服务器错误:`, err);
reject(err); reject(err);
@ -251,9 +268,9 @@ async function createOAuthCallbackServer(config, redirectUri, authClient, credPa
}); });
const host = '0.0.0.0'; const host = '0.0.0.0';
server.listen(config.port, host, () => { server.listen(port, host, () => {
console.log(`${config.logPrefix} OAuth 回调服务器已启动于 ${host}:${config.port}`); console.log(`${config.logPrefix} OAuth 回调服务器已启动于 ${host}:${port}`);
activeServers.set(config.port, server); activeServers.set(provider, { server, port });
resolve(server); resolve(server);
}); });
}); });
@ -272,8 +289,9 @@ async function handleGoogleOAuth(providerKey, currentConfig, options = {}) {
throw new Error(`未知的提供商: ${providerKey}`); throw new Error(`未知的提供商: ${providerKey}`);
} }
const port = parseInt(options.port) || config.port;
const host = 'localhost'; const host = 'localhost';
const redirectUri = `http://${host}:${config.port}`; const redirectUri = `http://${host}:${port}`;
const authClient = new OAuth2Client(config.clientId, config.clientSecret); const authClient = new OAuth2Client(config.clientId, config.clientSecret);
authClient.redirectUri = redirectUri; authClient.redirectUri = redirectUri;
@ -298,7 +316,8 @@ async function handleGoogleOAuth(providerKey, currentConfig, options = {}) {
authInfo: { authInfo: {
provider: providerKey, provider: providerKey,
redirectUri: redirectUri, 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 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 // 使用 HTTP localhost 作为 redirect_uri
const redirectUri = `http://127.0.0.1:${handlerPort}/oauth/callback`; const redirectUri = `http://127.0.0.1:${handlerPort}/oauth/callback`;
@ -629,7 +658,8 @@ async function handleKiroSocialAuth(provider, currentConfig, options = {}) {
socialProvider: provider, socialProvider: provider,
port: handlerPort, port: handlerPort,
redirectUri: redirectUri, redirectUri: redirectUri,
state: state state: state,
...options
} }
}; };
} }
@ -718,7 +748,8 @@ async function handleKiroBuilderIDDeviceCode(currentConfig, options = {}) {
verificationUri: deviceAuth.verificationUri, verificationUri: deviceAuth.verificationUri,
verificationUriComplete: deviceAuth.verificationUriComplete, verificationUriComplete: deviceAuth.verificationUriComplete,
expiresIn: deviceAuth.expiresIn, expiresIn: deviceAuth.expiresIn,
interval: deviceAuth.interval interval: deviceAuth.interval,
...options
} }
}; };
} }
@ -855,7 +886,7 @@ async function startKiroCallbackServer(codeVerifier, expectedState, options = {}
try { try {
const server = await createKiroHttpCallbackServer(port, codeVerifier, expectedState, options); 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}`); console.log(`${KIRO_OAUTH_CONFIG.logPrefix} 回调服务器已启动于端口 ${port}`);
return port; return port;
} catch (err) { } catch (err) {
@ -869,17 +900,31 @@ async function startKiroCallbackServer(codeVerifier, expectedState, options = {}
/** /**
* 关闭 Kiro 服务器 * 关闭 Kiro 服务器
*/ */
async function closeKiroServer(port) { async function closeKiroServer(provider, port = null) {
const existingServer = activeKiroServers.get(port); const existing = activeKiroServers.get(provider);
if (existingServer && existingServer.listening) { if (existing) {
return new Promise((resolve) => { await new Promise((resolve) => {
existingServer.close(() => { existing.server.close(() => {
activeKiroServers.delete(port); activeKiroServers.delete(provider);
console.log(`${KIRO_OAUTH_CONFIG.logPrefix} 已关闭端口 ${port} 上的旧服务器`); console.log(`${KIRO_OAUTH_CONFIG.logPrefix} 已关闭提供商 ${provider}端口 ${existing.port} 上的旧服务器`);
resolve(); 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(() => { server.close(() => {
activeKiroServers.delete(port); activeKiroServers.delete('claude-kiro-oauth');
}); });
} else { } else {
@ -996,7 +1041,7 @@ function createKiroHttpCallbackServer(port, codeVerifier, expectedState, options
setTimeout(() => { setTimeout(() => {
if (server.listening) { if (server.listening) {
server.close(() => { server.close(() => {
activeKiroServers.delete(port); activeKiroServers.delete('claude-kiro-oauth');
}); });
} }
}, KIRO_OAUTH_CONFIG.authTimeout); }, KIRO_OAUTH_CONFIG.authTimeout);

View file

@ -39,7 +39,8 @@ import {
updateTimeDisplay, updateTimeDisplay,
loadProviders, loadProviders,
openProviderManager, openProviderManager,
showAuthModal showAuthModal,
executeGenerateAuthUrl
} from './provider-manager.js'; } from './provider-manager.js';
import { import {
@ -150,6 +151,7 @@ window.showProviderManagerModal = showProviderManagerModal;
window.refreshProviderConfig = refreshProviderConfig; window.refreshProviderConfig = refreshProviderConfig;
window.fileUploadHandler = fileUploadHandler; window.fileUploadHandler = fileUploadHandler;
window.showAuthModal = showAuthModal; window.showAuthModal = showAuthModal;
window.executeGenerateAuthUrl = executeGenerateAuthUrl;
// 配置管理相关全局函数 // 配置管理相关全局函数
window.viewConfig = viewConfig; window.viewConfig = viewConfig;

View file

@ -261,52 +261,13 @@ async function handleGenerateCreds(event) {
* 实际执行授权逻辑 * 实际执行授权逻辑
*/ */
async function proceedWithAuth(providerType, targetInputId, extraOptions = {}) { async function proceedWithAuth(providerType, targetInputId, extraOptions = {}) {
try { if (window.executeGenerateAuthUrl) {
showToast(t('common.info'), t('modal.provider.auth.initializing'), 'info'); await window.executeGenerateAuthUrl(providerType, {
targetInputId,
// 使用 fileUploadHandler 中的 getProviderKey 获取目录名称 ...extraOptions
const providerDir = fileUploadHandler.getProviderKey(providerType); });
} else {
const response = await window.apiClient.post( console.error('executeGenerateAuthUrl not found');
`/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');
} }
} }

View file

@ -451,6 +451,24 @@ async function executeGenerateAuthUrl(providerType, extraOptions = {}) {
); );
if (response.success && response.authUrl) { 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); showAuthModal(response.authUrl, response.authInfo);
} else { } else {
@ -492,7 +510,8 @@ function showAuthModal(authUrl, authInfo) {
// 获取需要开放的端口号(从 authInfo 或当前页面 URL // 获取需要开放的端口号(从 authInfo 或当前页面 URL
const requiredPort = authInfo.callbackPort || authInfo.port || window.location.port || '3000'; 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 = ''; let instructionsHtml = '';
if (authInfo.provider === 'openai-qwen-oauth') { if (authInfo.provider === 'openai-qwen-oauth') {
instructionsHtml = ` instructionsHtml = `
@ -545,11 +564,19 @@ function showAuthModal(authUrl, authInfo) {
<div class="auth-info"> <div class="auth-info">
<p><strong data-i18n="oauth.modal.provider">${t('oauth.modal.provider')}</strong> ${authInfo.provider}</p> <p><strong data-i18n="oauth.modal.provider">${t('oauth.modal.provider')}</strong> ${authInfo.provider}</p>
<div class="port-info-section" style="margin: 12px 0; padding: 12px; background: #fef3c7; border: 1px solid #fcd34d; border-radius: 8px;"> <div class="port-info-section" style="margin: 12px 0; padding: 12px; background: #fef3c7; border: 1px solid #fcd34d; border-radius: 8px;">
<p style="margin: 0; display: flex; align-items: center; gap: 8px;"> <div style="margin: 0; display: flex; align-items: center; gap: 8px; flex-wrap: wrap;">
<i class="fas fa-network-wired" style="color: #d97706;"></i> <i class="fas fa-network-wired" style="color: #d97706;"></i>
<strong data-i18n="oauth.modal.requiredPort">${t('oauth.modal.requiredPort')}</strong> <strong data-i18n="oauth.modal.requiredPort">${t('oauth.modal.requiredPort')}</strong>
<code style="background: #fff; padding: 2px 8px; border-radius: 4px; font-weight: bold; color: #d97706;">${requiredPort}</code> ${isDeviceFlow ?
</p> `<code style="background: #fff; padding: 2px 8px; border-radius: 4px; font-weight: bold; color: #d97706;">${requiredPort}</code>` :
`<div style="display: flex; align-items: center; gap: 4px;">
<input type="number" class="auth-port-input" value="${requiredPort}" style="width: 80px; padding: 2px 8px; border: 1px solid #d97706; border-radius: 4px; font-weight: bold; color: #d97706; background: white;">
<button class="regenerate-port-btn" title="${t('common.generate')}" style="background: none; border: 1px solid #d97706; border-radius: 4px; cursor: pointer; color: #d97706; padding: 2px 6px;">
<i class="fas fa-sync-alt"></i>
</button>
</div>`
}
</div>
<p style="margin: 8px 0 0 0; font-size: 0.85rem; color: #92400e;" data-i18n="oauth.modal.portNote">${t('oauth.modal.portNote')}</p> <p style="margin: 8px 0 0 0; font-size: 0.85rem; color: #92400e;" data-i18n="oauth.modal.portNote">${t('oauth.modal.portNote')}</p>
</div> </div>
${instructionsHtml} ${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'); const copyBtn = modal.querySelector('.copy-btn');
copyBtn.addEventListener('click', () => { copyBtn.addEventListener('click', () => {
@ -715,5 +761,6 @@ export {
renderProviders, renderProviders,
updateProviderStatsDisplay, updateProviderStatsDisplay,
openProviderManager, openProviderManager,
showAuthModal showAuthModal,
executeGenerateAuthUrl
}; };