612 lines
No EOL
23 KiB
JavaScript
612 lines
No EOL
23 KiB
JavaScript
import { getRequestBody } from '../utils/common.js';
|
||
import {
|
||
handleGeminiCliOAuth,
|
||
handleGeminiAntigravityOAuth,
|
||
handleQwenOAuth,
|
||
handleKiroOAuth,
|
||
handleIFlowOAuth,
|
||
handleOrchidsOAuth,
|
||
handleCodexOAuth,
|
||
batchImportKiroRefreshTokensStream,
|
||
importAwsCredentials,
|
||
importOrchidsToken
|
||
} from '../auth/oauth-handlers.js';
|
||
|
||
/**
|
||
* 生成 OAuth 授权 URL
|
||
*/
|
||
export async function handleGenerateAuthUrl(req, res, currentConfig, providerType) {
|
||
try {
|
||
let authUrl = '';
|
||
let authInfo = {};
|
||
|
||
// 解析 options
|
||
let options = {};
|
||
try {
|
||
options = await getRequestBody(req);
|
||
} catch (e) {
|
||
// 如果没有请求体,使用默认空对象
|
||
}
|
||
|
||
// 根据提供商类型生成授权链接并启动回调服务器
|
||
if (providerType === 'gemini-cli-oauth') {
|
||
const result = await handleGeminiCliOAuth(currentConfig, options);
|
||
authUrl = result.authUrl;
|
||
authInfo = result.authInfo;
|
||
} else if (providerType === 'gemini-antigravity') {
|
||
const result = await handleGeminiAntigravityOAuth(currentConfig, options);
|
||
authUrl = result.authUrl;
|
||
authInfo = result.authInfo;
|
||
} else if (providerType === 'openai-qwen-oauth') {
|
||
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 if (providerType === 'openai-iflow') {
|
||
// iFlow OAuth 授权
|
||
const result = await handleIFlowOAuth(currentConfig, options);
|
||
authUrl = result.authUrl;
|
||
authInfo = result.authInfo;
|
||
} else if (providerType === 'claude-orchids-oauth') {
|
||
// Orchids OAuth(手动导入模式)
|
||
const result = await handleOrchidsOAuth(currentConfig, options);
|
||
authUrl = result.authUrl;
|
||
authInfo = result.authInfo;
|
||
} else if (providerType === 'openai-codex-oauth') {
|
||
// Codex OAuth(OAuth2 + PKCE)
|
||
const result = await handleCodexOAuth(currentConfig, options);
|
||
authUrl = result.authUrl;
|
||
authInfo = result.authInfo;
|
||
} else {
|
||
res.writeHead(400, { 'Content-Type': 'application/json' });
|
||
res.end(JSON.stringify({
|
||
error: {
|
||
message: `Unsupported provider type: ${providerType}`
|
||
}
|
||
}));
|
||
return true;
|
||
}
|
||
|
||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||
res.end(JSON.stringify({
|
||
success: true,
|
||
authUrl: authUrl,
|
||
authInfo: authInfo
|
||
}));
|
||
return true;
|
||
|
||
} catch (error) {
|
||
console.error(`[UI API] Failed to generate auth URL for ${providerType}:`, error);
|
||
res.writeHead(500, { 'Content-Type': 'application/json' });
|
||
res.end(JSON.stringify({
|
||
error: {
|
||
message: `Failed to generate auth URL: ${error.message}`
|
||
}
|
||
}));
|
||
return true;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 处理手动 OAuth 回调
|
||
*/
|
||
export async function handleManualOAuthCallback(req, res) {
|
||
try {
|
||
const body = await getRequestBody(req);
|
||
const { provider, callbackUrl, authMethod } = body;
|
||
|
||
if (!provider || !callbackUrl) {
|
||
res.writeHead(400, { 'Content-Type': 'application/json' });
|
||
res.end(JSON.stringify({
|
||
success: false,
|
||
error: 'provider and callbackUrl are required'
|
||
}));
|
||
return true;
|
||
}
|
||
|
||
console.log(`[OAuth Manual Callback] Processing manual callback for ${provider}`);
|
||
console.log(`[OAuth Manual Callback] Callback URL: ${callbackUrl}`);
|
||
|
||
// 解析回调URL
|
||
const url = new URL(callbackUrl);
|
||
const code = url.searchParams.get('code');
|
||
const state = url.searchParams.get('state');
|
||
const token = url.searchParams.get('token');
|
||
|
||
if (!code && !token) {
|
||
res.writeHead(400, { 'Content-Type': 'application/json' });
|
||
res.end(JSON.stringify({
|
||
success: false,
|
||
error: 'Callback URL must contain code or token parameter'
|
||
}));
|
||
return true;
|
||
}
|
||
|
||
// 特殊处理 Codex OAuth 回调
|
||
if (provider === 'openai-codex-oauth' && code && state) {
|
||
const { handleCodexOAuthCallback } = await import('../auth/oauth-handlers.js');
|
||
const result = await handleCodexOAuthCallback(code, state);
|
||
|
||
res.writeHead(result.success ? 200 : 500, { 'Content-Type': 'application/json' });
|
||
res.end(JSON.stringify(result));
|
||
return true;
|
||
}
|
||
|
||
// 通过fetch请求本地OAuth回调服务器处理
|
||
// 使用localhost而不是原始hostname,确保请求到达本地服务器
|
||
const localUrl = new URL(callbackUrl);
|
||
localUrl.hostname = 'localhost';
|
||
localUrl.protocol = 'http:';
|
||
|
||
try {
|
||
const response = await fetch(localUrl.href);
|
||
|
||
if (response.ok) {
|
||
console.log(`[OAuth Manual Callback] Successfully processed callback for ${provider}`);
|
||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||
res.end(JSON.stringify({
|
||
success: true,
|
||
message: 'OAuth callback processed successfully'
|
||
}));
|
||
} else {
|
||
const errorText = await response.text();
|
||
console.error(`[OAuth Manual Callback] Callback processing failed:`, errorText);
|
||
res.writeHead(500, { 'Content-Type': 'application/json' });
|
||
res.end(JSON.stringify({
|
||
success: false,
|
||
error: `Callback processing failed: ${response.status}`
|
||
}));
|
||
}
|
||
} catch (fetchError) {
|
||
console.error(`[OAuth Manual Callback] Failed to process callback:`, fetchError);
|
||
res.writeHead(500, { 'Content-Type': 'application/json' });
|
||
res.end(JSON.stringify({
|
||
success: false,
|
||
error: `Failed to process callback: ${fetchError.message}`
|
||
}));
|
||
}
|
||
|
||
return true;
|
||
} catch (error) {
|
||
console.error('[OAuth Manual Callback] Error:', error);
|
||
res.writeHead(500, { 'Content-Type': 'application/json' });
|
||
res.end(JSON.stringify({
|
||
success: false,
|
||
error: error.message
|
||
}));
|
||
return true;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 批量导入 Kiro refreshToken(带实时进度 SSE)
|
||
*/
|
||
export async function handleBatchImportKiroTokens(req, res) {
|
||
try {
|
||
const body = await getRequestBody(req);
|
||
const { refreshTokens, region } = body;
|
||
|
||
if (!refreshTokens || !Array.isArray(refreshTokens) || refreshTokens.length === 0) {
|
||
res.writeHead(400, { 'Content-Type': 'application/json' });
|
||
res.end(JSON.stringify({
|
||
success: false,
|
||
error: 'refreshTokens array is required and must not be empty'
|
||
}));
|
||
return true;
|
||
}
|
||
|
||
console.log(`[Kiro Batch Import] Starting batch import of ${refreshTokens.length} tokens with SSE...`);
|
||
|
||
// 设置 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`);
|
||
|
||
// 发送完成事件
|
||
sendSSE('complete', {
|
||
success: true,
|
||
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;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 导入 AWS SSO 凭据用于 Kiro
|
||
*/
|
||
export async function handleImportAwsCredentials(req, res) {
|
||
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,
|
||
error: error.message
|
||
}));
|
||
return true;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 导入 Orchids Token
|
||
* 支持三种格式:
|
||
* 1. cookieString 格式 (完整的 Cookie 字符串,包含 __client 和 __session)
|
||
* 2. token 字符串格式 (JWT|rotating_token) - 已废弃
|
||
* 3. credentials 对象格式 (cookies, clerkSessionId, userId, workingDir)
|
||
*/
|
||
export async function handleImportOrchidsToken(req, res) {
|
||
try {
|
||
const body = await getRequestBody(req);
|
||
const { token, credentials, workingDir, cookieString } = body;
|
||
|
||
// 新格式:完整的 Cookie 字符串
|
||
if (cookieString && typeof cookieString === 'string') {
|
||
console.log('[Orchids Import] Starting cookie string import...');
|
||
|
||
// 解析 Cookie 字符串
|
||
const parsedResult = parseOrchidsCookieString(cookieString);
|
||
if (!parsedResult.success) {
|
||
res.writeHead(400, { 'Content-Type': 'application/json' });
|
||
res.end(JSON.stringify({
|
||
success: false,
|
||
error: parsedResult.error
|
||
}));
|
||
return true;
|
||
}
|
||
|
||
// 保存凭据
|
||
const result = await saveOrchidsCredentials(parsedResult.credentials);
|
||
|
||
if (result.success) {
|
||
console.log(`[Orchids Import] Successfully imported credentials to: ${result.path}`);
|
||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||
res.end(JSON.stringify({
|
||
success: true,
|
||
path: result.path,
|
||
sessionId: result.sessionId,
|
||
userId: result.userId,
|
||
message: 'Orchids credentials imported successfully'
|
||
}));
|
||
} else {
|
||
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;
|
||
}
|
||
|
||
// 如果提供了 credentials 对象,直接保存
|
||
if (credentials && typeof credentials === 'object') {
|
||
console.log('[Orchids Import] Starting credentials import...');
|
||
|
||
// 验证必需字段
|
||
if (!credentials.cookies && (!credentials.clerkSessionId)) {
|
||
res.writeHead(400, { 'Content-Type': 'application/json' });
|
||
res.end(JSON.stringify({
|
||
success: false,
|
||
error: 'credentials must contain cookies or clerkSessionId'
|
||
}));
|
||
return true;
|
||
}
|
||
|
||
// 直接保存凭据
|
||
const result = await saveOrchidsCredentials(credentials);
|
||
|
||
if (result.success) {
|
||
console.log(`[Orchids Import] Successfully imported token to: ${result.path}`);
|
||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||
res.end(JSON.stringify({
|
||
success: true,
|
||
path: result.path,
|
||
sessionId: result.sessionId,
|
||
userId: result.userId,
|
||
message: 'Orchids credentials imported successfully'
|
||
}));
|
||
} else {
|
||
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;
|
||
}
|
||
|
||
// 原有的 token 字符串格式
|
||
if (!token || typeof token !== 'string') {
|
||
res.writeHead(400, { 'Content-Type': 'application/json' });
|
||
res.end(JSON.stringify({
|
||
success: false,
|
||
error: 'cookieString, token string or credentials object is required'
|
||
}));
|
||
return true;
|
||
}
|
||
|
||
console.log('[Orchids Import] Starting token import...');
|
||
|
||
const result = await importOrchidsToken(token, { workingDir });
|
||
|
||
if (result.success) {
|
||
console.log(`[Orchids Import] Successfully imported token to: ${result.path}`);
|
||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||
res.end(JSON.stringify({
|
||
success: true,
|
||
path: result.path,
|
||
sessionId: result.sessionId,
|
||
userId: result.userId,
|
||
message: 'Orchids token imported successfully'
|
||
}));
|
||
} else {
|
||
res.writeHead(500, { 'Content-Type': 'application/json' });
|
||
res.end(JSON.stringify({
|
||
success: false,
|
||
error: result.error
|
||
}));
|
||
}
|
||
return true;
|
||
|
||
} catch (error) {
|
||
console.error('[Orchids Import] Error:', error);
|
||
res.writeHead(500, { 'Content-Type': 'application/json' });
|
||
res.end(JSON.stringify({
|
||
success: false,
|
||
error: error.message
|
||
}));
|
||
return true;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 直接保存 Orchids 凭据(从 UI 表单提交)
|
||
*/
|
||
async function saveOrchidsCredentials(credentials) {
|
||
const fs = await import('fs');
|
||
const path = await import('path');
|
||
const { broadcastEvent } = await import('../services/ui-manager.js');
|
||
const { autoLinkProviderConfigs } = await import('../services/service-manager.js');
|
||
const { CONFIG } = await import('../core/config-manager.js');
|
||
|
||
try {
|
||
// 准备凭据数据
|
||
const credentialsData = {
|
||
cookies: credentials.cookies || '',
|
||
clerkSessionId: credentials.clerkSessionId || `sess_${Date.now()}`,
|
||
userId: credentials.userId || 'user_unknown',
|
||
workingDir: credentials.workingDir || 'E:\\path\\to\\default\\project',
|
||
expiresAt: credentials.expiresAt || new Date(Date.now() + 7 * 24 * 60 * 60 * 1000).toISOString(),
|
||
importedAt: new Date().toISOString()
|
||
};
|
||
|
||
// 生成文件路径: configs/orchids/{timestamp}_orchids_creds/{timestamp}_orchids_creds.json
|
||
// 与 importOrchidsToken 保持一致的目录结构
|
||
const timestamp = Date.now();
|
||
const folderName = `${timestamp}_orchids_creds`;
|
||
const targetDir = path.default.join(process.cwd(), 'configs', 'orchids', folderName);
|
||
await fs.promises.mkdir(targetDir, { recursive: true });
|
||
|
||
const filename = `${folderName}.json`;
|
||
const credPath = path.default.join(targetDir, filename);
|
||
await fs.promises.writeFile(credPath, JSON.stringify(credentialsData, null, 2));
|
||
|
||
const relativePath = path.default.relative(process.cwd(), credPath);
|
||
|
||
console.log(`[Orchids Import] Credentials saved to: ${relativePath}`);
|
||
|
||
// 广播事件
|
||
broadcastEvent('oauth_success', {
|
||
provider: 'claude-orchids-oauth',
|
||
relativePath: relativePath,
|
||
timestamp: new Date().toISOString()
|
||
});
|
||
|
||
// 自动关联新生成的凭据到 Pools
|
||
await autoLinkProviderConfigs(CONFIG);
|
||
|
||
return {
|
||
success: true,
|
||
path: relativePath,
|
||
sessionId: credentialsData.clerkSessionId,
|
||
userId: credentialsData.userId
|
||
};
|
||
|
||
} catch (error) {
|
||
console.error('[Orchids Import] Save credentials failed:', error);
|
||
return {
|
||
success: false,
|
||
error: error.message
|
||
};
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 解析 Orchids Cookie 字符串
|
||
* 从完整的 Cookie 字符串中提取 __client、__session 和 clerkSessionId
|
||
* @param {string} cookieString - 完整的 Cookie 字符串
|
||
* @returns {Object} 解析结果
|
||
*/
|
||
function parseOrchidsCookieString(cookieString) {
|
||
try {
|
||
// 提取 __client cookie
|
||
const clientMatch = cookieString.match(/__client=([^;]+)/);
|
||
if (!clientMatch) {
|
||
return { success: false, error: 'Cookie 中缺少 __client' };
|
||
}
|
||
const clientCookie = clientMatch[1].trim();
|
||
|
||
// 提取 __session cookie
|
||
const sessionMatch = cookieString.match(/__session=([^;]+)/);
|
||
if (!sessionMatch) {
|
||
return { success: false, error: 'Cookie 中缺少 __session' };
|
||
}
|
||
const sessionCookie = sessionMatch[1].trim();
|
||
|
||
// 从 __session JWT 中解析 clerkSessionId (sid) 和 userId (sub)
|
||
let clerkSessionId = null;
|
||
let userId = 'user_unknown';
|
||
|
||
try {
|
||
const sessionParts = sessionCookie.split('.');
|
||
if (sessionParts.length === 3) {
|
||
const payloadBase64 = sessionParts[1].replace(/-/g, '+').replace(/_/g, '/');
|
||
const payloadJson = Buffer.from(payloadBase64, 'base64').toString('utf-8');
|
||
const payload = JSON.parse(payloadJson);
|
||
|
||
if (payload.sid) {
|
||
clerkSessionId = payload.sid;
|
||
}
|
||
if (payload.sub) {
|
||
userId = payload.sub;
|
||
}
|
||
}
|
||
} catch (e) {
|
||
console.warn('[Orchids Import] Failed to parse __session JWT:', e.message);
|
||
}
|
||
|
||
// 如果无法从 __session 获取 sid,尝试从 __client 获取
|
||
if (!clerkSessionId) {
|
||
try {
|
||
const clientParts = clientCookie.split('.');
|
||
if (clientParts.length === 3) {
|
||
const payloadBase64 = clientParts[1].replace(/-/g, '+').replace(/_/g, '/');
|
||
const payloadJson = Buffer.from(payloadBase64, 'base64').toString('utf-8');
|
||
const payload = JSON.parse(payloadJson);
|
||
|
||
// 从 client_id 推断 session_id
|
||
if (payload.id && payload.id.startsWith('client_')) {
|
||
clerkSessionId = 'sess_' + payload.id.substring(7);
|
||
}
|
||
}
|
||
} catch (e) {
|
||
console.warn('[Orchids Import] Failed to parse __client JWT:', e.message);
|
||
}
|
||
}
|
||
|
||
if (!clerkSessionId) {
|
||
return { success: false, error: '无法从 Cookie 中提取 Session ID' };
|
||
}
|
||
|
||
// 构建 cookies 字符串(只保留 __client 和 __session)
|
||
const cookies = `__client=${clientCookie}; __session=${sessionCookie}`;
|
||
|
||
return {
|
||
success: true,
|
||
credentials: {
|
||
cookies: cookies,
|
||
clerkSessionId: clerkSessionId,
|
||
userId: userId,
|
||
expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000).toISOString()
|
||
}
|
||
};
|
||
|
||
} catch (error) {
|
||
return { success: false, error: `解析 Cookie 失败: ${error.message}` };
|
||
}
|
||
} |