Merge pull request #119 from Ravens2121/feature/claude-kiro-oauth
feat: 为 claude-kiro-oauth 提供商添加 OAuth 授权生成功能(支持 Google/GitHub/AWS Builder ID)
This commit is contained in:
commit
abf874b43c
3 changed files with 585 additions and 3 deletions
|
|
@ -55,6 +55,54 @@ const QWEN_OAUTH_CONFIG = {
|
|||
logPrefix: '[Qwen Auth]'
|
||||
};
|
||||
|
||||
/**
|
||||
* Kiro OAuth 配置(支持多种认证方式)
|
||||
*/
|
||||
const KIRO_OAUTH_CONFIG = {
|
||||
// Kiro Auth Service 端点 (用于 Social Auth)
|
||||
authServiceEndpoint: 'https://prod.us-east-1.auth.desktop.kiro.dev',
|
||||
|
||||
// AWS SSO OIDC 端点 (用于 Builder ID)
|
||||
ssoOIDCEndpoint: 'https://oidc.us-east-1.amazonaws.com',
|
||||
|
||||
// AWS Builder ID 起始 URL
|
||||
builderIDStartURL: 'https://view.awsapps.com/start',
|
||||
|
||||
// 本地回调端口范围(用于 Social Auth HTTP 回调)
|
||||
callbackPortStart: 19876,
|
||||
callbackPortEnd: 19880,
|
||||
|
||||
// 超时配置
|
||||
authTimeout: 10 * 60 * 1000, // 10 分钟
|
||||
pollInterval: 5000, // 5 秒
|
||||
|
||||
// CodeWhisperer Scopes
|
||||
scopes: [
|
||||
'codewhisperer:completions',
|
||||
'codewhisperer:analysis',
|
||||
'codewhisperer:conversations',
|
||||
'codewhisperer:transformations',
|
||||
'codewhisperer:taskassist'
|
||||
],
|
||||
|
||||
// 凭据存储(符合现有规范)
|
||||
credentialsDir: '.kiro',
|
||||
credentialsFile: 'oauth_creds.json',
|
||||
|
||||
// 日志前缀
|
||||
logPrefix: '[Kiro Auth]'
|
||||
};
|
||||
|
||||
/**
|
||||
* 活动的 Kiro 回调服务器管理
|
||||
*/
|
||||
const activeKiroServers = new Map();
|
||||
|
||||
/**
|
||||
* 活动的 Kiro 轮询任务管理(用于 Builder ID Device Code)
|
||||
*/
|
||||
const activeKiroPollingTasks = new Map();
|
||||
|
||||
/**
|
||||
* 生成 HTML 响应页面
|
||||
* @param {boolean} isSuccess - 是否成功
|
||||
|
|
@ -514,4 +562,426 @@ export async function handleQwenOAuth(currentConfig, options = {}) {
|
|||
console.error(`${QWEN_OAUTH_CONFIG.logPrefix} 请求失败:`, error);
|
||||
throw new Error(`Qwen OAuth 授权失败: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理 Kiro OAuth 授权(统一入口)
|
||||
* @param {Object} currentConfig - 当前配置对象
|
||||
* @param {Object} options - 额外选项
|
||||
* - method: 'google' | 'github' | 'builder-id'
|
||||
* - saveToConfigs: boolean
|
||||
* @returns {Promise<Object>} 返回授权URL和相关信息
|
||||
*/
|
||||
export async function handleKiroOAuth(currentConfig, options = {}) {
|
||||
const method = options.method || 'google'; // 默认使用 Google
|
||||
|
||||
console.log(`${KIRO_OAUTH_CONFIG.logPrefix} Starting OAuth with method: ${method}`);
|
||||
|
||||
switch (method) {
|
||||
case 'google':
|
||||
return handleKiroSocialAuth('Google', currentConfig, options);
|
||||
case 'github':
|
||||
return handleKiroSocialAuth('Github', currentConfig, options);
|
||||
case 'builder-id':
|
||||
return handleKiroBuilderIDDeviceCode(currentConfig, options);
|
||||
default:
|
||||
throw new Error(`不支持的认证方式: ${method}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Kiro Social Auth (Google/GitHub) - 使用 HTTP localhost 回调
|
||||
*/
|
||||
async function handleKiroSocialAuth(provider, currentConfig, options = {}) {
|
||||
// 生成 PKCE 参数
|
||||
const codeVerifier = generateCodeVerifier();
|
||||
const codeChallenge = generateCodeChallenge(codeVerifier);
|
||||
const state = crypto.randomBytes(16).toString('base64url');
|
||||
|
||||
// 启动本地回调服务器并获取端口
|
||||
const handlerPort = await startKiroCallbackServer(codeVerifier, state, options);
|
||||
|
||||
// 使用 HTTP localhost 作为 redirect_uri
|
||||
const redirectUri = `http://127.0.0.1:${handlerPort}/oauth/callback`;
|
||||
|
||||
// 构建授权 URL
|
||||
const authUrl = `${KIRO_OAUTH_CONFIG.authServiceEndpoint}/login?` +
|
||||
`idp=${provider}&` +
|
||||
`redirect_uri=${encodeURIComponent(redirectUri)}&` +
|
||||
`code_challenge=${codeChallenge}&` +
|
||||
`code_challenge_method=S256&` +
|
||||
`state=${state}&` +
|
||||
`prompt=select_account`;
|
||||
|
||||
return {
|
||||
authUrl,
|
||||
authInfo: {
|
||||
provider: 'claude-kiro-oauth',
|
||||
authMethod: 'social',
|
||||
socialProvider: provider,
|
||||
port: handlerPort,
|
||||
redirectUri: redirectUri,
|
||||
state: state
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Kiro Builder ID - Device Code Flow(类似 Qwen OAuth 模式)
|
||||
*/
|
||||
async function handleKiroBuilderIDDeviceCode(currentConfig, options = {}) {
|
||||
// 1. 注册 OIDC 客户端
|
||||
const regResponse = await fetch(`${KIRO_OAUTH_CONFIG.ssoOIDCEndpoint}/client/register`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'User-Agent': 'KiroIDE'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
clientName: 'Kiro IDE',
|
||||
clientType: 'public',
|
||||
scopes: KIRO_OAUTH_CONFIG.scopes,
|
||||
grantTypes: ['urn:ietf:params:oauth:grant-type:device_code', 'refresh_token']
|
||||
})
|
||||
});
|
||||
|
||||
if (!regResponse.ok) {
|
||||
throw new Error(`Kiro OAuth 客户端注册失败: ${regResponse.status}`);
|
||||
}
|
||||
|
||||
const regData = await regResponse.json();
|
||||
|
||||
// 2. 启动设备授权
|
||||
const authResponse = await fetch(`${KIRO_OAUTH_CONFIG.ssoOIDCEndpoint}/device_authorization`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'User-Agent': 'KiroIDE'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
clientId: regData.clientId,
|
||||
clientSecret: regData.clientSecret,
|
||||
startUrl: KIRO_OAUTH_CONFIG.builderIDStartURL
|
||||
})
|
||||
});
|
||||
|
||||
if (!authResponse.ok) {
|
||||
throw new Error(`Kiro OAuth 设备授权失败: ${authResponse.status}`);
|
||||
}
|
||||
|
||||
const deviceAuth = await authResponse.json();
|
||||
|
||||
// 3. 启动后台轮询(类似 Qwen OAuth 的模式)
|
||||
const taskId = `kiro-${deviceAuth.deviceCode.substring(0, 8)}-${Date.now()}`;
|
||||
|
||||
// 停止之前的轮询任务
|
||||
for (const [existingTaskId] of activeKiroPollingTasks.entries()) {
|
||||
if (existingTaskId.startsWith('kiro-')) {
|
||||
stopKiroPollingTask(existingTaskId);
|
||||
}
|
||||
}
|
||||
|
||||
// 异步轮询
|
||||
pollKiroBuilderIDToken(
|
||||
regData.clientId,
|
||||
regData.clientSecret,
|
||||
deviceAuth.deviceCode,
|
||||
deviceAuth.interval || 5,
|
||||
deviceAuth.expiresIn || 300,
|
||||
taskId,
|
||||
options
|
||||
).catch(error => {
|
||||
console.error(`${KIRO_OAUTH_CONFIG.logPrefix} 轮询失败 [${taskId}]:`, error);
|
||||
broadcastEvent('oauth_error', {
|
||||
provider: 'claude-kiro-oauth',
|
||||
error: error.message,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
});
|
||||
|
||||
return {
|
||||
authUrl: deviceAuth.verificationUriComplete,
|
||||
authInfo: {
|
||||
provider: 'claude-kiro-oauth',
|
||||
authMethod: 'builder-id',
|
||||
deviceCode: deviceAuth.deviceCode,
|
||||
userCode: deviceAuth.userCode,
|
||||
verificationUri: deviceAuth.verificationUri,
|
||||
verificationUriComplete: deviceAuth.verificationUriComplete,
|
||||
expiresIn: deviceAuth.expiresIn,
|
||||
interval: deviceAuth.interval
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 轮询获取 Kiro Builder ID Token
|
||||
*/
|
||||
async function pollKiroBuilderIDToken(clientId, clientSecret, deviceCode, interval, expiresIn, taskId, options = {}) {
|
||||
let credPath = path.join(os.homedir(), KIRO_OAUTH_CONFIG.credentialsDir, KIRO_OAUTH_CONFIG.credentialsFile);
|
||||
const maxAttempts = Math.floor(expiresIn / interval);
|
||||
let attempts = 0;
|
||||
|
||||
const taskControl = { shouldStop: false };
|
||||
activeKiroPollingTasks.set(taskId, taskControl);
|
||||
|
||||
console.log(`${KIRO_OAUTH_CONFIG.logPrefix} 开始轮询令牌 [${taskId}]`);
|
||||
|
||||
const poll = async () => {
|
||||
if (taskControl.shouldStop) {
|
||||
throw new Error('轮询任务已被取消');
|
||||
}
|
||||
|
||||
if (attempts >= maxAttempts) {
|
||||
activeKiroPollingTasks.delete(taskId);
|
||||
throw new Error('授权超时');
|
||||
}
|
||||
|
||||
attempts++;
|
||||
|
||||
try {
|
||||
const response = await fetch(`${KIRO_OAUTH_CONFIG.ssoOIDCEndpoint}/token`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'User-Agent': 'KiroIDE'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
clientId,
|
||||
clientSecret,
|
||||
deviceCode,
|
||||
grantType: 'urn:ietf:params:oauth:grant-type:device_code'
|
||||
})
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (response.ok && data.accessToken) {
|
||||
console.log(`${KIRO_OAUTH_CONFIG.logPrefix} 成功获取令牌 [${taskId}]`);
|
||||
|
||||
// 保存令牌(符合现有规范)
|
||||
if (options.saveToConfigs) {
|
||||
const targetDir = path.join(process.cwd(), 'configs', 'kiro');
|
||||
await fs.promises.mkdir(targetDir, { recursive: true });
|
||||
const timestamp = Date.now();
|
||||
credPath = path.join(targetDir, `${timestamp}_oauth_creds.json`);
|
||||
}
|
||||
|
||||
const tokenData = {
|
||||
accessToken: data.accessToken,
|
||||
refreshToken: data.refreshToken,
|
||||
expiresAt: new Date(Date.now() + data.expiresIn * 1000).toISOString(),
|
||||
authMethod: 'builder-id',
|
||||
clientId,
|
||||
clientSecret,
|
||||
region: 'us-east-1'
|
||||
};
|
||||
|
||||
await fs.promises.mkdir(path.dirname(credPath), { recursive: true });
|
||||
await fs.promises.writeFile(credPath, JSON.stringify(tokenData, null, 2));
|
||||
|
||||
activeKiroPollingTasks.delete(taskId);
|
||||
|
||||
// 广播成功事件(符合现有规范)
|
||||
broadcastEvent('oauth_success', {
|
||||
provider: 'claude-kiro-oauth',
|
||||
credPath,
|
||||
relativePath: path.relative(process.cwd(), credPath),
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
|
||||
return tokenData;
|
||||
}
|
||||
|
||||
// 检查错误类型
|
||||
if (data.error === 'authorization_pending') {
|
||||
console.log(`${KIRO_OAUTH_CONFIG.logPrefix} 等待用户授权 [${taskId}]... (${attempts}/${maxAttempts})`);
|
||||
await new Promise(resolve => setTimeout(resolve, interval * 1000));
|
||||
return poll();
|
||||
} else if (data.error === 'slow_down') {
|
||||
await new Promise(resolve => setTimeout(resolve, (interval + 5) * 1000));
|
||||
return poll();
|
||||
} else {
|
||||
activeKiroPollingTasks.delete(taskId);
|
||||
throw new Error(`授权失败: ${data.error || '未知错误'}`);
|
||||
}
|
||||
} catch (error) {
|
||||
if (error.message.includes('授权') || error.message.includes('取消')) {
|
||||
throw error;
|
||||
}
|
||||
await new Promise(resolve => setTimeout(resolve, interval * 1000));
|
||||
return poll();
|
||||
}
|
||||
};
|
||||
|
||||
return poll();
|
||||
}
|
||||
|
||||
/**
|
||||
* 停止 Kiro 轮询任务
|
||||
*/
|
||||
function stopKiroPollingTask(taskId) {
|
||||
const task = activeKiroPollingTasks.get(taskId);
|
||||
if (task) {
|
||||
task.shouldStop = true;
|
||||
activeKiroPollingTasks.delete(taskId);
|
||||
console.log(`${KIRO_OAUTH_CONFIG.logPrefix} 已停止轮询任务: ${taskId}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 启动 Kiro 回调服务器(用于 Social Auth HTTP 回调)
|
||||
*/
|
||||
async function startKiroCallbackServer(codeVerifier, expectedState, options = {}) {
|
||||
const portStart = KIRO_OAUTH_CONFIG.callbackPortStart;
|
||||
const portEnd = KIRO_OAUTH_CONFIG.callbackPortEnd;
|
||||
|
||||
for (let port = portStart; port <= portEnd; port++) {
|
||||
// 关闭已存在的服务器
|
||||
await closeKiroServer(port);
|
||||
|
||||
try {
|
||||
const server = await createKiroHttpCallbackServer(port, codeVerifier, expectedState, options);
|
||||
activeKiroServers.set(port, server);
|
||||
console.log(`${KIRO_OAUTH_CONFIG.logPrefix} 回调服务器已启动于端口 ${port}`);
|
||||
return port;
|
||||
} catch (err) {
|
||||
console.log(`${KIRO_OAUTH_CONFIG.logPrefix} 端口 ${port} 被占用,尝试下一个...`);
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error('所有端口都被占用');
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭 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} 上的旧服务器`);
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建 Kiro HTTP 回调服务器
|
||||
*/
|
||||
function createKiroHttpCallbackServer(port, codeVerifier, expectedState, options = {}) {
|
||||
const redirectUri = `http://127.0.0.1:${port}/oauth/callback`;
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const server = http.createServer(async (req, res) => {
|
||||
try {
|
||||
const url = new URL(req.url, `http://127.0.0.1:${port}`);
|
||||
|
||||
if (url.pathname === '/oauth/callback') {
|
||||
const code = url.searchParams.get('code');
|
||||
const state = url.searchParams.get('state');
|
||||
const errorParam = url.searchParams.get('error');
|
||||
|
||||
if (errorParam) {
|
||||
res.writeHead(400, { 'Content-Type': 'text/html; charset=utf-8' });
|
||||
res.end(generateResponsePage(false, `授权失败: ${errorParam}`));
|
||||
return;
|
||||
}
|
||||
|
||||
if (state !== expectedState) {
|
||||
res.writeHead(400, { 'Content-Type': 'text/html; charset=utf-8' });
|
||||
res.end(generateResponsePage(false, 'State 验证失败'));
|
||||
return;
|
||||
}
|
||||
|
||||
// 交换 Code 获取 Token(使用动态的 redirect_uri)
|
||||
const tokenResponse = await fetch(`${KIRO_OAUTH_CONFIG.authServiceEndpoint}/oauth/token`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'User-Agent': 'AIClient-2-API/1.0.0'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
code,
|
||||
code_verifier: codeVerifier,
|
||||
redirect_uri: redirectUri
|
||||
})
|
||||
});
|
||||
|
||||
if (!tokenResponse.ok) {
|
||||
const errorText = await tokenResponse.text();
|
||||
console.error(`${KIRO_OAUTH_CONFIG.logPrefix} Token exchange failed:`, errorText);
|
||||
res.writeHead(500, { 'Content-Type': 'text/html; charset=utf-8' });
|
||||
res.end(generateResponsePage(false, `获取令牌失败: ${tokenResponse.status}`));
|
||||
return;
|
||||
}
|
||||
|
||||
const tokenData = await tokenResponse.json();
|
||||
|
||||
// 保存令牌
|
||||
let credPath = path.join(os.homedir(), KIRO_OAUTH_CONFIG.credentialsDir, KIRO_OAUTH_CONFIG.credentialsFile);
|
||||
|
||||
if (options.saveToConfigs) {
|
||||
const targetDir = path.join(process.cwd(), 'configs', 'kiro');
|
||||
await fs.promises.mkdir(targetDir, { recursive: true });
|
||||
const timestamp = Date.now();
|
||||
credPath = path.join(targetDir, `${timestamp}_oauth_creds.json`);
|
||||
}
|
||||
|
||||
const saveData = {
|
||||
accessToken: tokenData.accessToken,
|
||||
refreshToken: tokenData.refreshToken,
|
||||
profileArn: tokenData.profileArn,
|
||||
expiresAt: new Date(Date.now() + (tokenData.expiresIn || 3600) * 1000).toISOString(),
|
||||
authMethod: 'social',
|
||||
region: 'us-east-1'
|
||||
};
|
||||
|
||||
await fs.promises.mkdir(path.dirname(credPath), { recursive: true });
|
||||
await fs.promises.writeFile(credPath, JSON.stringify(saveData, null, 2));
|
||||
|
||||
console.log(`${KIRO_OAUTH_CONFIG.logPrefix} 令牌已保存: ${credPath}`);
|
||||
|
||||
// 广播成功事件
|
||||
broadcastEvent('oauth_success', {
|
||||
provider: 'claude-kiro-oauth',
|
||||
credPath,
|
||||
relativePath: path.relative(process.cwd(), credPath),
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
|
||||
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
|
||||
res.end(generateResponsePage(true, '授权成功!您可以关闭此页面'));
|
||||
|
||||
// 关闭服务器
|
||||
server.close(() => {
|
||||
activeKiroServers.delete(port);
|
||||
});
|
||||
|
||||
} else {
|
||||
res.writeHead(204);
|
||||
res.end();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`${KIRO_OAUTH_CONFIG.logPrefix} 处理回调出错:`, error);
|
||||
res.writeHead(500, { 'Content-Type': 'text/html; charset=utf-8' });
|
||||
res.end(generateResponsePage(false, `服务器错误: ${error.message}`));
|
||||
}
|
||||
});
|
||||
|
||||
server.on('error', reject);
|
||||
server.listen(port, '127.0.0.1', () => resolve(server));
|
||||
|
||||
// 超时自动关闭
|
||||
setTimeout(() => {
|
||||
if (server.listening) {
|
||||
server.close(() => {
|
||||
activeKiroServers.delete(port);
|
||||
});
|
||||
}
|
||||
}, KIRO_OAUTH_CONFIG.authTimeout);
|
||||
});
|
||||
}
|
||||
|
|
@ -9,7 +9,7 @@ import { getAllProviderModels, getProviderModels } from './provider-models.js';
|
|||
import { CONFIG } from './config-manager.js';
|
||||
import { serviceInstances, getServiceAdapter } from './adapter.js';
|
||||
import { initApiService } from './service-manager.js';
|
||||
import { handleGeminiCliOAuth, handleGeminiAntigravityOAuth, handleQwenOAuth } from './oauth-handlers.js';
|
||||
import { handleGeminiCliOAuth, handleGeminiAntigravityOAuth, handleQwenOAuth, handleKiroOAuth } from './oauth-handlers.js';
|
||||
import {
|
||||
generateUUID,
|
||||
normalizePath,
|
||||
|
|
@ -1372,6 +1372,12 @@ export async function handleUIApiRequests(method, pathParam, req, res, currentCo
|
|||
const result = await handleQwenOAuth(currentConfig, options);
|
||||
authUrl = result.authUrl;
|
||||
authInfo = result.authInfo;
|
||||
} else if (providerType === 'claude-kiro-oauth') {
|
||||
// Kiro OAuth 支持多种认证方式
|
||||
// options.method 可以是: 'google' | 'github' | 'builder-id'
|
||||
const result = await handleKiroOAuth(currentConfig, options);
|
||||
authUrl = result.authUrl;
|
||||
authInfo = result.authInfo;
|
||||
} else {
|
||||
res.writeHead(400, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({
|
||||
|
|
|
|||
|
|
@ -322,7 +322,7 @@ async function openProviderManager(providerType) {
|
|||
*/
|
||||
function generateAuthButton(providerType) {
|
||||
// 只为支持OAuth的提供商显示授权按钮
|
||||
const oauthProviders = ['gemini-cli-oauth', 'gemini-antigravity', 'openai-qwen-oauth'];
|
||||
const oauthProviders = ['gemini-cli-oauth', 'gemini-antigravity', 'openai-qwen-oauth', 'claude-kiro-oauth'];
|
||||
|
||||
if (!oauthProviders.includes(providerType)) {
|
||||
return '';
|
||||
|
|
@ -341,6 +341,97 @@ function generateAuthButton(providerType) {
|
|||
* @param {string} providerType - 提供商类型
|
||||
*/
|
||||
async function handleGenerateAuthUrl(providerType) {
|
||||
// 如果是 Kiro OAuth,先显示认证方式选择对话框
|
||||
if (providerType === 'claude-kiro-oauth') {
|
||||
showKiroAuthMethodSelector(providerType);
|
||||
return;
|
||||
}
|
||||
|
||||
await executeGenerateAuthUrl(providerType, {});
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示 Kiro OAuth 认证方式选择对话框
|
||||
* @param {string} providerType - 提供商类型
|
||||
*/
|
||||
function showKiroAuthMethodSelector(providerType) {
|
||||
const modal = document.createElement('div');
|
||||
modal.className = 'modal-overlay';
|
||||
modal.style.display = 'flex';
|
||||
|
||||
modal.innerHTML = `
|
||||
<div class="modal-content" style="max-width: 500px;">
|
||||
<div class="modal-header">
|
||||
<h3><i class="fas fa-key"></i> 选择认证方式</h3>
|
||||
<button class="modal-close">×</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="auth-method-options" style="display: flex; flex-direction: column; gap: 12px;">
|
||||
<button class="auth-method-btn" data-method="google" style="display: flex; align-items: center; gap: 12px; padding: 16px; border: 2px solid #e0e0e0; border-radius: 8px; background: white; cursor: pointer; transition: all 0.2s;">
|
||||
<i class="fab fa-google" style="font-size: 24px; color: #4285f4;"></i>
|
||||
<div style="text-align: left;">
|
||||
<div style="font-weight: 600; color: #333;">Google 账号登录</div>
|
||||
<div style="font-size: 12px; color: #666;">使用 Google 账号进行社交登录</div>
|
||||
</div>
|
||||
</button>
|
||||
<button class="auth-method-btn" data-method="github" style="display: flex; align-items: center; gap: 12px; padding: 16px; border: 2px solid #e0e0e0; border-radius: 8px; background: white; cursor: pointer; transition: all 0.2s;">
|
||||
<i class="fab fa-github" style="font-size: 24px; color: #333;"></i>
|
||||
<div style="text-align: left;">
|
||||
<div style="font-weight: 600; color: #333;">GitHub 账号登录</div>
|
||||
<div style="font-size: 12px; color: #666;">使用 GitHub 账号进行社交登录</div>
|
||||
</div>
|
||||
</button>
|
||||
<button class="auth-method-btn" data-method="builder-id" style="display: flex; align-items: center; gap: 12px; padding: 16px; border: 2px solid #e0e0e0; border-radius: 8px; background: white; cursor: pointer; transition: all 0.2s;">
|
||||
<i class="fab fa-aws" style="font-size: 24px; color: #ff9900;"></i>
|
||||
<div style="text-align: left;">
|
||||
<div style="font-weight: 600; color: #333;">AWS Builder ID</div>
|
||||
<div style="font-size: 12px; color: #666;">使用 AWS Builder ID 进行设备码授权</div>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="modal-cancel">${t('modal.provider.cancel')}</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
document.body.appendChild(modal);
|
||||
|
||||
// 关闭按钮事件
|
||||
const closeBtn = modal.querySelector('.modal-close');
|
||||
const cancelBtn = modal.querySelector('.modal-cancel');
|
||||
[closeBtn, cancelBtn].forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
modal.remove();
|
||||
});
|
||||
});
|
||||
|
||||
// 认证方式选择按钮事件
|
||||
const methodBtns = modal.querySelectorAll('.auth-method-btn');
|
||||
methodBtns.forEach(btn => {
|
||||
btn.addEventListener('mouseenter', () => {
|
||||
btn.style.borderColor = '#00a67e';
|
||||
btn.style.background = '#f8fffe';
|
||||
});
|
||||
btn.addEventListener('mouseleave', () => {
|
||||
btn.style.borderColor = '#e0e0e0';
|
||||
btn.style.background = 'white';
|
||||
});
|
||||
btn.addEventListener('click', async () => {
|
||||
const method = btn.dataset.method;
|
||||
modal.remove();
|
||||
await executeGenerateAuthUrl(providerType, { method });
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行生成授权链接
|
||||
* @param {string} providerType - 提供商类型
|
||||
* @param {Object} extraOptions - 额外选项
|
||||
*/
|
||||
async function executeGenerateAuthUrl(providerType, extraOptions = {}) {
|
||||
try {
|
||||
showToast(t('common.info'), t('modal.provider.auth.initializing'), 'info');
|
||||
|
||||
|
|
@ -351,7 +442,8 @@ async function handleGenerateAuthUrl(providerType) {
|
|||
`/providers/${encodeURIComponent(providerType)}/generate-auth-url`,
|
||||
{
|
||||
saveToConfigs: true,
|
||||
providerDir: providerDir
|
||||
providerDir: providerDir,
|
||||
...extraOptions
|
||||
}
|
||||
);
|
||||
|
||||
|
|
@ -408,6 +500,20 @@ function showAuthModal(authUrl, authInfo) {
|
|||
</ol>
|
||||
</div>
|
||||
`;
|
||||
} else if (authInfo.provider === 'claude-kiro-oauth') {
|
||||
const methodDisplay = authInfo.authMethod === 'builder-id' ? 'AWS Builder ID' : `Social (${authInfo.socialProvider || 'Google'})`;
|
||||
instructionsHtml = `
|
||||
<div class="auth-instructions">
|
||||
<h4 data-i18n="oauth.modal.steps">${t('oauth.modal.steps')}</h4>
|
||||
<p><strong>认证方式:</strong> ${methodDisplay}</p>
|
||||
<ol>
|
||||
<li>点击下方按钮在浏览器中打开授权链接</li>
|
||||
<li>使用您的 ${authInfo.authMethod === 'builder-id' ? 'AWS Builder ID' : authInfo.socialProvider || 'Google'} 账号登录</li>
|
||||
<li>授权完成后页面会自动关闭</li>
|
||||
<li>刷新本页面查看凭据文件</li>
|
||||
</ol>
|
||||
</div>
|
||||
`;
|
||||
} else {
|
||||
instructionsHtml = `
|
||||
<div class="auth-instructions">
|
||||
|
|
|
|||
Loading…
Reference in a new issue