feat(kiro): 添加 AWS SSO 凭据导入和批量导入流式进度功能
1. 新增 checkKiroCredentialsDuplicate 函数用于检测重复凭据 2. 批量导入支持 SSE 流式响应实时显示进度 3. 新增 importAwsCredentials 函数支持 AWS Builder ID 模式导入 4. 前端新增 AWS 凭据导入模态框,支持文件上传和 JSON 粘贴两种模式 5. 添加中英文国际化翻译支持
This commit is contained in:
parent
c3b1adfb6e
commit
f6d456cdb4
4 changed files with 1196 additions and 60 deletions
|
|
@ -1548,13 +1548,72 @@ async function refreshKiroToken(refreshToken, region = KIRO_REFRESH_CONSTANTS.DE
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查 Kiro 凭据是否已存在(基于 refreshToken + provider 组合)
|
||||
* @param {string} refreshToken - 要检查的 refreshToken
|
||||
* @param {string} provider - 提供商名称 (默认: 'claude-kiro-oauth')
|
||||
* @returns {Promise<{isDuplicate: boolean, existingPath?: string}>} 检查结果
|
||||
*/
|
||||
export async function checkKiroCredentialsDuplicate(refreshToken, provider = 'claude-kiro-oauth') {
|
||||
const kiroDir = path.join(process.cwd(), 'configs', 'kiro');
|
||||
|
||||
try {
|
||||
// 检查 configs/kiro 目录是否存在
|
||||
if (!fs.existsSync(kiroDir)) {
|
||||
return { isDuplicate: false };
|
||||
}
|
||||
|
||||
// 递归扫描所有 JSON 文件
|
||||
const scanDirectory = async (dirPath) => {
|
||||
const entries = await fs.promises.readdir(dirPath, { withFileTypes: true });
|
||||
|
||||
for (const entry of entries) {
|
||||
const fullPath = path.join(dirPath, entry.name);
|
||||
|
||||
if (entry.isDirectory()) {
|
||||
const result = await scanDirectory(fullPath);
|
||||
if (result.isDuplicate) {
|
||||
return result;
|
||||
}
|
||||
} else if (entry.isFile() && entry.name.endsWith('.json')) {
|
||||
try {
|
||||
const content = await fs.promises.readFile(fullPath, 'utf8');
|
||||
const credentials = JSON.parse(content);
|
||||
|
||||
// 检查 refreshToken 是否匹配
|
||||
if (credentials.refreshToken && credentials.refreshToken === refreshToken) {
|
||||
const relativePath = path.relative(process.cwd(), fullPath);
|
||||
console.log(`${KIRO_OAUTH_CONFIG.logPrefix} Found duplicate refreshToken in: ${relativePath}`);
|
||||
return {
|
||||
isDuplicate: true,
|
||||
existingPath: relativePath
|
||||
};
|
||||
}
|
||||
} catch (parseError) {
|
||||
// 忽略解析错误的文件
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { isDuplicate: false };
|
||||
};
|
||||
|
||||
return await scanDirectory(kiroDir);
|
||||
|
||||
} catch (error) {
|
||||
console.warn(`${KIRO_OAUTH_CONFIG.logPrefix} Error checking duplicates:`, error.message);
|
||||
return { isDuplicate: false };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量导入 Kiro refreshToken 并生成凭据文件
|
||||
* @param {string[]} refreshTokens - refreshToken 数组
|
||||
* @param {string} region - AWS 区域 (默认: us-east-1)
|
||||
* @param {boolean} skipDuplicateCheck - 是否跳过重复检查 (默认: false)
|
||||
* @returns {Promise<Object>} 批量处理结果
|
||||
*/
|
||||
export async function batchImportKiroRefreshTokens(refreshTokens, region = KIRO_REFRESH_CONSTANTS.DEFAULT_REGION) {
|
||||
export async function batchImportKiroRefreshTokens(refreshTokens, region = KIRO_REFRESH_CONSTANTS.DEFAULT_REGION, skipDuplicateCheck = false) {
|
||||
const results = {
|
||||
total: refreshTokens.length,
|
||||
success: 0,
|
||||
|
|
@ -1575,6 +1634,21 @@ export async function batchImportKiroRefreshTokens(refreshTokens, region = KIRO_
|
|||
continue;
|
||||
}
|
||||
|
||||
// 检查重复
|
||||
if (!skipDuplicateCheck) {
|
||||
const duplicateCheck = await checkKiroCredentialsDuplicate(refreshToken);
|
||||
if (duplicateCheck.isDuplicate) {
|
||||
results.details.push({
|
||||
index: i + 1,
|
||||
success: false,
|
||||
error: 'duplicate',
|
||||
existingPath: duplicateCheck.existingPath
|
||||
});
|
||||
results.failed++;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
console.log(`${KIRO_OAUTH_CONFIG.logPrefix} 正在刷新第 ${i + 1}/${refreshTokens.length} 个 token...`);
|
||||
|
||||
|
|
@ -1626,4 +1700,268 @@ export async function batchImportKiroRefreshTokens(refreshTokens, region = KIRO_
|
|||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量导入 Kiro refreshToken 并生成凭据文件(流式版本,支持实时进度回调)
|
||||
* @param {string[]} refreshTokens - refreshToken 数组
|
||||
* @param {string} region - AWS 区域 (默认: us-east-1)
|
||||
* @param {Function} onProgress - 进度回调函数,每处理完一个 token 调用
|
||||
* @param {boolean} skipDuplicateCheck - 是否跳过重复检查 (默认: false)
|
||||
* @returns {Promise<Object>} 批量处理结果
|
||||
*/
|
||||
export async function batchImportKiroRefreshTokensStream(refreshTokens, region = KIRO_REFRESH_CONSTANTS.DEFAULT_REGION, onProgress = null, skipDuplicateCheck = false) {
|
||||
const results = {
|
||||
total: refreshTokens.length,
|
||||
success: 0,
|
||||
failed: 0,
|
||||
details: []
|
||||
};
|
||||
|
||||
for (let i = 0; i < refreshTokens.length; i++) {
|
||||
const refreshToken = refreshTokens[i].trim();
|
||||
const progressData = {
|
||||
index: i + 1,
|
||||
total: refreshTokens.length,
|
||||
current: null
|
||||
};
|
||||
|
||||
if (!refreshToken) {
|
||||
progressData.current = {
|
||||
index: i + 1,
|
||||
success: false,
|
||||
error: 'Empty token'
|
||||
};
|
||||
results.details.push(progressData.current);
|
||||
results.failed++;
|
||||
|
||||
// 发送进度更新
|
||||
if (onProgress) {
|
||||
onProgress({
|
||||
...progressData,
|
||||
successCount: results.success,
|
||||
failedCount: results.failed
|
||||
});
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// 检查重复
|
||||
if (!skipDuplicateCheck) {
|
||||
const duplicateCheck = await checkKiroCredentialsDuplicate(refreshToken);
|
||||
if (duplicateCheck.isDuplicate) {
|
||||
progressData.current = {
|
||||
index: i + 1,
|
||||
success: false,
|
||||
error: 'duplicate',
|
||||
existingPath: duplicateCheck.existingPath
|
||||
};
|
||||
results.details.push(progressData.current);
|
||||
results.failed++;
|
||||
|
||||
// 发送进度更新
|
||||
if (onProgress) {
|
||||
onProgress({
|
||||
...progressData,
|
||||
successCount: results.success,
|
||||
failedCount: results.failed
|
||||
});
|
||||
}
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
console.log(`${KIRO_OAUTH_CONFIG.logPrefix} 正在刷新第 ${i + 1}/${refreshTokens.length} 个 token...`);
|
||||
|
||||
const tokenData = await refreshKiroToken(refreshToken, region);
|
||||
|
||||
// 生成文件路径: configs/kiro/{timestamp}_kiro-auth-token/{timestamp}_kiro-auth-token.json
|
||||
const timestamp = Date.now();
|
||||
const folderName = `${timestamp}_kiro-auth-token`;
|
||||
const targetDir = path.join(process.cwd(), 'configs', 'kiro', folderName);
|
||||
await fs.promises.mkdir(targetDir, { recursive: true });
|
||||
|
||||
const credPath = path.join(targetDir, `${folderName}.json`);
|
||||
await fs.promises.writeFile(credPath, JSON.stringify(tokenData, null, 2));
|
||||
|
||||
const relativePath = path.relative(process.cwd(), credPath);
|
||||
|
||||
console.log(`${KIRO_OAUTH_CONFIG.logPrefix} Token ${i + 1} 已保存: ${relativePath}`);
|
||||
|
||||
progressData.current = {
|
||||
index: i + 1,
|
||||
success: true,
|
||||
path: relativePath,
|
||||
expiresAt: tokenData.expiresAt
|
||||
};
|
||||
results.details.push(progressData.current);
|
||||
results.success++;
|
||||
|
||||
} catch (error) {
|
||||
console.error(`${KIRO_OAUTH_CONFIG.logPrefix} Token ${i + 1} 刷新失败:`, error.message);
|
||||
|
||||
progressData.current = {
|
||||
index: i + 1,
|
||||
success: false,
|
||||
error: error.message
|
||||
};
|
||||
results.details.push(progressData.current);
|
||||
results.failed++;
|
||||
}
|
||||
|
||||
// 发送进度更新
|
||||
if (onProgress) {
|
||||
onProgress({
|
||||
...progressData,
|
||||
successCount: results.success,
|
||||
failedCount: results.failed
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 如果有成功的,广播事件并自动关联
|
||||
if (results.success > 0) {
|
||||
broadcastEvent('oauth_batch_success', {
|
||||
provider: 'claude-kiro-oauth',
|
||||
count: results.success,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
|
||||
// 自动关联新生成的凭据到 Pools
|
||||
await autoLinkProviderConfigs(CONFIG);
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
/**
|
||||
* 导入 AWS SSO 凭据用于 Kiro (Builder ID 模式)
|
||||
* 从用户上传的 AWS SSO cache 文件中导入凭据
|
||||
* @param {Object} credentials - 合并后的凭据对象,需包含 clientId 和 clientSecret
|
||||
* @param {boolean} skipDuplicateCheck - 是否跳过重复检查 (默认: false)
|
||||
* @returns {Promise<Object>} 导入结果
|
||||
*/
|
||||
export async function importAwsCredentials(credentials, skipDuplicateCheck = false) {
|
||||
try {
|
||||
// 验证必需字段 - 需要四个字段都存在
|
||||
const missingFields = [];
|
||||
if (!credentials.clientId) missingFields.push('clientId');
|
||||
if (!credentials.clientSecret) missingFields.push('clientSecret');
|
||||
if (!credentials.accessToken) missingFields.push('accessToken');
|
||||
if (!credentials.refreshToken) missingFields.push('refreshToken');
|
||||
|
||||
if (missingFields.length > 0) {
|
||||
return {
|
||||
success: false,
|
||||
error: `Missing required fields: ${missingFields.join(', ')}`
|
||||
};
|
||||
}
|
||||
|
||||
// 检查重复凭据
|
||||
if (!skipDuplicateCheck) {
|
||||
const duplicateCheck = await checkKiroCredentialsDuplicate(credentials.refreshToken);
|
||||
if (duplicateCheck.isDuplicate) {
|
||||
return {
|
||||
success: false,
|
||||
error: 'duplicate',
|
||||
existingPath: duplicateCheck.existingPath
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`${KIRO_OAUTH_CONFIG.logPrefix} Importing AWS credentials...`);
|
||||
|
||||
// 准备凭据数据 - 四个字段都是必需的
|
||||
const credentialsData = {
|
||||
clientId: credentials.clientId,
|
||||
clientSecret: credentials.clientSecret,
|
||||
accessToken: credentials.accessToken,
|
||||
refreshToken: credentials.refreshToken,
|
||||
authMethod: credentials.authMethod || 'builder-id',
|
||||
region: credentials.region || KIRO_REFRESH_CONSTANTS.DEFAULT_REGION
|
||||
};
|
||||
|
||||
// 可选字段
|
||||
if (credentials.expiresAt) {
|
||||
credentialsData.expiresAt = credentials.expiresAt;
|
||||
}
|
||||
if (credentials.startUrl) {
|
||||
credentialsData.startUrl = credentials.startUrl;
|
||||
}
|
||||
if (credentials.registrationExpiresAt) {
|
||||
credentialsData.registrationExpiresAt = credentials.registrationExpiresAt;
|
||||
}
|
||||
|
||||
// 尝试刷新获取最新的 accessToken
|
||||
try {
|
||||
console.log(`${KIRO_OAUTH_CONFIG.logPrefix} Attempting to refresh token with provided credentials...`);
|
||||
|
||||
const region = credentials.region || KIRO_REFRESH_CONSTANTS.DEFAULT_REGION;
|
||||
const refreshUrl = KIRO_REFRESH_CONSTANTS.REFRESH_IDC_URL.replace('{{region}}', region);
|
||||
|
||||
const refreshResponse = await fetch(refreshUrl, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
refreshToken: credentials.refreshToken,
|
||||
clientId: credentials.clientId,
|
||||
clientSecret: credentials.clientSecret,
|
||||
grantType: 'refresh_token'
|
||||
})
|
||||
});
|
||||
|
||||
if (refreshResponse.ok) {
|
||||
const tokenData = await refreshResponse.json();
|
||||
credentialsData.accessToken = tokenData.accessToken;
|
||||
credentialsData.refreshToken = tokenData.refreshToken;
|
||||
const expiresIn = tokenData.expiresIn || 3600;
|
||||
credentialsData.expiresAt = new Date(Date.now() + expiresIn * 1000).toISOString();
|
||||
console.log(`${KIRO_OAUTH_CONFIG.logPrefix} Token refreshed successfully`);
|
||||
} else {
|
||||
console.warn(`${KIRO_OAUTH_CONFIG.logPrefix} Token refresh failed, saving original credentials`);
|
||||
}
|
||||
} catch (refreshError) {
|
||||
console.warn(`${KIRO_OAUTH_CONFIG.logPrefix} Token refresh error:`, refreshError.message);
|
||||
// 继续保存原始凭据
|
||||
}
|
||||
|
||||
// 生成文件路径: configs/kiro/{timestamp}_kiro-auth-token/{timestamp}_kiro-auth-token.json
|
||||
const timestamp = Date.now();
|
||||
const folderName = `${timestamp}_kiro-auth-token`;
|
||||
const targetDir = path.join(process.cwd(), 'configs', 'kiro', folderName);
|
||||
await fs.promises.mkdir(targetDir, { recursive: true });
|
||||
|
||||
const credPath = path.join(targetDir, `${folderName}.json`);
|
||||
await fs.promises.writeFile(credPath, JSON.stringify(credentialsData, null, 2));
|
||||
|
||||
const relativePath = path.relative(process.cwd(), credPath);
|
||||
|
||||
console.log(`${KIRO_OAUTH_CONFIG.logPrefix} AWS credentials saved to: ${relativePath}`);
|
||||
|
||||
// 广播事件
|
||||
broadcastEvent('oauth_success', {
|
||||
provider: 'claude-kiro-oauth',
|
||||
relativePath: relativePath,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
|
||||
// 自动关联新生成的凭据到 Pools
|
||||
await autoLinkProviderConfigs(CONFIG);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
path: relativePath
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
console.error(`${KIRO_OAUTH_CONFIG.logPrefix} AWS credentials import failed:`, error);
|
||||
return {
|
||||
success: false,
|
||||
error: error.message
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -56,7 +56,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, handleKiroOAuth, handleIFlowOAuth, batchImportKiroRefreshTokens } from './oauth-handlers.js';
|
||||
import { handleGeminiCliOAuth, handleGeminiAntigravityOAuth, handleQwenOAuth, handleKiroOAuth, handleIFlowOAuth, batchImportKiroRefreshTokens, batchImportKiroRefreshTokensStream, importAwsCredentials } from './oauth-handlers.js';
|
||||
import {
|
||||
generateUUID,
|
||||
normalizePath,
|
||||
|
|
@ -2126,8 +2126,8 @@ export async function handleUIApiRequests(method, pathParam, req, res, currentCo
|
|||
return true;
|
||||
}
|
||||
|
||||
// Batch import Kiro refresh tokens
|
||||
// 批量导入 Kiro refreshToken
|
||||
// Batch import Kiro refresh tokens with SSE (real-time progress)
|
||||
// 批量导入 Kiro refreshToken(带实时进度 SSE)
|
||||
if (method === 'POST' && pathParam === '/api/kiro/batch-import-tokens') {
|
||||
try {
|
||||
const body = await getRequestBody(req);
|
||||
|
|
@ -2142,21 +2142,125 @@ export async function handleUIApiRequests(method, pathParam, req, res, currentCo
|
|||
return true;
|
||||
}
|
||||
|
||||
console.log(`[Kiro Batch Import] Starting batch import of ${refreshTokens.length} tokens...`);
|
||||
console.log(`[Kiro Batch Import] Starting batch import of ${refreshTokens.length} tokens with SSE...`);
|
||||
|
||||
const result = await batchImportKiroRefreshTokens(refreshTokens, region || 'us-east-1');
|
||||
// 设置 SSE 响应头
|
||||
res.writeHead(200, {
|
||||
'Content-Type': 'text/event-stream',
|
||||
'Cache-Control': 'no-cache',
|
||||
'Connection': 'keep-alive',
|
||||
'X-Accel-Buffering': 'no'
|
||||
});
|
||||
|
||||
// 发送 SSE 事件的辅助函数
|
||||
const sendSSE = (event, data) => {
|
||||
res.write(`event: ${event}\n`);
|
||||
res.write(`data: ${JSON.stringify(data)}\n\n`);
|
||||
};
|
||||
|
||||
// 发送开始事件
|
||||
sendSSE('start', { total: refreshTokens.length });
|
||||
|
||||
// 执行流式批量导入
|
||||
const result = await batchImportKiroRefreshTokensStream(
|
||||
refreshTokens,
|
||||
region || 'us-east-1',
|
||||
(progress) => {
|
||||
// 每处理完一个 token 发送进度更新
|
||||
sendSSE('progress', progress);
|
||||
}
|
||||
);
|
||||
|
||||
console.log(`[Kiro Batch Import] Completed: ${result.success} success, ${result.failed} failed`);
|
||||
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({
|
||||
// 发送完成事件
|
||||
sendSSE('complete', {
|
||||
success: true,
|
||||
...result
|
||||
}));
|
||||
total: result.total,
|
||||
successCount: result.success,
|
||||
failedCount: result.failed,
|
||||
details: result.details
|
||||
});
|
||||
|
||||
res.end();
|
||||
return true;
|
||||
|
||||
} catch (error) {
|
||||
console.error('[Kiro Batch Import] Error:', error);
|
||||
// 如果已经开始发送 SSE,则发送错误事件
|
||||
if (res.headersSent) {
|
||||
res.write(`event: error\n`);
|
||||
res.write(`data: ${JSON.stringify({ error: error.message })}\n\n`);
|
||||
res.end();
|
||||
} else {
|
||||
res.writeHead(500, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({
|
||||
success: false,
|
||||
error: error.message
|
||||
}));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Import AWS SSO credentials for Kiro
|
||||
// 导入 AWS SSO 凭据用于 Kiro
|
||||
if (method === 'POST' && pathParam === '/api/kiro/import-aws-credentials') {
|
||||
try {
|
||||
const body = await getRequestBody(req);
|
||||
const { credentials } = body;
|
||||
|
||||
if (!credentials || typeof credentials !== 'object') {
|
||||
res.writeHead(400, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({
|
||||
success: false,
|
||||
error: 'credentials object is required'
|
||||
}));
|
||||
return true;
|
||||
}
|
||||
|
||||
// 验证必需字段 - 需要四个字段都存在
|
||||
const missingFields = [];
|
||||
if (!credentials.clientId) missingFields.push('clientId');
|
||||
if (!credentials.clientSecret) missingFields.push('clientSecret');
|
||||
if (!credentials.accessToken) missingFields.push('accessToken');
|
||||
if (!credentials.refreshToken) missingFields.push('refreshToken');
|
||||
|
||||
if (missingFields.length > 0) {
|
||||
res.writeHead(400, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({
|
||||
success: false,
|
||||
error: `Missing required fields: ${missingFields.join(', ')}`
|
||||
}));
|
||||
return true;
|
||||
}
|
||||
|
||||
console.log('[Kiro AWS Import] Starting AWS credentials import...');
|
||||
|
||||
const result = await importAwsCredentials(credentials);
|
||||
|
||||
if (result.success) {
|
||||
console.log(`[Kiro AWS Import] Successfully imported credentials to: ${result.path}`);
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({
|
||||
success: true,
|
||||
path: result.path,
|
||||
message: 'AWS credentials imported successfully'
|
||||
}));
|
||||
} else {
|
||||
// 重复凭据返回 409 Conflict,其他错误返回 500
|
||||
const statusCode = result.error === 'duplicate' ? 409 : 500;
|
||||
res.writeHead(statusCode, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({
|
||||
success: false,
|
||||
error: result.error,
|
||||
existingPath: result.existingPath || null
|
||||
}));
|
||||
}
|
||||
return true;
|
||||
|
||||
} catch (error) {
|
||||
console.error('[Kiro AWS Import] Error:', error);
|
||||
res.writeHead(500, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({
|
||||
success: false,
|
||||
|
|
|
|||
|
|
@ -133,18 +133,49 @@ const translations = {
|
|||
'oauth.kiro.step3': '授权完成后页面会自动关闭',
|
||||
'oauth.kiro.step4': '刷新本页面查看凭据文件',
|
||||
'oauth.kiro.batchImport': '批量导入 refreshToken',
|
||||
'oauth.kiro.batchImportDesc': '批量导入已有的 refreshToken 生成凭据文件',
|
||||
'oauth.kiro.batchImportDesc': '批量导入已有的 refreshToken 生成凭据文件,该模式不支持 AWS 账号。',
|
||||
'oauth.kiro.batchImportInstructions': '请输入 refreshToken,每行一个。系统将自动刷新并生成凭据文件。',
|
||||
'oauth.kiro.awsImport': '导入 AWS 账号',
|
||||
'oauth.kiro.awsImportDesc': '从 AWS SSO cache 目录导入凭据文件,适用于 AWS Builder ID 模式。',
|
||||
'oauth.kiro.awsImportInstructions': '请上传 AWS SSO cache 目录中的 JSON 文件,需包含 clientId、clientSecret、accessToken、refreshToken 四个字段。',
|
||||
'oauth.kiro.awsModeFile': '文件上传',
|
||||
'oauth.kiro.awsModeJson': 'JSON 粘贴',
|
||||
'oauth.kiro.awsUploadFiles': '上传凭据文件',
|
||||
'oauth.kiro.awsDragDrop': '拖拽文件到此处',
|
||||
'oauth.kiro.awsClickUpload': '或点击选择文件',
|
||||
'oauth.kiro.awsFileHint': '如果一个文件不包含全部字段,可以多次上传不同的文件进行补全',
|
||||
'oauth.kiro.awsSelectedFiles': '已选择的文件',
|
||||
'oauth.kiro.awsClearFiles': '清空全部',
|
||||
'oauth.kiro.awsFileReplaced': '已替换同名文件: {filename}',
|
||||
'oauth.kiro.awsJsonInput': '粘贴 JSON 凭据',
|
||||
'oauth.kiro.awsJsonPlaceholderSimple': '在此粘贴包含 clientId、clientSecret、accessToken、refreshToken 的 JSON...',
|
||||
'oauth.kiro.awsJsonExample': '查看 JSON 格式示例',
|
||||
'oauth.kiro.awsJsonHint': '可以直接粘贴合并后的 JSON,或从 AWS SSO cache 文件复制内容',
|
||||
'oauth.kiro.awsJsonParseError': 'JSON 格式错误',
|
||||
'oauth.kiro.awsParseError': '解析文件 {filename} 失败',
|
||||
'oauth.kiro.awsValidationSuccess': '验证通过!已找到全部必需字段',
|
||||
'oauth.kiro.awsValidationFailed': '验证失败!缺少必需字段',
|
||||
'oauth.kiro.awsMissingFields': '缺少 {count} 个字段',
|
||||
'oauth.kiro.awsUploadMore': '请上传包含缺失字段的文件,或切换到 JSON 模式手动补全',
|
||||
'oauth.kiro.awsPreviewJson': '合并后的凭据预览',
|
||||
'oauth.kiro.awsConfirmImport': '确认导入',
|
||||
'oauth.kiro.awsNoCredentials': '没有可导入的凭据',
|
||||
'oauth.kiro.awsImporting': '正在导入...',
|
||||
'oauth.kiro.awsImportSuccess': 'AWS 凭据导入成功!',
|
||||
'oauth.kiro.awsImportFailed': 'AWS 凭据导入失败',
|
||||
'oauth.kiro.refreshTokensLabel': 'RefreshToken 列表',
|
||||
'oauth.kiro.refreshTokensPlaceholder': '每行输入一个 refreshToken\n例如:\naorAxxxxxxxx\naorAyyyyyyyy\naorAzzzzzzzz',
|
||||
'oauth.kiro.tokenCount': '待导入数量:',
|
||||
'oauth.kiro.importing': '正在导入中,请稍候...',
|
||||
'oauth.kiro.importingProgress': '正在导入 {current}/{total}...',
|
||||
'oauth.kiro.startImport': '开始导入',
|
||||
'oauth.kiro.noTokens': '请输入至少一个 refreshToken',
|
||||
'oauth.kiro.importSuccess': '导入成功!共 {count} 个凭据已生成',
|
||||
'oauth.kiro.importAllFailed': '导入失败!共 {count} 个 token 刷新失败',
|
||||
'oauth.kiro.importPartial': '部分成功:{success} 个成功,{failed} 个失败',
|
||||
'oauth.kiro.importError': '导入出错',
|
||||
'oauth.kiro.duplicateToken': '重复凭据 - 此 refreshToken 已存在',
|
||||
'oauth.kiro.duplicateCredentials': '该凭据已存在,请勿重复导入',
|
||||
'oauth.iflow.step1': '点击下方按钮在浏览器中打开 iFlow 授权页面',
|
||||
'oauth.iflow.step2': '使用您的 iFlow 账号登录并授权',
|
||||
'oauth.iflow.step3': '授权完成后,系统会自动获取 API Key',
|
||||
|
|
@ -415,6 +446,8 @@ const translations = {
|
|||
'common.loading': '加载中...',
|
||||
'common.upload': '上传',
|
||||
'common.generate': '生成',
|
||||
'common.found': '已找到',
|
||||
'common.missing': '缺失',
|
||||
'common.search': '搜索',
|
||||
'common.welcome': '欢迎使用AIClient2API管理控制台!',
|
||||
'common.fileType': '不支持的文件类型,请选择 JSON、TXT、KEY、PEM、P12 或 PFX 文件',
|
||||
|
|
@ -574,18 +607,49 @@ const translations = {
|
|||
'oauth.kiro.step3': 'The page will close automatically after authorization',
|
||||
'oauth.kiro.step4': 'Refresh this page to view the credentials file',
|
||||
'oauth.kiro.batchImport': 'Batch Import refreshToken',
|
||||
'oauth.kiro.batchImportDesc': 'Batch import existing refreshTokens to generate credential files',
|
||||
'oauth.kiro.batchImportDesc': 'Batch import existing refresh tokens to generate credential files. This mode does not support AWS accounts.',
|
||||
'oauth.kiro.batchImportInstructions': 'Enter refreshTokens, one per line. The system will automatically refresh and generate credential files.',
|
||||
'oauth.kiro.awsImport': 'Import AWS Account',
|
||||
'oauth.kiro.awsImportDesc': 'Import credential files from AWS SSO cache directory. For AWS Builder ID mode.',
|
||||
'oauth.kiro.awsImportInstructions': 'Upload JSON files from AWS SSO cache directory. Must contain clientId, clientSecret, accessToken, and refreshToken.',
|
||||
'oauth.kiro.awsModeFile': 'File Upload',
|
||||
'oauth.kiro.awsModeJson': 'Paste JSON',
|
||||
'oauth.kiro.awsUploadFiles': 'Upload Credential Files',
|
||||
'oauth.kiro.awsDragDrop': 'Drag and drop files here',
|
||||
'oauth.kiro.awsClickUpload': 'or click to select files',
|
||||
'oauth.kiro.awsFileHint': 'If one file doesn\'t contain all fields, you can upload multiple files to complete them',
|
||||
'oauth.kiro.awsSelectedFiles': 'Selected Files',
|
||||
'oauth.kiro.awsClearFiles': 'Clear All',
|
||||
'oauth.kiro.awsFileReplaced': 'Replaced file: {filename}',
|
||||
'oauth.kiro.awsJsonInput': 'Paste JSON Credentials',
|
||||
'oauth.kiro.awsJsonPlaceholderSimple': 'Paste JSON containing clientId, clientSecret, accessToken, refreshToken here...',
|
||||
'oauth.kiro.awsJsonExample': 'View JSON format example',
|
||||
'oauth.kiro.awsJsonHint': 'You can paste merged JSON directly, or copy content from AWS SSO cache files',
|
||||
'oauth.kiro.awsJsonParseError': 'Invalid JSON format',
|
||||
'oauth.kiro.awsParseError': 'Failed to parse file {filename}',
|
||||
'oauth.kiro.awsValidationSuccess': 'Validation passed! All required fields found',
|
||||
'oauth.kiro.awsValidationFailed': 'Validation failed! Required fields missing',
|
||||
'oauth.kiro.awsMissingFields': '{count} field(s) missing',
|
||||
'oauth.kiro.awsUploadMore': 'Please upload files containing the missing fields, or switch to JSON mode to complete manually',
|
||||
'oauth.kiro.awsPreviewJson': 'Merged Credentials Preview',
|
||||
'oauth.kiro.awsConfirmImport': 'Confirm Import',
|
||||
'oauth.kiro.awsNoCredentials': 'No credentials to import',
|
||||
'oauth.kiro.awsImporting': 'Importing...',
|
||||
'oauth.kiro.awsImportSuccess': 'AWS credentials imported successfully!',
|
||||
'oauth.kiro.awsImportFailed': 'AWS credentials import failed',
|
||||
'oauth.kiro.refreshTokensLabel': 'RefreshToken List',
|
||||
'oauth.kiro.refreshTokensPlaceholder': 'Enter one refreshToken per line\nExample:\naorAxxxxxxxx\naorAyyyyyyyy\naorAzzzzzzzz',
|
||||
'oauth.kiro.tokenCount': 'Tokens to import:',
|
||||
'oauth.kiro.importing': 'Importing, please wait...',
|
||||
'oauth.kiro.importingProgress': 'Importing {current}/{total}...',
|
||||
'oauth.kiro.startImport': 'Start Import',
|
||||
'oauth.kiro.noTokens': 'Please enter at least one refreshToken',
|
||||
'oauth.kiro.importSuccess': 'Import successful! {count} credentials generated',
|
||||
'oauth.kiro.importAllFailed': 'Import failed! {count} tokens failed to refresh',
|
||||
'oauth.kiro.importPartial': 'Partial success: {success} succeeded, {failed} failed',
|
||||
'oauth.kiro.importError': 'Import error',
|
||||
'oauth.kiro.duplicateToken': 'Duplicate - this refreshToken already exists',
|
||||
'oauth.kiro.duplicateCredentials': 'This credential already exists, please do not import duplicates',
|
||||
'oauth.iflow.step1': 'Click the button below to open the iFlow authorization page',
|
||||
'oauth.iflow.step2': 'Log in with your iFlow account and authorize',
|
||||
'oauth.iflow.step3': 'After authorization, the system will automatically fetch the API Key',
|
||||
|
|
@ -857,6 +921,8 @@ const translations = {
|
|||
'common.loading': 'Loading...',
|
||||
'common.upload': 'Upload',
|
||||
'common.generate': 'Generate',
|
||||
'common.found': 'Found',
|
||||
'common.missing': 'Missing',
|
||||
'common.search': 'Search',
|
||||
'common.welcome': 'Welcome to AIClient2API Management Console!',
|
||||
'common.fileType': 'Unsupported file type. Please select JSON, TXT, KEY, PEM, P12, or PFX.',
|
||||
|
|
|
|||
|
|
@ -496,6 +496,13 @@ function showKiroAuthMethodSelector(providerType) {
|
|||
<div style="font-size: 12px; color: #666;" data-i18n="oauth.kiro.batchImportDesc">${t('oauth.kiro.batchImportDesc')}</div>
|
||||
</div>
|
||||
</button>
|
||||
<button class="auth-method-btn" data-method="aws-import" 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="fas fa-cloud-upload-alt" style="font-size: 24px; color: #ff9900;"></i>
|
||||
<div style="text-align: left;">
|
||||
<div style="font-weight: 600; color: #333;" data-i18n="oauth.kiro.awsImport">${t('oauth.kiro.awsImport')}</div>
|
||||
<div style="font-size: 12px; color: #666;" data-i18n="oauth.kiro.awsImportDesc">${t('oauth.kiro.awsImportDesc')}</div>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
|
|
@ -532,6 +539,8 @@ function showKiroAuthMethodSelector(providerType) {
|
|||
|
||||
if (method === 'batch-import') {
|
||||
showKiroBatchImportModal();
|
||||
} else if (method === 'aws-import') {
|
||||
showKiroAwsImportModal();
|
||||
} else {
|
||||
await executeGenerateAuthUrl(providerType, { method });
|
||||
}
|
||||
|
|
@ -629,7 +638,7 @@ function showKiroBatchImportModal() {
|
|||
});
|
||||
});
|
||||
|
||||
// 提交按钮事件
|
||||
// 提交按钮事件 - 使用 SSE 流式响应实时显示进度
|
||||
submitBtn.addEventListener('click', async () => {
|
||||
const tokens = textarea.value.split('\n').filter(line => line.trim());
|
||||
|
||||
|
|
@ -644,63 +653,153 @@ function showKiroBatchImportModal() {
|
|||
cancelBtn.disabled = true;
|
||||
progressDiv.style.display = 'block';
|
||||
resultDiv.style.display = 'none';
|
||||
progressBar.style.width = '0%';
|
||||
|
||||
// 创建实时结果显示区域
|
||||
resultDiv.style.cssText = 'display: block; margin-top: 16px; padding: 12px; border-radius: 8px; background: #f3f4f6; border: 1px solid #d1d5db;';
|
||||
resultDiv.innerHTML = `
|
||||
<div style="display: flex; align-items: center; gap: 8px; margin-bottom: 8px;">
|
||||
<i class="fas fa-spinner fa-spin" style="color: #10b981;"></i>
|
||||
<strong id="batchProgressText">${t('oauth.kiro.importingProgress', { current: 0, total: tokens.length })}</strong>
|
||||
</div>
|
||||
<div id="batchResultsList" style="max-height: 200px; overflow-y: auto; font-size: 12px; margin-top: 8px;"></div>
|
||||
`;
|
||||
|
||||
const progressText = resultDiv.querySelector('#batchProgressText');
|
||||
const resultsList = resultDiv.querySelector('#batchResultsList');
|
||||
|
||||
let successCount = 0;
|
||||
let failedCount = 0;
|
||||
const details = [];
|
||||
|
||||
try {
|
||||
const response = await window.apiClient.post('/kiro/batch-import-tokens', {
|
||||
refreshTokens: tokens
|
||||
// 使用 fetch + SSE 获取流式响应
|
||||
const response = await fetch('/api/kiro/batch-import-tokens', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ refreshTokens: tokens })
|
||||
});
|
||||
|
||||
progressBar.style.width = '100%';
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
if (response.success) {
|
||||
// 显示结果
|
||||
const isAllSuccess = response.failed === 0;
|
||||
const isAllFailed = response.success === 0;
|
||||
const reader = response.body.getReader();
|
||||
const decoder = new TextDecoder();
|
||||
let buffer = '';
|
||||
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) break;
|
||||
|
||||
let resultClass, resultIcon, resultMessage;
|
||||
if (isAllSuccess) {
|
||||
resultClass = 'background: #f0fdf4; border: 1px solid #bbf7d0; color: #166534;';
|
||||
resultIcon = 'fa-check-circle';
|
||||
resultMessage = t('oauth.kiro.importSuccess', { count: response.success });
|
||||
} else if (isAllFailed) {
|
||||
resultClass = 'background: #fef2f2; border: 1px solid #fecaca; color: #991b1b;';
|
||||
resultIcon = 'fa-times-circle';
|
||||
resultMessage = t('oauth.kiro.importAllFailed', { count: response.failed });
|
||||
} else {
|
||||
resultClass = 'background: #fffbeb; border: 1px solid #fde68a; color: #92400e;';
|
||||
resultIcon = 'fa-exclamation-triangle';
|
||||
resultMessage = t('oauth.kiro.importPartial', { success: response.success, failed: response.failed });
|
||||
}
|
||||
buffer += decoder.decode(value, { stream: true });
|
||||
|
||||
resultDiv.style.cssText = `display: block; margin-top: 16px; padding: 12px; border-radius: 8px; ${resultClass}`;
|
||||
resultDiv.innerHTML = `
|
||||
<div style="display: flex; align-items: center; gap: 8px; margin-bottom: 8px;">
|
||||
<i class="fas ${resultIcon}"></i>
|
||||
<strong>${resultMessage}</strong>
|
||||
</div>
|
||||
${response.details && response.details.length > 0 ? `
|
||||
<div style="max-height: 150px; overflow-y: auto; font-size: 12px; margin-top: 8px;">
|
||||
${response.details.map(d => `
|
||||
<div style="padding: 4px 0; border-bottom: 1px solid rgba(0,0,0,0.1);">
|
||||
Token ${d.index}: ${d.success
|
||||
? `<span style="color: #166534;">✓ ${d.path}</span>`
|
||||
: `<span style="color: #991b1b;">✗ ${d.error}</span>`}
|
||||
</div>
|
||||
`).join('')}
|
||||
</div>
|
||||
` : ''}
|
||||
`;
|
||||
// 解析 SSE 事件
|
||||
const lines = buffer.split('\n');
|
||||
buffer = lines.pop() || ''; // 保留最后一个可能不完整的行
|
||||
|
||||
progressDiv.style.display = 'none';
|
||||
let eventType = '';
|
||||
let eventData = '';
|
||||
|
||||
// 如果有成功的,刷新提供商列表
|
||||
if (response.success > 0) {
|
||||
loadProviders();
|
||||
loadConfigList();
|
||||
for (const line of lines) {
|
||||
if (line.startsWith('event: ')) {
|
||||
eventType = line.substring(7).trim();
|
||||
} else if (line.startsWith('data: ')) {
|
||||
eventData = line.substring(6).trim();
|
||||
|
||||
if (eventType && eventData) {
|
||||
try {
|
||||
const data = JSON.parse(eventData);
|
||||
|
||||
if (eventType === 'start') {
|
||||
// 开始事件
|
||||
console.log(`[Batch Import] Starting import of ${data.total} tokens`);
|
||||
} else if (eventType === 'progress') {
|
||||
// 进度更新
|
||||
const { index, total, current, successCount: sc, failedCount: fc } = data;
|
||||
successCount = sc;
|
||||
failedCount = fc;
|
||||
details.push(current);
|
||||
|
||||
// 更新进度条
|
||||
const percentage = Math.round((index / total) * 100);
|
||||
progressBar.style.width = `${percentage}%`;
|
||||
|
||||
// 更新进度文本
|
||||
progressText.textContent = t('oauth.kiro.importingProgress', { current: index, total: total });
|
||||
|
||||
// 添加结果项
|
||||
const resultItem = document.createElement('div');
|
||||
resultItem.style.cssText = 'padding: 4px 0; border-bottom: 1px solid rgba(0,0,0,0.1);';
|
||||
|
||||
if (current.success) {
|
||||
resultItem.innerHTML = `Token ${current.index}: <span style="color: #166534;">✓ ${current.path}</span>`;
|
||||
} else if (current.error === 'duplicate') {
|
||||
resultItem.innerHTML = `Token ${current.index}: <span style="color: #d97706;">⚠ ${t('oauth.kiro.duplicateToken')}</span>
|
||||
${current.existingPath ? `<span style="color: #666; font-size: 11px;">(${current.existingPath})</span>` : ''}`;
|
||||
} else {
|
||||
resultItem.innerHTML = `Token ${current.index}: <span style="color: #991b1b;">✗ ${current.error}</span>`;
|
||||
}
|
||||
|
||||
resultsList.appendChild(resultItem);
|
||||
// 自动滚动到底部
|
||||
resultsList.scrollTop = resultsList.scrollHeight;
|
||||
|
||||
} else if (eventType === 'complete') {
|
||||
// 完成事件
|
||||
progressBar.style.width = '100%';
|
||||
progressDiv.style.display = 'none';
|
||||
|
||||
const isAllSuccess = data.failedCount === 0;
|
||||
const isAllFailed = data.successCount === 0;
|
||||
|
||||
let resultClass, resultIcon, resultMessage;
|
||||
if (isAllSuccess) {
|
||||
resultClass = 'background: #f0fdf4; border: 1px solid #bbf7d0; color: #166534;';
|
||||
resultIcon = 'fa-check-circle';
|
||||
resultMessage = t('oauth.kiro.importSuccess', { count: data.successCount });
|
||||
} else if (isAllFailed) {
|
||||
resultClass = 'background: #fef2f2; border: 1px solid #fecaca; color: #991b1b;';
|
||||
resultIcon = 'fa-times-circle';
|
||||
resultMessage = t('oauth.kiro.importAllFailed', { count: data.failedCount });
|
||||
} else {
|
||||
resultClass = 'background: #fffbeb; border: 1px solid #fde68a; color: #92400e;';
|
||||
resultIcon = 'fa-exclamation-triangle';
|
||||
resultMessage = t('oauth.kiro.importPartial', { success: data.successCount, failed: data.failedCount });
|
||||
}
|
||||
|
||||
// 更新结果区域样式
|
||||
resultDiv.style.cssText = `display: block; margin-top: 16px; padding: 12px; border-radius: 8px; ${resultClass}`;
|
||||
|
||||
// 更新标题
|
||||
const headerDiv = resultDiv.querySelector('div:first-child');
|
||||
headerDiv.innerHTML = `<i class="fas ${resultIcon}"></i> <strong>${resultMessage}</strong>`;
|
||||
|
||||
// 如果有成功的,刷新提供商列表
|
||||
if (data.successCount > 0) {
|
||||
loadProviders();
|
||||
loadConfigList();
|
||||
}
|
||||
|
||||
} else if (eventType === 'error') {
|
||||
throw new Error(data.error);
|
||||
}
|
||||
} catch (parseError) {
|
||||
console.warn('Failed to parse SSE data:', parseError);
|
||||
}
|
||||
|
||||
eventType = '';
|
||||
eventData = '';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('批量导入失败:', error);
|
||||
console.error('[Kiro Batch Import] Failed:', error);
|
||||
progressDiv.style.display = 'none';
|
||||
resultDiv.style.cssText = 'display: block; margin-top: 16px; padding: 12px; border-radius: 8px; background: #fef2f2; border: 1px solid #fecaca; color: #991b1b;';
|
||||
resultDiv.innerHTML = `
|
||||
<div style="display: flex; align-items: center; gap: 8px;">
|
||||
|
|
@ -708,7 +807,6 @@ function showKiroBatchImportModal() {
|
|||
<strong>${t('oauth.kiro.importError')}: ${error.message}</strong>
|
||||
</div>
|
||||
`;
|
||||
progressDiv.style.display = 'none';
|
||||
} finally {
|
||||
// 重新启用按钮
|
||||
textarea.disabled = false;
|
||||
|
|
@ -718,6 +816,536 @@ function showKiroBatchImportModal() {
|
|||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示 Kiro AWS 账号导入模态框
|
||||
* 支持从 AWS SSO cache 目录导入凭据文件,或直接粘贴 JSON
|
||||
*/
|
||||
function showKiroAwsImportModal() {
|
||||
const modal = document.createElement('div');
|
||||
modal.className = 'modal-overlay';
|
||||
modal.style.display = 'flex';
|
||||
|
||||
modal.innerHTML = `
|
||||
<div class="modal-content" style="max-width: 700px;">
|
||||
<div class="modal-header">
|
||||
<h3><i class="fas fa-cloud-upload-alt" style="color: #ff9900;"></i> <span data-i18n="oauth.kiro.awsImport">${t('oauth.kiro.awsImport')}</span></h3>
|
||||
<button class="modal-close">×</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="aws-import-instructions" style="margin-bottom: 16px; padding: 12px; background: #fff7ed; border: 1px solid #fed7aa; border-radius: 8px;">
|
||||
<p style="margin: 0; font-size: 14px; color: #9a3412;">
|
||||
<i class="fas fa-info-circle"></i>
|
||||
<span data-i18n="oauth.kiro.awsImportInstructions">${t('oauth.kiro.awsImportInstructions')}</span>
|
||||
</p>
|
||||
<p style="margin: 8px 0 0 0; font-size: 12px; color: #c2410c;">
|
||||
<i class="fas fa-folder-open"></i>
|
||||
<code style="background: #fed7aa; padding: 2px 6px; border-radius: 4px;">C:\\Users\\{username}\\.aws\\sso\\cache</code>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- 输入模式切换 -->
|
||||
<div class="input-mode-toggle" style="display: flex; gap: 8px; margin-bottom: 16px;">
|
||||
<button class="mode-btn active" data-mode="file" style="flex: 1; padding: 10px 16px; border: 2px solid #ff9900; border-radius: 8px; background: #fff7ed; color: #9a3412; font-weight: 600; cursor: pointer; transition: all 0.2s;">
|
||||
<i class="fas fa-file-upload"></i>
|
||||
<span data-i18n="oauth.kiro.awsModeFile">${t('oauth.kiro.awsModeFile')}</span>
|
||||
</button>
|
||||
<button class="mode-btn" data-mode="json" style="flex: 1; padding: 10px 16px; border: 2px solid #d1d5db; border-radius: 8px; background: white; color: #6b7280; font-weight: 600; cursor: pointer; transition: all 0.2s;">
|
||||
<i class="fas fa-code"></i>
|
||||
<span data-i18n="oauth.kiro.awsModeJson">${t('oauth.kiro.awsModeJson')}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 文件上传模式 -->
|
||||
<div class="file-mode-section" id="fileModeSection">
|
||||
<div class="form-group" style="margin-bottom: 16px;">
|
||||
<label style="display: block; margin-bottom: 8px; font-weight: 600; color: #374151;">
|
||||
<span data-i18n="oauth.kiro.awsUploadFiles">${t('oauth.kiro.awsUploadFiles')}</span>
|
||||
</label>
|
||||
<div class="aws-file-upload-area" style="border: 2px dashed #d1d5db; border-radius: 8px; padding: 24px; text-align: center; cursor: pointer; transition: all 0.2s;">
|
||||
<input type="file" id="awsFilesInput" multiple accept=".json" style="display: none;">
|
||||
<i class="fas fa-cloud-upload-alt" style="font-size: 36px; color: #9ca3af; margin-bottom: 8px;"></i>
|
||||
<p style="margin: 0; color: #6b7280;" data-i18n="oauth.kiro.awsDragDrop">${t('oauth.kiro.awsDragDrop')}</p>
|
||||
<p style="margin: 4px 0 0 0; font-size: 12px; color: #9ca3af;" data-i18n="oauth.kiro.awsClickUpload">${t('oauth.kiro.awsClickUpload')}</p>
|
||||
</div>
|
||||
<p style="margin: 8px 0 0 0; font-size: 12px; color: #6b7280;">
|
||||
<i class="fas fa-lightbulb" style="color: #f59e0b;"></i>
|
||||
<span data-i18n="oauth.kiro.awsFileHint">${t('oauth.kiro.awsFileHint')}</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="aws-files-list" id="awsFilesList" style="display: none; margin-bottom: 16px;">
|
||||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px;">
|
||||
<label style="font-weight: 600; color: #374151;" data-i18n="oauth.kiro.awsSelectedFiles">${t('oauth.kiro.awsSelectedFiles')}</label>
|
||||
<button id="clearFilesBtn" style="background: none; border: none; color: #ef4444; cursor: pointer; font-size: 12px; padding: 4px 8px; border-radius: 4px; transition: all 0.2s;">
|
||||
<i class="fas fa-trash-alt"></i>
|
||||
<span data-i18n="oauth.kiro.awsClearFiles">${t('oauth.kiro.awsClearFiles')}</span>
|
||||
</button>
|
||||
</div>
|
||||
<div id="awsFilesContainer" style="background: #f9fafb; border-radius: 8px; padding: 12px;"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- JSON 输入模式 -->
|
||||
<div class="json-mode-section" id="jsonModeSection" style="display: none;">
|
||||
<div class="form-group" style="margin-bottom: 16px;">
|
||||
<label style="display: block; margin-bottom: 8px; font-weight: 600; color: #374151;">
|
||||
<span data-i18n="oauth.kiro.awsJsonInput">${t('oauth.kiro.awsJsonInput')}</span>
|
||||
</label>
|
||||
<textarea
|
||||
id="awsJsonInput"
|
||||
rows="12"
|
||||
style="width: 100%; padding: 12px; border: 1px solid #d1d5db; border-radius: 8px; font-family: monospace; font-size: 13px; resize: vertical;"
|
||||
placeholder="${t('oauth.kiro.awsJsonPlaceholderSimple')}"
|
||||
data-i18n-placeholder="oauth.kiro.awsJsonPlaceholderSimple"
|
||||
></textarea>
|
||||
<p style="margin: 8px 0 0 0; font-size: 12px; color: #6b7280;">
|
||||
<i class="fas fa-lightbulb" style="color: #f59e0b;"></i>
|
||||
<span data-i18n="oauth.kiro.awsJsonHint">${t('oauth.kiro.awsJsonHint')}</span>
|
||||
</p>
|
||||
</div>
|
||||
<details style="margin-bottom: 16px; background: #f9fafb; border: 1px solid #e5e7eb; border-radius: 8px;">
|
||||
<summary style="padding: 12px; cursor: pointer; font-weight: 600; color: #374151; user-select: none;">
|
||||
<i class="fas fa-code" style="color: #ff9900; margin-right: 8px;"></i>
|
||||
<span data-i18n="oauth.kiro.awsJsonExample">${t('oauth.kiro.awsJsonExample')}</span>
|
||||
</summary>
|
||||
<pre style="margin: 0; padding: 12px; background: #1f2937; color: #10b981; border-radius: 0 0 8px 8px; font-family: monospace; font-size: 12px; overflow-x: auto; white-space: pre;">{
|
||||
"clientId": "VYZBSTx3Q7QEq1W3Wn8c5nVzLWVhc3QtMQ",
|
||||
"clientSecret": "eyJraWQi...OAMc",
|
||||
"expiresAt": "2026-01-09T04:43:18.079944400+00:00",
|
||||
"accessToken": "aoaAAAAAGlgghoSqRgQK...2tfhmdNZDA",
|
||||
"authMethod": "IdC",
|
||||
"provider": "BuilderId",
|
||||
"refreshToken": "aorAAAAAGn...uKw+E3",
|
||||
"region": "us-east-1"
|
||||
}</pre>
|
||||
</details>
|
||||
</div>
|
||||
|
||||
<div class="aws-validation-result" id="awsValidationResult" style="display: none; margin-bottom: 16px; padding: 12px; border-radius: 8px;"></div>
|
||||
|
||||
<div class="aws-json-preview" id="awsJsonPreview" style="display: none; margin-bottom: 16px;">
|
||||
<label style="display: block; margin-bottom: 8px; font-weight: 600; color: #374151;">
|
||||
<i class="fas fa-eye"></i>
|
||||
<span data-i18n="oauth.kiro.awsPreviewJson">${t('oauth.kiro.awsPreviewJson')}</span>
|
||||
</label>
|
||||
<pre id="awsJsonContent" style="background: #1f2937; color: #10b981; padding: 16px; border-radius: 8px; font-family: monospace; font-size: 12px; max-height: 200px; overflow: auto; white-space: pre-wrap; word-break: break-all;"></pre>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="modal-cancel" data-i18n="modal.provider.cancel">${t('modal.provider.cancel')}</button>
|
||||
<button class="btn btn-primary aws-import-submit" id="awsImportSubmit" disabled>
|
||||
<i class="fas fa-check"></i>
|
||||
<span data-i18n="oauth.kiro.awsConfirmImport">${t('oauth.kiro.awsConfirmImport')}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
document.body.appendChild(modal);
|
||||
|
||||
const fileInput = modal.querySelector('#awsFilesInput');
|
||||
const uploadArea = modal.querySelector('.aws-file-upload-area');
|
||||
const filesListDiv = modal.querySelector('#awsFilesList');
|
||||
const filesContainer = modal.querySelector('#awsFilesContainer');
|
||||
const clearFilesBtn = modal.querySelector('#clearFilesBtn');
|
||||
const validationResult = modal.querySelector('#awsValidationResult');
|
||||
const jsonPreview = modal.querySelector('#awsJsonPreview');
|
||||
const jsonContent = modal.querySelector('#awsJsonContent');
|
||||
const submitBtn = modal.querySelector('#awsImportSubmit');
|
||||
const closeBtn = modal.querySelector('.modal-close');
|
||||
const cancelBtn = modal.querySelector('.modal-cancel');
|
||||
const modeBtns = modal.querySelectorAll('.mode-btn');
|
||||
const fileModeSection = modal.querySelector('#fileModeSection');
|
||||
const jsonModeSection = modal.querySelector('#jsonModeSection');
|
||||
const jsonInputTextarea = modal.querySelector('#awsJsonInput');
|
||||
|
||||
let uploadedFiles = [];
|
||||
let mergedCredentials = null;
|
||||
let currentMode = 'file';
|
||||
|
||||
// 清空文件按钮事件
|
||||
clearFilesBtn.addEventListener('click', () => {
|
||||
uploadedFiles = [];
|
||||
filesContainer.innerHTML = '';
|
||||
filesListDiv.style.display = 'none';
|
||||
validationResult.style.display = 'none';
|
||||
jsonPreview.style.display = 'none';
|
||||
submitBtn.disabled = true;
|
||||
mergedCredentials = null;
|
||||
// 清空 file input
|
||||
fileInput.value = '';
|
||||
});
|
||||
|
||||
// 清空按钮 hover 效果
|
||||
clearFilesBtn.addEventListener('mouseenter', () => {
|
||||
clearFilesBtn.style.background = '#fef2f2';
|
||||
});
|
||||
clearFilesBtn.addEventListener('mouseleave', () => {
|
||||
clearFilesBtn.style.background = 'none';
|
||||
});
|
||||
|
||||
// 模式切换
|
||||
modeBtns.forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
const mode = btn.dataset.mode;
|
||||
if (mode === currentMode) return;
|
||||
|
||||
currentMode = mode;
|
||||
|
||||
// 更新按钮样式
|
||||
modeBtns.forEach(b => {
|
||||
if (b.dataset.mode === mode) {
|
||||
b.style.borderColor = '#ff9900';
|
||||
b.style.background = '#fff7ed';
|
||||
b.style.color = '#9a3412';
|
||||
b.classList.add('active');
|
||||
} else {
|
||||
b.style.borderColor = '#d1d5db';
|
||||
b.style.background = 'white';
|
||||
b.style.color = '#6b7280';
|
||||
b.classList.remove('active');
|
||||
}
|
||||
});
|
||||
|
||||
// 切换显示区域
|
||||
if (mode === 'file') {
|
||||
fileModeSection.style.display = 'block';
|
||||
jsonModeSection.style.display = 'none';
|
||||
// 重新验证文件模式的内容
|
||||
validateAndPreview();
|
||||
} else {
|
||||
fileModeSection.style.display = 'none';
|
||||
jsonModeSection.style.display = 'block';
|
||||
// 验证 JSON 输入
|
||||
validateJsonInput();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// JSON 输入实时验证
|
||||
jsonInputTextarea.addEventListener('input', () => {
|
||||
validateJsonInput();
|
||||
});
|
||||
|
||||
// 验证 JSON 输入
|
||||
function validateJsonInput() {
|
||||
const inputValue = jsonInputTextarea.value.trim();
|
||||
|
||||
if (!inputValue) {
|
||||
validationResult.style.display = 'none';
|
||||
jsonPreview.style.display = 'none';
|
||||
submitBtn.disabled = true;
|
||||
mergedCredentials = null;
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
mergedCredentials = JSON.parse(inputValue);
|
||||
validateAndShowResult();
|
||||
} catch (error) {
|
||||
validationResult.style.cssText = 'display: block; margin-bottom: 16px; padding: 12px; border-radius: 8px; background: #fef2f2; border: 1px solid #fecaca; color: #991b1b;';
|
||||
validationResult.innerHTML = `
|
||||
<div style="display: flex; align-items: center; gap: 8px;">
|
||||
<i class="fas fa-exclamation-triangle"></i>
|
||||
<strong data-i18n="oauth.kiro.awsJsonParseError">${t('oauth.kiro.awsJsonParseError')}</strong>
|
||||
</div>
|
||||
<p style="margin: 8px 0 0 0; font-size: 12px;">${error.message}</p>
|
||||
`;
|
||||
jsonPreview.style.display = 'none';
|
||||
submitBtn.disabled = true;
|
||||
mergedCredentials = null;
|
||||
}
|
||||
}
|
||||
|
||||
// 文件上传区域交互
|
||||
uploadArea.addEventListener('click', () => fileInput.click());
|
||||
|
||||
uploadArea.addEventListener('dragover', (e) => {
|
||||
e.preventDefault();
|
||||
uploadArea.style.borderColor = '#ff9900';
|
||||
uploadArea.style.background = '#fffbeb';
|
||||
});
|
||||
|
||||
uploadArea.addEventListener('dragleave', (e) => {
|
||||
e.preventDefault();
|
||||
uploadArea.style.borderColor = '#d1d5db';
|
||||
uploadArea.style.background = 'transparent';
|
||||
});
|
||||
|
||||
uploadArea.addEventListener('drop', (e) => {
|
||||
e.preventDefault();
|
||||
uploadArea.style.borderColor = '#d1d5db';
|
||||
uploadArea.style.background = 'transparent';
|
||||
|
||||
const files = Array.from(e.dataTransfer.files).filter(f => f.name.endsWith('.json'));
|
||||
if (files.length > 0) {
|
||||
processFiles(files);
|
||||
}
|
||||
});
|
||||
|
||||
fileInput.addEventListener('change', () => {
|
||||
const files = Array.from(fileInput.files);
|
||||
if (files.length > 0) {
|
||||
processFiles(files);
|
||||
}
|
||||
});
|
||||
|
||||
// 处理上传的文件(支持追加)
|
||||
async function processFiles(files) {
|
||||
for (const file of files) {
|
||||
// 检查是否已存在同名文件
|
||||
const existingIndex = uploadedFiles.findIndex(f => f.name === file.name);
|
||||
|
||||
try {
|
||||
const content = await readFileAsText(file);
|
||||
const json = JSON.parse(content);
|
||||
|
||||
if (existingIndex >= 0) {
|
||||
// 替换已存在的同名文件
|
||||
uploadedFiles[existingIndex] = {
|
||||
name: file.name,
|
||||
content: json
|
||||
};
|
||||
showToast(t('common.info'), t('oauth.kiro.awsFileReplaced', { filename: file.name }), 'info');
|
||||
} else {
|
||||
// 追加新文件
|
||||
uploadedFiles.push({
|
||||
name: file.name,
|
||||
content: json
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Failed to parse ${file.name}:`, error);
|
||||
showToast(t('common.error'), t('oauth.kiro.awsParseError', { filename: file.name }), 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// 重新渲染文件列表
|
||||
renderFilesList();
|
||||
|
||||
filesListDiv.style.display = uploadedFiles.length > 0 ? 'block' : 'none';
|
||||
|
||||
// 清空 file input 以便可以再次选择相同文件
|
||||
fileInput.value = '';
|
||||
|
||||
validateAndPreview();
|
||||
}
|
||||
|
||||
// 渲染文件列表
|
||||
function renderFilesList() {
|
||||
filesContainer.innerHTML = '';
|
||||
|
||||
for (const file of uploadedFiles) {
|
||||
const fileDiv = document.createElement('div');
|
||||
fileDiv.style.cssText = 'display: flex; justify-content: space-between; align-items: center; padding: 8px; background: white; border-radius: 4px; margin-bottom: 4px;';
|
||||
fileDiv.dataset.filename = file.name;
|
||||
|
||||
const fields = Object.keys(file.content).slice(0, 5).join(', ');
|
||||
const moreFields = Object.keys(file.content).length > 5 ? '...' : '';
|
||||
|
||||
fileDiv.innerHTML = `
|
||||
<div style="flex: 1; min-width: 0;">
|
||||
<i class="fas fa-file-code" style="color: #ff9900; margin-right: 8px;"></i>
|
||||
<span style="font-weight: 500;">${file.name}</span>
|
||||
<div style="font-size: 11px; color: #6b7280; margin-top: 2px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">${fields}${moreFields}</div>
|
||||
</div>
|
||||
<button class="remove-file-btn" data-filename="${file.name}" style="background: none; border: none; color: #ef4444; cursor: pointer; padding: 4px 8px; margin-left: 8px; flex-shrink: 0;">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
`;
|
||||
filesContainer.appendChild(fileDiv);
|
||||
}
|
||||
|
||||
// 添加删除文件按钮事件
|
||||
filesContainer.querySelectorAll('.remove-file-btn').forEach(btn => {
|
||||
btn.addEventListener('click', (e) => {
|
||||
const filename = e.currentTarget.dataset.filename;
|
||||
uploadedFiles = uploadedFiles.filter(f => f.name !== filename);
|
||||
renderFilesList();
|
||||
filesListDiv.style.display = uploadedFiles.length > 0 ? 'block' : 'none';
|
||||
validateAndPreview();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// 验证并预览(文件模式)
|
||||
function validateAndPreview() {
|
||||
if (currentMode !== 'file') return;
|
||||
|
||||
if (uploadedFiles.length === 0) {
|
||||
validationResult.style.display = 'none';
|
||||
jsonPreview.style.display = 'none';
|
||||
submitBtn.disabled = true;
|
||||
mergedCredentials = null;
|
||||
return;
|
||||
}
|
||||
|
||||
// 智能合并所有文件的内容
|
||||
// 如果多个文件都有 expiresAt,使用包含 refreshToken 的文件中的 expiresAt
|
||||
mergedCredentials = {};
|
||||
let expiresAtFromRefreshTokenFile = null;
|
||||
|
||||
for (const file of uploadedFiles) {
|
||||
// 如果这个文件包含 refreshToken,记录它的 expiresAt
|
||||
if (file.content.refreshToken && file.content.expiresAt) {
|
||||
expiresAtFromRefreshTokenFile = file.content.expiresAt;
|
||||
}
|
||||
Object.assign(mergedCredentials, file.content);
|
||||
}
|
||||
|
||||
// 如果找到了包含 refreshToken 的文件的 expiresAt,使用它
|
||||
if (expiresAtFromRefreshTokenFile) {
|
||||
mergedCredentials.expiresAt = expiresAtFromRefreshTokenFile;
|
||||
}
|
||||
|
||||
validateAndShowResult();
|
||||
}
|
||||
|
||||
// 验证并显示结果(通用)
|
||||
function validateAndShowResult() {
|
||||
if (!mergedCredentials) {
|
||||
validationResult.style.display = 'none';
|
||||
jsonPreview.style.display = 'none';
|
||||
submitBtn.disabled = true;
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查所有四个必需字段
|
||||
const hasClientId = !!mergedCredentials.clientId;
|
||||
const hasClientSecret = !!mergedCredentials.clientSecret;
|
||||
const hasAccessToken = !!mergedCredentials.accessToken;
|
||||
const hasRefreshToken = !!mergedCredentials.refreshToken;
|
||||
|
||||
// 所有四个字段都必须存在
|
||||
const isValid = hasClientId && hasClientSecret && hasAccessToken && hasRefreshToken;
|
||||
|
||||
// 构建字段状态列表
|
||||
const fieldsList = [
|
||||
{ key: 'clientId', has: hasClientId },
|
||||
{ key: 'clientSecret', has: hasClientSecret },
|
||||
{ key: 'accessToken', has: hasAccessToken },
|
||||
{ key: 'refreshToken', has: hasRefreshToken }
|
||||
];
|
||||
|
||||
const fieldsHtml = fieldsList.map(f => `
|
||||
<li>${f.key}: ${f.has
|
||||
? `<code style="background: #dcfce7; padding: 1px 4px; border-radius: 2px; color: #166534;">✓ ${t('common.found')}</code>`
|
||||
: `<code style="background: #fecaca; padding: 1px 4px; border-radius: 2px; color: #991b1b;">✗ ${t('common.missing')}</code>`
|
||||
}</li>
|
||||
`).join('');
|
||||
|
||||
if (isValid) {
|
||||
validationResult.style.cssText = 'display: block; margin-bottom: 16px; padding: 12px; border-radius: 8px; background: #f0fdf4; border: 1px solid #bbf7d0; color: #166534;';
|
||||
validationResult.innerHTML = `
|
||||
<div style="display: flex; align-items: center; gap: 8px;">
|
||||
<i class="fas fa-check-circle"></i>
|
||||
<strong data-i18n="oauth.kiro.awsValidationSuccess">${t('oauth.kiro.awsValidationSuccess')}</strong>
|
||||
</div>
|
||||
<ul style="margin: 8px 0 0 24px; font-size: 13px; list-style: none; padding: 0;">
|
||||
${fieldsHtml}
|
||||
</ul>
|
||||
`;
|
||||
submitBtn.disabled = false;
|
||||
} else {
|
||||
const missingCount = fieldsList.filter(f => !f.has).length;
|
||||
validationResult.style.cssText = 'display: block; margin-bottom: 16px; padding: 12px; border-radius: 8px; background: #fef2f2; border: 1px solid #fecaca; color: #991b1b;';
|
||||
validationResult.innerHTML = `
|
||||
<div style="display: flex; align-items: center; gap: 8px;">
|
||||
<i class="fas fa-exclamation-triangle"></i>
|
||||
<strong>${t('oauth.kiro.awsValidationFailed')}</strong>
|
||||
<span style="font-weight: normal; font-size: 12px;">(${t('oauth.kiro.awsMissingFields', { count: missingCount })})</span>
|
||||
</div>
|
||||
<ul style="margin: 8px 0 0 24px; font-size: 13px; list-style: none; padding: 0;">
|
||||
${fieldsHtml}
|
||||
</ul>
|
||||
<p style="margin: 12px 0 0 0; font-size: 12px; padding: 8px; background: #fee2e2; border-radius: 4px;">
|
||||
<i class="fas fa-lightbulb" style="color: #dc2626;"></i>
|
||||
<span data-i18n="oauth.kiro.awsUploadMore">${t('oauth.kiro.awsUploadMore')}</span>
|
||||
</p>
|
||||
`;
|
||||
submitBtn.disabled = true;
|
||||
}
|
||||
|
||||
// 显示 JSON 预览
|
||||
jsonPreview.style.display = 'block';
|
||||
|
||||
// 隐藏敏感信息的部分内容
|
||||
const previewData = { ...mergedCredentials };
|
||||
if (previewData.clientSecret) {
|
||||
previewData.clientSecret = previewData.clientSecret.substring(0, 8) + '...' + previewData.clientSecret.slice(-4);
|
||||
}
|
||||
if (previewData.accessToken) {
|
||||
previewData.accessToken = previewData.accessToken.substring(0, 20) + '...' + previewData.accessToken.slice(-10);
|
||||
}
|
||||
if (previewData.refreshToken) {
|
||||
previewData.refreshToken = previewData.refreshToken.substring(0, 10) + '...' + previewData.refreshToken.slice(-6);
|
||||
}
|
||||
|
||||
jsonContent.textContent = JSON.stringify(previewData, null, 2);
|
||||
}
|
||||
|
||||
// 读取文件内容
|
||||
function readFileAsText(file) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
reader.onload = (e) => resolve(e.target.result);
|
||||
reader.onerror = (e) => reject(e);
|
||||
reader.readAsText(file);
|
||||
});
|
||||
}
|
||||
|
||||
// 关闭按钮事件
|
||||
[closeBtn, cancelBtn].forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
modal.remove();
|
||||
});
|
||||
});
|
||||
|
||||
// 提交按钮事件
|
||||
submitBtn.addEventListener('click', async () => {
|
||||
if (!mergedCredentials) {
|
||||
showToast(t('common.warning'), t('oauth.kiro.awsNoCredentials'), 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
// 禁用按钮
|
||||
submitBtn.disabled = true;
|
||||
submitBtn.innerHTML = `<i class="fas fa-spinner fa-spin"></i> <span>${t('oauth.kiro.awsImporting')}</span>`;
|
||||
|
||||
try {
|
||||
// 确保 authMethod 为 builder-id(AWS 账号模式)
|
||||
if (!mergedCredentials.authMethod) {
|
||||
mergedCredentials.authMethod = 'builder-id';
|
||||
}
|
||||
|
||||
const response = await window.apiClient.post('/kiro/import-aws-credentials', {
|
||||
credentials: mergedCredentials
|
||||
});
|
||||
|
||||
if (response.success) {
|
||||
showToast(t('common.success'), t('oauth.kiro.awsImportSuccess'), 'success');
|
||||
modal.remove();
|
||||
|
||||
// 刷新提供商列表和配置列表
|
||||
loadProviders();
|
||||
loadConfigList();
|
||||
} else if (response.error === 'duplicate') {
|
||||
// 显示重复凭据警告
|
||||
const existingPath = response.existingPath || '';
|
||||
showToast(t('common.warning'), t('oauth.kiro.duplicateCredentials') + (existingPath ? ` (${existingPath})` : ''), 'warning');
|
||||
} else {
|
||||
showToast(t('common.error'), response.error || t('oauth.kiro.awsImportFailed'), 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('AWS import failed:', error);
|
||||
showToast(t('common.error'), t('oauth.kiro.awsImportFailed') + ': ' + error.message, 'error');
|
||||
} finally {
|
||||
submitBtn.disabled = false;
|
||||
submitBtn.innerHTML = `<i class="fas fa-check"></i> <span data-i18n="oauth.kiro.awsConfirmImport">${t('oauth.kiro.awsConfirmImport')}</span>`;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行生成授权链接
|
||||
* @param {string} providerType - 提供商类型
|
||||
|
|
|
|||
Loading…
Reference in a new issue