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
|
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 \
|
||||||
|
|
|
||||||
|
|
@ -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`:コンテナ名
|
||||||
|
|
|
||||||
|
|
@ -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`:容器名称
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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 // 生产环境已注释
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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');
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
};
|
};
|
||||||
Loading…
Reference in a new issue