feat(oauth): 重构OAuth授权流程并支持自定义端口
- 重构OAuth回调服务器管理,改为按提供商而非端口存储 - 在授权模态框中添加端口自定义功能 - 支持在生成授权URL时指定自定义端口 - 更新Dockerfile和文档以反映新增的OAuth端口需求 - 将授权逻辑从event-handlers.js移至provider-manager.js - 优化服务器关闭逻辑,避免端口冲突
This commit is contained in:
parent
16b7ee454b
commit
fc3eef0b3d
9 changed files with 147 additions and 94 deletions
|
|
@ -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 \
|
||||
|
|
|
|||
|
|
@ -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`:コンテナ名
|
||||
|
|
|
|||
|
|
@ -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`:容器名称
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 // 生产环境已注释
|
||||
|
|
|
|||
|
|
@ -135,17 +135,33 @@ function generateResponsePage(isSuccess, message) {
|
|||
* @param {number} port - 端口号
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
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.Server>} 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);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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) {
|
|||
<div class="auth-info">
|
||||
<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;">
|
||||
<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>
|
||||
<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>
|
||||
</p>
|
||||
${isDeviceFlow ?
|
||||
`<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>
|
||||
</div>
|
||||
${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
|
||||
};
|
||||
Loading…
Reference in a new issue