AIClient-2-API/src/auth/oauth-handlers.js
hex2077 0974a5e83d refactor(auth): 使用 axios 替代 fetch 以支持代理配置
重构 OAuth 处理中的网络请求模块,将原生 fetch 替换为 axios,以正确处理代理配置。axios 提供了更完善的代理支持和错误处理机制,同时保持与原有 fetch API 的兼容性。

refactor(providers): 优化健康检查逻辑

- 提前返回未启用健康检查的情况
- 仅在开启自动刷新 token 时执行快速预检
- 使用 AbortController 替代 Promise.race 实现超时控制
- 优化错误消息和日志信息
2026-01-16 17:35:15 +08:00

2329 lines
87 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { OAuth2Client } from 'google-auth-library';
import http from 'http';
import fs from 'fs';
import path from 'path';
import os from 'os';
import crypto from 'crypto';
import open from 'open';
import axios from 'axios';
import { broadcastEvent } from '../services/ui-manager.js';
import { autoLinkProviderConfigs } from '../services/service-manager.js';
import { CONFIG } from '../core/config-manager.js';
import { getGoogleAuthProxyConfig, getProxyConfigForProvider } from '../utils/proxy-utils.js';
/**
* OAuth 提供商配置
*/
const OAUTH_PROVIDERS = {
'gemini-cli-oauth': {
clientId: '681255809395-oo8ft2oprdrnp9e3aqf6av3hmdib135j.apps.googleusercontent.com',
clientSecret: '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',
port: 8085,
credentialsDir: '.gemini',
credentialsFile: 'oauth_creds.json',
scope: ['https://www.googleapis.com/auth/cloud-platform'],
logPrefix: '[Gemini Auth]'
},
'gemini-antigravity': {
clientId: '1071006060591-tmhssin2h21lcre235vtolojh4g403ep.apps.googleusercontent.com',
clientSecret: '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',
port: 8086,
credentialsDir: '.antigravity',
credentialsFile: 'oauth_creds.json',
scope: ['https://www.googleapis.com/auth/cloud-platform'],
logPrefix: '[Antigravity Auth]'
}
};
/**
* 活动的服务器实例管理
*/
const activeServers = new Map();
/**
* 活动的轮询任务管理
*/
const activePollingTasks = new Map();
/**
* Qwen OAuth 配置
*/
const QWEN_OAUTH_CONFIG = {
clientId: 'f0304373b74a44d2b584a3fb70ca9e56',
scope: 'openid profile email model.completion',
deviceCodeEndpoint: 'https://chat.qwen.ai/api/v1/oauth2/device/code',
tokenEndpoint: 'https://chat.qwen.ai/api/v1/oauth2/token',
grantType: 'urn:ietf:params:oauth:grant-type:device_code',
credentialsDir: '.qwen',
credentialsFile: 'oauth_creds.json',
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]'
};
/**
* iFlow OAuth 配置
*/
const IFLOW_OAUTH_CONFIG = {
// OAuth 端点
tokenEndpoint: 'https://iflow.cn/oauth/token',
authorizeEndpoint: 'https://iflow.cn/oauth',
userInfoEndpoint: 'https://iflow.cn/api/oauth/getUserInfo',
successRedirectURL: 'https://iflow.cn/oauth/success',
// 客户端凭据
clientId: '10009311001',
clientSecret: '4Z3YjXycVsQvyGF1etiNlIBB4RsqSDtW',
// 本地回调端口
callbackPort: 8087,
// 凭据存储
credentialsDir: '.iflow',
credentialsFile: 'oauth_creds.json',
// 日志前缀
logPrefix: '[iFlow Auth]'
};
/**
* 活动的 iFlow 回调服务器管理
*/
const activeIFlowServers = new Map();
/**
* 活动的 Kiro 回调服务器管理
*/
const activeKiroServers = new Map();
/**
* 活动的 Kiro 轮询任务管理(用于 Builder ID Device Code
*/
const activeKiroPollingTasks = new Map();
/**
* 创建带代理支持的 fetch 请求
* 使用 axios 替代原生 fetch以正确支持代理配置
* @param {string} url - 请求 URL
* @param {Object} options - fetch 选项(兼容 fetch API 格式)
* @param {string} providerType - 提供商类型,用于获取代理配置
* @returns {Promise<Object>} 返回类似 fetch Response 的对象
*/
async function fetchWithProxy(url, options = {}, providerType) {
const proxyConfig = getProxyConfigForProvider(CONFIG, providerType);
// 构建 axios 配置
const axiosConfig = {
url,
method: options.method || 'GET',
headers: options.headers || {},
timeout: 30000, // 30 秒超时
};
// 处理请求体
if (options.body) {
axiosConfig.data = options.body;
}
// 配置代理
if (proxyConfig) {
axiosConfig.httpAgent = proxyConfig.httpAgent;
axiosConfig.httpsAgent = proxyConfig.httpsAgent;
axiosConfig.proxy = false; // 禁用 axios 内置代理,使用我们的 agent
console.log(`[OAuth] Using proxy for ${providerType}: ${CONFIG.PROXY_URL}`);
}
try {
const response = await axios(axiosConfig);
// 返回类似 fetch Response 的对象
return {
ok: response.status >= 200 && response.status < 300,
status: response.status,
statusText: response.statusText,
headers: response.headers,
json: async () => response.data,
text: async () => typeof response.data === 'string' ? response.data : JSON.stringify(response.data),
};
} catch (error) {
// 处理 axios 错误,转换为类似 fetch 的响应格式
if (error.response) {
// 服务器返回了错误状态码
return {
ok: false,
status: error.response.status,
statusText: error.response.statusText,
headers: error.response.headers,
json: async () => error.response.data,
text: async () => typeof error.response.data === 'string' ? error.response.data : JSON.stringify(error.response.data),
};
}
// 网络错误或其他错误
throw error;
}
}
/**
* 生成 HTML 响应页面
* @param {boolean} isSuccess - 是否成功
* @param {string} message - 显示消息
* @returns {string} HTML 内容
*/
function generateResponsePage(isSuccess, message) {
const title = isSuccess ? '授权成功!' : '授权失败';
return `<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>${title}</title>
</head>
<body>
<div class="container">
<h1>${title}</h1>
<p>${message}</p>
</div>
</body>
</html>`;
}
/**
* 关闭指定端口的活动服务器
* @param {number} port - 端口号
* @returns {Promise<void>}
*/
async function closeActiveServer(provider, port = null) {
// 1. 关闭该提供商之前的所有服务器
const existing = activeServers.get(provider);
if (existing) {
await new Promise((resolve) => {
existing.server.close(() => {
activeServers.delete(provider);
console.log(`[OAuth] 已关闭提供商 ${provider} 在端口 ${existing.port} 上的旧服务器`);
resolve();
});
});
}
// 2. 如果指定了端口,检查是否有其他提供商占用了该端口
if (port) {
for (const [p, info] of activeServers.entries()) {
if (info.port === port) {
await new Promise((resolve) => {
info.server.close(() => {
activeServers.delete(p);
console.log(`[OAuth] 已关闭端口 ${port} 上被占用(提供商: ${p})的旧服务器`);
resolve();
});
});
}
}
}
}
/**
* 创建 OAuth 回调服务器
* @param {Object} config - OAuth 提供商配置
* @param {string} redirectUri - 重定向 URI
* @param {OAuth2Client} authClient - OAuth2 客户端
* @param {string} credPath - 凭据保存路径
* @param {string} provider - 提供商标识
* @returns {Promise<http.Server>} HTTP 服务器实例
*/
async function createOAuthCallbackServer(config, redirectUri, authClient, credPath, provider, options = {}) {
const port = parseInt(options.port) || config.port;
// 先关闭该提供商之前可能运行的所有服务器,或该端口上的旧服务器
await closeActiveServer(provider, port);
return new Promise((resolve, reject) => {
const server = http.createServer(async (req, res) => {
try {
const url = new URL(req.url, redirectUri);
const code = url.searchParams.get('code');
const errorParam = url.searchParams.get('error');
if (code) {
console.log(`${config.logPrefix} 收到来自 Google 的成功回调: ${req.url}`);
try {
const { tokens } = await authClient.getToken(code);
let finalCredPath = credPath;
// 如果指定了保存到 configs 目录
if (options.saveToConfigs) {
const providerDir = options.providerDir;
const targetDir = path.join(process.cwd(), 'configs', providerDir);
await fs.promises.mkdir(targetDir, { recursive: true });
const timestamp = Date.now();
const filename = `${timestamp}_oauth_creds.json`;
finalCredPath = path.join(targetDir, filename);
}
await fs.promises.mkdir(path.dirname(finalCredPath), { recursive: true });
await fs.promises.writeFile(finalCredPath, JSON.stringify(tokens, null, 2));
console.log(`${config.logPrefix} 新令牌已接收并保存到文件: ${finalCredPath}`);
const relativePath = path.relative(process.cwd(), finalCredPath);
// 广播授权成功事件
broadcastEvent('oauth_success', {
provider: provider,
credPath: finalCredPath,
relativePath: relativePath,
timestamp: new Date().toISOString()
});
// 自动关联新生成的凭据到 Pools
await autoLinkProviderConfigs(CONFIG);
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
res.end(generateResponsePage(true, '您可以关闭此页面'));
} catch (tokenError) {
console.error(`${config.logPrefix} 获取令牌失败:`, tokenError);
res.writeHead(500, { 'Content-Type': 'text/html; charset=utf-8' });
res.end(generateResponsePage(false, `获取令牌失败: ${tokenError.message}`));
} finally {
server.close(() => {
activeServers.delete(provider);
});
}
} else if (errorParam) {
const errorMessage = `授权失败。Google 返回错误: ${errorParam}`;
console.error(`${config.logPrefix}`, errorMessage);
res.writeHead(400, { 'Content-Type': 'text/html; charset=utf-8' });
res.end(generateResponsePage(false, errorMessage));
server.close(() => {
activeServers.delete(provider);
});
} else {
console.log(`${config.logPrefix} 忽略无关请求: ${req.url}`);
res.writeHead(204);
res.end();
}
} catch (error) {
console.error(`${config.logPrefix} 处理回调时出错:`, error);
res.writeHead(500, { 'Content-Type': 'text/html; charset=utf-8' });
res.end(generateResponsePage(false, `服务器错误: ${error.message}`));
if (server.listening) {
server.close(() => {
activeServers.delete(provider);
});
}
}
});
server.on('error', (err) => {
if (err.code === 'EADDRINUSE') {
console.error(`${config.logPrefix} 端口 ${port} 已被占用`);
reject(new Error(`端口 ${port} 已被占用`));
} else {
console.error(`${config.logPrefix} 服务器错误:`, err);
reject(err);
}
});
const host = '0.0.0.0';
server.listen(port, host, () => {
console.log(`${config.logPrefix} OAuth 回调服务器已启动于 ${host}:${port}`);
activeServers.set(provider, { server, port });
resolve(server);
});
});
}
/**
* 处理 Google OAuth 授权(通用函数)
* @param {string} providerKey - 提供商键名
* @param {Object} currentConfig - 当前配置对象
* @param {Object} options - 额外选项
* @returns {Promise<Object>} 返回授权URL和相关信息
*/
async function handleGoogleOAuth(providerKey, currentConfig, options = {}) {
const config = OAUTH_PROVIDERS[providerKey];
if (!config) {
throw new Error(`未知的提供商: ${providerKey}`);
}
const port = parseInt(options.port) || config.port;
const host = 'localhost';
const redirectUri = `http://${host}:${port}`;
// 获取代理配置
const proxyConfig = getGoogleAuthProxyConfig(currentConfig, providerKey);
// 构建 OAuth2Client 选项
const oauth2Options = {
clientId: config.clientId,
clientSecret: config.clientSecret,
};
if (proxyConfig) {
oauth2Options.transporterOptions = proxyConfig;
console.log(`${config.logPrefix} Using proxy for OAuth token exchange`);
}
const authClient = new OAuth2Client(oauth2Options);
authClient.redirectUri = redirectUri;
const authUrl = authClient.generateAuthUrl({
access_type: 'offline',
prompt: 'select_account',
scope: config.scope
});
// 启动回调服务器
const credPath = path.join(os.homedir(), config.credentialsDir, config.credentialsFile);
try {
await createOAuthCallbackServer(config, redirectUri, authClient, credPath, providerKey, options);
} catch (error) {
throw new Error(`启动回调服务器失败: ${error.message}`);
}
return {
authUrl,
authInfo: {
provider: providerKey,
redirectUri: redirectUri,
port: port,
...options
}
};
}
/**
* 处理 Gemini CLI OAuth 授权
* @param {Object} currentConfig - 当前配置对象
* @param {Object} options - 额外选项
* @returns {Promise<Object>} 返回授权URL和相关信息
*/
export async function handleGeminiCliOAuth(currentConfig, options = {}) {
return handleGoogleOAuth('gemini-cli-oauth', currentConfig, options);
}
/**
* 处理 Gemini Antigravity OAuth 授权
* @param {Object} currentConfig - 当前配置对象
* @param {Object} options - 额外选项
* @returns {Promise<Object>} 返回授权URL和相关信息
*/
export async function handleGeminiAntigravityOAuth(currentConfig, options = {}) {
return handleGoogleOAuth('gemini-antigravity', currentConfig, options);
}
/**
* 生成 PKCE 代码验证器
* @returns {string} Base64URL 编码的随机字符串
*/
function generateCodeVerifier() {
return crypto.randomBytes(32).toString('base64url');
}
/**
* 生成 PKCE 代码挑战
* @param {string} codeVerifier - 代码验证器
* @returns {string} Base64URL 编码的 SHA256 哈希
*/
function generateCodeChallenge(codeVerifier) {
const hash = crypto.createHash('sha256');
hash.update(codeVerifier);
return hash.digest('base64url');
}
/**
* 停止活动的轮询任务
* @param {string} taskId - 任务标识符
*/
function stopPollingTask(taskId) {
const task = activePollingTasks.get(taskId);
if (task) {
task.shouldStop = true;
activePollingTasks.delete(taskId);
console.log(`${QWEN_OAUTH_CONFIG.logPrefix} 已停止轮询任务: ${taskId}`);
}
}
/**
* 轮询获取 Qwen OAuth 令牌
* @param {string} deviceCode - 设备代码
* @param {string} codeVerifier - PKCE 代码验证器
* @param {number} interval - 轮询间隔(秒)
* @param {number} expiresIn - 过期时间(秒)
* @param {string} taskId - 任务标识符
* @param {Object} options - 额外选项
* @returns {Promise<Object>} 返回令牌信息
*/
async function pollQwenToken(deviceCode, codeVerifier, interval = 5, expiresIn = 300, taskId = 'default', options = {}) {
let credPath = path.join(os.homedir(), QWEN_OAUTH_CONFIG.credentialsDir, QWEN_OAUTH_CONFIG.credentialsFile);
const maxAttempts = Math.floor(expiresIn / interval);
let attempts = 0;
// 创建任务控制对象
const taskControl = { shouldStop: false };
activePollingTasks.set(taskId, taskControl);
console.log(`${QWEN_OAUTH_CONFIG.logPrefix} 开始轮询令牌 [${taskId}],间隔 ${interval} 秒,最多尝试 ${maxAttempts}`);
const poll = async () => {
// 检查是否需要停止
if (taskControl.shouldStop) {
console.log(`${QWEN_OAUTH_CONFIG.logPrefix} 轮询任务 [${taskId}] 已被停止`);
throw new Error('轮询任务已被取消');
}
if (attempts >= maxAttempts) {
activePollingTasks.delete(taskId);
throw new Error('授权超时,请重新开始授权流程');
}
attempts++;
const bodyData = {
client_id: QWEN_OAUTH_CONFIG.clientId,
device_code: deviceCode,
grant_type: QWEN_OAUTH_CONFIG.grantType,
code_verifier: codeVerifier
};
const formBody = Object.entries(bodyData)
.map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`)
.join('&');
try {
const response = await fetchWithProxy(QWEN_OAUTH_CONFIG.tokenEndpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'Accept': 'application/json'
},
body: formBody
}, 'openai-qwen-oauth');
const data = await response.json();
if (response.ok && data.access_token) {
// 成功获取令牌
console.log(`${QWEN_OAUTH_CONFIG.logPrefix} 成功获取令牌 [${taskId}]`);
// 如果指定了保存到 configs 目录
if (options.saveToConfigs) {
const targetDir = path.join(process.cwd(), 'configs', options.providerDir);
await fs.promises.mkdir(targetDir, { recursive: true });
const timestamp = Date.now();
const filename = `${timestamp}_oauth_creds.json`;
credPath = path.join(targetDir, filename);
}
// 保存令牌到文件
await fs.promises.mkdir(path.dirname(credPath), { recursive: true });
await fs.promises.writeFile(credPath, JSON.stringify(data, null, 2));
console.log(`${QWEN_OAUTH_CONFIG.logPrefix} 令牌已保存到 ${credPath}`);
const relativePath = path.relative(process.cwd(), credPath);
// 清理任务
activePollingTasks.delete(taskId);
// 广播授权成功事件
broadcastEvent('oauth_success', {
provider: 'openai-qwen-oauth',
credPath: credPath,
relativePath: relativePath,
timestamp: new Date().toISOString()
});
// 自动关联新生成的凭据到 Pools
await autoLinkProviderConfigs(CONFIG);
return data;
}
// 检查错误类型
if (data.error === 'authorization_pending') {
// 用户尚未完成授权,继续轮询
console.log(`${QWEN_OAUTH_CONFIG.logPrefix} 等待用户授权 [${taskId}]... (第 ${attempts}/${maxAttempts} 次尝试)`);
await new Promise(resolve => setTimeout(resolve, interval * 1000));
return poll();
} else if (data.error === 'slow_down') {
// 需要降低轮询频率
console.log(`${QWEN_OAUTH_CONFIG.logPrefix} 降低轮询频率`);
await new Promise(resolve => setTimeout(resolve, (interval + 5) * 1000));
return poll();
} else if (data.error === 'expired_token') {
activePollingTasks.delete(taskId);
throw new Error('设备代码已过期,请重新开始授权流程');
} else if (data.error === 'access_denied') {
activePollingTasks.delete(taskId);
throw new Error('用户拒绝了授权请求');
} else {
activePollingTasks.delete(taskId);
throw new Error(`授权失败: ${data.error || '未知错误'}`);
}
} catch (error) {
if (error.message.includes('授权') || error.message.includes('过期') || error.message.includes('拒绝')) {
throw error;
}
console.error(`${QWEN_OAUTH_CONFIG.logPrefix} 轮询出错:`, error);
// 网络错误,继续重试
await new Promise(resolve => setTimeout(resolve, interval * 1000));
return poll();
}
};
return poll();
}
/**
* 处理 Qwen OAuth 授权(设备授权流程)
* @param {Object} currentConfig - 当前配置对象
* @param {Object} options - 额外选项
* @returns {Promise<Object>} 返回授权URL和相关信息
*/
export async function handleQwenOAuth(currentConfig, options = {}) {
const codeVerifier = generateCodeVerifier();
const codeChallenge = generateCodeChallenge(codeVerifier);
const bodyData = {
client_id: QWEN_OAUTH_CONFIG.clientId,
scope: QWEN_OAUTH_CONFIG.scope,
code_challenge: codeChallenge,
code_challenge_method: 'S256'
};
const formBody = Object.entries(bodyData)
.map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`)
.join('&');
try {
const response = await fetchWithProxy(QWEN_OAUTH_CONFIG.deviceCodeEndpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'Accept': 'application/json'
},
body: formBody
}, 'openai-qwen-oauth');
if (!response.ok) {
throw new Error(`Qwen OAuth请求失败: ${response.status} ${response.statusText}`);
}
const deviceAuth = await response.json();
if (!deviceAuth.device_code || !deviceAuth.verification_uri_complete) {
throw new Error('Qwen OAuth响应格式错误缺少必要字段');
}
// 启动后台轮询获取令牌
const interval = 5;
// const expiresIn = deviceAuth.expires_in || 1800;
const expiresIn = 300;
// 生成唯一的任务ID
const taskId = `qwen-${deviceAuth.device_code.substring(0, 8)}-${Date.now()}`;
// 先停止之前可能存在的所有 Qwen 轮询任务
for (const [existingTaskId] of activePollingTasks.entries()) {
if (existingTaskId.startsWith('qwen-')) {
stopPollingTask(existingTaskId);
}
}
// 不等待轮询完成,立即返回授权信息
pollQwenToken(deviceAuth.device_code, codeVerifier, interval, expiresIn, taskId, options)
.catch(error => {
console.error(`${QWEN_OAUTH_CONFIG.logPrefix} 轮询失败 [${taskId}]:`, error);
// 广播授权失败事件
broadcastEvent('oauth_error', {
provider: 'openai-qwen-oauth',
error: error.message,
timestamp: new Date().toISOString()
});
});
return {
authUrl: deviceAuth.verification_uri_complete,
authInfo: {
provider: 'openai-qwen-oauth',
deviceCode: deviceAuth.device_code,
userCode: deviceAuth.user_code,
verificationUri: deviceAuth.verification_uri,
verificationUriComplete: deviceAuth.verification_uri_complete,
expiresIn: expiresIn,
interval: interval,
codeVerifier: codeVerifier
}
};
} catch (error) {
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 || options.authMethod || 'google'; // 默认使用 Google同时支持 authMethod 参数
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');
// 启动本地回调服务器并获取端口
let handlerPort;
const providerKey = 'claude-kiro-oauth';
if (options.port) {
const port = parseInt(options.port);
await closeKiroServer(providerKey, port);
const server = await createKiroHttpCallbackServer(port, codeVerifier, state, options);
activeKiroServers.set(providerKey, { server, port });
handlerPort = port;
} else {
handlerPort = await startKiroCallbackServer(codeVerifier, state, options);
}
// 使用 HTTP localhost 作为 redirect_uri
const redirectUri = `http://127.0.0.1:${handlerPort}/oauth/callback`;
// 构建授权 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,
...options
}
};
}
/**
* Kiro Builder ID - Device Code Flow类似 Qwen OAuth 模式)
*/
async function handleKiroBuilderIDDeviceCode(currentConfig, options = {}) {
// 停止之前的轮询任务
for (const [existingTaskId] of activeKiroPollingTasks.entries()) {
if (existingTaskId.startsWith('kiro-')) {
stopKiroPollingTask(existingTaskId);
}
}
// 获取 Builder ID Start URL优先使用前端传入的值否则使用默认值
const builderIDStartURL = options.builderIDStartURL || KIRO_OAUTH_CONFIG.builderIDStartURL;
console.log(`${KIRO_OAUTH_CONFIG.logPrefix} Using Builder ID Start URL: ${builderIDStartURL}`);
// 1. 注册 OIDC 客户端
const regResponse = await fetchWithProxy(`${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']
})
}, 'claude-kiro-oauth');
if (!regResponse.ok) {
throw new Error(`Kiro OAuth 客户端注册失败: ${regResponse.status}`);
}
const regData = await regResponse.json();
// 2. 启动设备授权
const authResponse = await fetchWithProxy(`${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: builderIDStartURL
})
}, 'claude-kiro-oauth');
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()}`;
// 异步轮询
pollKiroBuilderIDToken(
regData.clientId,
regData.clientSecret,
deviceAuth.deviceCode,
5,
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,
...options
}
};
}
/**
* 轮询获取 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 fetchWithProxy(`${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'
})
}, 'claude-kiro-oauth');
const data = await response.json();
if (response.ok && data.accessToken) {
console.log(`${KIRO_OAUTH_CONFIG.logPrefix} 成功获取令牌 [${taskId}]`);
// 保存令牌(符合现有规范)
if (options.saveToConfigs) {
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 });
credPath = path.join(targetDir, `${folderName}.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()
});
// 自动关联新生成的凭据到 Pools
await autoLinkProviderConfigs(CONFIG);
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('claude-kiro-oauth', { server, port });
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(provider, port = null) {
const existing = activeKiroServers.get(provider);
if (existing) {
await new Promise((resolve) => {
existing.server.close(() => {
activeKiroServers.delete(provider);
console.log(`${KIRO_OAUTH_CONFIG.logPrefix} 已关闭提供商 ${provider} 在端口 ${existing.port} 上的旧服务器`);
resolve();
});
});
}
if (port) {
for (const [p, info] of activeKiroServers.entries()) {
if (info.port === port) {
await new Promise((resolve) => {
info.server.close(() => {
activeKiroServers.delete(p);
console.log(`${KIRO_OAUTH_CONFIG.logPrefix} 已关闭端口 ${port} 上的旧服务器`);
resolve();
});
});
}
}
}
}
/**
* 创建 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 fetchWithProxy(`${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
})
}, 'claude-kiro-oauth');
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 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 });
credPath = path.join(targetDir, `${folderName}.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()
});
// 自动关联新生成的凭据到 Pools
await autoLinkProviderConfigs(CONFIG);
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
res.end(generateResponsePage(true, '授权成功!您可以关闭此页面'));
// 关闭服务器
server.close(() => {
activeKiroServers.delete('claude-kiro-oauth');
});
} 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('claude-kiro-oauth');
});
}
}, KIRO_OAUTH_CONFIG.authTimeout);
});
}
/**
* 生成 iFlow 授权链接
* @param {string} state - 状态参数
* @param {number} port - 回调端口
* @returns {Object} 包含 authUrl 和 redirectUri
*/
function generateIFlowAuthorizationURL(state, port) {
const redirectUri = `http://localhost:${port}/oauth2callback`;
const params = new URLSearchParams({
loginMethod: 'phone',
type: 'phone',
redirect: redirectUri,
state: state,
client_id: IFLOW_OAUTH_CONFIG.clientId
});
const authUrl = `${IFLOW_OAUTH_CONFIG.authorizeEndpoint}?${params.toString()}`;
return { authUrl, redirectUri };
}
/**
* 交换授权码获取 iFlow 令牌
* @param {string} code - 授权码
* @param {string} redirectUri - 重定向 URI
* @returns {Promise<Object>} 令牌数据
*/
async function exchangeIFlowCodeForTokens(code, redirectUri) {
const form = new URLSearchParams({
grant_type: 'authorization_code',
code: code,
redirect_uri: redirectUri,
client_id: IFLOW_OAUTH_CONFIG.clientId,
client_secret: IFLOW_OAUTH_CONFIG.clientSecret
});
// 生成 Basic Auth 头
const basicAuth = Buffer.from(`${IFLOW_OAUTH_CONFIG.clientId}:${IFLOW_OAUTH_CONFIG.clientSecret}`).toString('base64');
const response = await fetchWithProxy(IFLOW_OAUTH_CONFIG.tokenEndpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'Accept': 'application/json',
'Authorization': `Basic ${basicAuth}`
},
body: form.toString()
}, 'openai-iflow');
if (!response.ok) {
const errorText = await response.text();
throw new Error(`iFlow token exchange failed: ${response.status} ${errorText}`);
}
const tokenData = await response.json();
if (!tokenData.access_token) {
throw new Error('iFlow token: missing access token in response');
}
return {
accessToken: tokenData.access_token,
refreshToken: tokenData.refresh_token,
tokenType: tokenData.token_type,
scope: tokenData.scope,
expiresIn: tokenData.expires_in,
expiresAt: new Date(Date.now() + tokenData.expires_in * 1000).toISOString()
};
}
/**
* 获取 iFlow 用户信息(包含 API Key
* @param {string} accessToken - 访问令牌
* @returns {Promise<Object>} 用户信息
*/
async function fetchIFlowUserInfo(accessToken) {
if (!accessToken || accessToken.trim() === '') {
throw new Error('iFlow api key: access token is empty');
}
const endpoint = `${IFLOW_OAUTH_CONFIG.userInfoEndpoint}?accessToken=${encodeURIComponent(accessToken)}`;
const response = await fetchWithProxy(endpoint, {
method: 'GET',
headers: {
'Accept': 'application/json'
}
}, 'openai-iflow');
if (!response.ok) {
const errorText = await response.text();
throw new Error(`iFlow user info failed: ${response.status} ${errorText}`);
}
const result = await response.json();
if (!result.success) {
throw new Error('iFlow api key: request not successful');
}
if (!result.data || !result.data.apiKey) {
throw new Error('iFlow api key: missing api key in response');
}
// 获取邮箱或手机号作为账户标识
let email = (result.data.email || '').trim();
if (!email) {
email = (result.data.phone || '').trim();
}
if (!email) {
throw new Error('iFlow token: missing account email/phone in user info');
}
return {
apiKey: result.data.apiKey,
email: email,
phone: result.data.phone || ''
};
}
/**
* 关闭 iFlow 服务器
* @param {string} provider - 提供商标识
* @param {number} port - 端口号(可选)
*/
async function closeIFlowServer(provider, port = null) {
const existing = activeIFlowServers.get(provider);
if (existing) {
await new Promise((resolve) => {
existing.server.close(() => {
activeIFlowServers.delete(provider);
console.log(`${IFLOW_OAUTH_CONFIG.logPrefix} 已关闭提供商 ${provider} 在端口 ${existing.port} 上的旧服务器`);
resolve();
});
});
}
if (port) {
for (const [p, info] of activeIFlowServers.entries()) {
if (info.port === port) {
await new Promise((resolve) => {
info.server.close(() => {
activeIFlowServers.delete(p);
console.log(`${IFLOW_OAUTH_CONFIG.logPrefix} 已关闭端口 ${port} 上的旧服务器`);
resolve();
});
});
}
}
}
}
/**
* 创建 iFlow OAuth 回调服务器
* @param {number} port - 端口号
* @param {string} redirectUri - 重定向 URI
* @param {string} expectedState - 预期的 state 参数
* @param {Object} options - 额外选项
* @returns {Promise<http.Server>} HTTP 服务器实例
*/
function createIFlowCallbackServer(port, redirectUri, expectedState, options = {}) {
return new Promise((resolve, reject) => {
const server = http.createServer(async (req, res) => {
try {
const url = new URL(req.url, `http://localhost:${port}`);
if (url.pathname === '/oauth2callback') {
const code = url.searchParams.get('code');
const state = url.searchParams.get('state');
const errorParam = url.searchParams.get('error');
if (errorParam) {
console.error(`${IFLOW_OAUTH_CONFIG.logPrefix} 授权失败: ${errorParam}`);
res.writeHead(400, { 'Content-Type': 'text/html; charset=utf-8' });
res.end(generateResponsePage(false, `授权失败: ${errorParam}`));
server.close(() => {
activeIFlowServers.delete('openai-iflow');
});
return;
}
if (state !== expectedState) {
console.error(`${IFLOW_OAUTH_CONFIG.logPrefix} State 验证失败`);
res.writeHead(400, { 'Content-Type': 'text/html; charset=utf-8' });
res.end(generateResponsePage(false, 'State 验证失败'));
server.close(() => {
activeIFlowServers.delete('openai-iflow');
});
return;
}
if (!code) {
console.error(`${IFLOW_OAUTH_CONFIG.logPrefix} 缺少授权码`);
res.writeHead(400, { 'Content-Type': 'text/html; charset=utf-8' });
res.end(generateResponsePage(false, '缺少授权码'));
server.close(() => {
activeIFlowServers.delete('openai-iflow');
});
return;
}
console.log(`${IFLOW_OAUTH_CONFIG.logPrefix} 收到授权回调,正在交换令牌...`);
try {
// 1. 交换授权码获取令牌
const tokenData = await exchangeIFlowCodeForTokens(code, redirectUri);
console.log(`${IFLOW_OAUTH_CONFIG.logPrefix} 令牌交换成功`);
// 2. 获取用户信息(包含 API Key
const userInfo = await fetchIFlowUserInfo(tokenData.accessToken);
console.log(`${IFLOW_OAUTH_CONFIG.logPrefix} 用户信息获取成功: ${userInfo.email}`);
// 3. 组合完整的凭据数据
const credentialsData = {
access_token: tokenData.accessToken,
refresh_token: tokenData.refreshToken,
expiry_date: new Date(tokenData.expiresAt).getTime(),
token_type: tokenData.tokenType,
scope: tokenData.scope,
apiKey: userInfo.apiKey
};
// 4. 保存凭据
let credPath = path.join(os.homedir(), IFLOW_OAUTH_CONFIG.credentialsDir, IFLOW_OAUTH_CONFIG.credentialsFile);
if (options.saveToConfigs) {
const providerDir = options.providerDir || 'iflow';
const targetDir = path.join(process.cwd(), 'configs', providerDir);
await fs.promises.mkdir(targetDir, { recursive: true });
const timestamp = Date.now();
const filename = `${timestamp}_oauth_creds.json`;
credPath = path.join(targetDir, filename);
}
await fs.promises.mkdir(path.dirname(credPath), { recursive: true });
await fs.promises.writeFile(credPath, JSON.stringify(credentialsData, null, 2));
console.log(`${IFLOW_OAUTH_CONFIG.logPrefix} 凭据已保存: ${credPath}`);
const relativePath = path.relative(process.cwd(), credPath);
// 5. 广播授权成功事件
broadcastEvent('oauth_success', {
provider: 'openai-iflow',
credPath: credPath,
relativePath: relativePath,
email: userInfo.email,
timestamp: new Date().toISOString()
});
// 6. 自动关联新生成的凭据到 Pools
await autoLinkProviderConfigs(CONFIG);
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
res.end(generateResponsePage(true, `授权成功!账户: ${userInfo.email},您可以关闭此页面`));
} catch (tokenError) {
console.error(`${IFLOW_OAUTH_CONFIG.logPrefix} 令牌处理失败:`, tokenError);
res.writeHead(500, { 'Content-Type': 'text/html; charset=utf-8' });
res.end(generateResponsePage(false, `令牌处理失败: ${tokenError.message}`));
} finally {
server.close(() => {
activeIFlowServers.delete('openai-iflow');
});
}
} else {
// 忽略其他请求
res.writeHead(204);
res.end();
}
} catch (error) {
console.error(`${IFLOW_OAUTH_CONFIG.logPrefix} 处理回调出错:`, error);
res.writeHead(500, { 'Content-Type': 'text/html; charset=utf-8' });
res.end(generateResponsePage(false, `服务器错误: ${error.message}`));
if (server.listening) {
server.close(() => {
activeIFlowServers.delete('openai-iflow');
});
}
}
});
server.on('error', (err) => {
if (err.code === 'EADDRINUSE') {
console.error(`${IFLOW_OAUTH_CONFIG.logPrefix} 端口 ${port} 已被占用`);
reject(new Error(`端口 ${port} 已被占用`));
} else {
console.error(`${IFLOW_OAUTH_CONFIG.logPrefix} 服务器错误:`, err);
reject(err);
}
});
const host = '0.0.0.0';
server.listen(port, host, () => {
console.log(`${IFLOW_OAUTH_CONFIG.logPrefix} OAuth 回调服务器已启动于 ${host}:${port}`);
resolve(server);
});
// 10 分钟超时自动关闭
setTimeout(() => {
if (server.listening) {
console.log(`${IFLOW_OAUTH_CONFIG.logPrefix} 回调服务器超时,自动关闭`);
server.close(() => {
activeIFlowServers.delete('openai-iflow');
});
}
}, 10 * 60 * 1000);
});
}
/**
* 处理 iFlow OAuth 授权
* @param {Object} currentConfig - 当前配置对象
* @param {Object} options - 额外选项
* - port: 自定义端口号
* - saveToConfigs: 是否保存到 configs 目录
* - providerDir: 提供商目录名
* @returns {Promise<Object>} 返回授权URL和相关信息
*/
export async function handleIFlowOAuth(currentConfig, options = {}) {
const port = parseInt(options.port) || IFLOW_OAUTH_CONFIG.callbackPort;
const providerKey = 'openai-iflow';
// 生成 state 参数
const state = crypto.randomBytes(16).toString('base64url');
// 生成授权链接
const { authUrl, redirectUri } = generateIFlowAuthorizationURL(state, port);
console.log(`${IFLOW_OAUTH_CONFIG.logPrefix} 生成授权链接: ${authUrl}`);
// 关闭之前可能存在的服务器
await closeIFlowServer(providerKey, port);
// 启动回调服务器
try {
const server = await createIFlowCallbackServer(port, redirectUri, state, options);
activeIFlowServers.set(providerKey, { server, port });
} catch (error) {
throw new Error(`启动 iFlow 回调服务器失败: ${error.message}`);
}
return {
authUrl,
authInfo: {
provider: 'openai-iflow',
redirectUri: redirectUri,
callbackPort: port,
state: state,
...options
}
};
}
/**
* 使用 refresh_token 刷新 iFlow 令牌
* @param {string} refreshToken - 刷新令牌
* @returns {Promise<Object>} 新的令牌数据
*/
export async function refreshIFlowTokens(refreshToken) {
const form = new URLSearchParams({
grant_type: 'refresh_token',
refresh_token: refreshToken,
client_id: IFLOW_OAUTH_CONFIG.clientId,
client_secret: IFLOW_OAUTH_CONFIG.clientSecret
});
// 生成 Basic Auth 头
const basicAuth = Buffer.from(`${IFLOW_OAUTH_CONFIG.clientId}:${IFLOW_OAUTH_CONFIG.clientSecret}`).toString('base64');
const response = await fetchWithProxy(IFLOW_OAUTH_CONFIG.tokenEndpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'Accept': 'application/json',
'Authorization': `Basic ${basicAuth}`
},
body: form.toString()
}, 'openai-iflow');
if (!response.ok) {
const errorText = await response.text();
throw new Error(`iFlow token refresh failed: ${response.status} ${errorText}`);
}
const tokenData = await response.json();
if (!tokenData.access_token) {
throw new Error('iFlow token refresh: missing access token in response');
}
// 获取用户信息以更新 API Key
const userInfo = await fetchIFlowUserInfo(tokenData.access_token);
return {
access_token: tokenData.access_token,
refresh_token: tokenData.refresh_token,
expiry_date: Date.now() + tokenData.expires_in * 1000,
token_type: tokenData.token_type,
scope: tokenData.scope,
apiKey: userInfo.apiKey
};
}
/**
* Kiro Token 刷新常量
*/
const KIRO_REFRESH_CONSTANTS = {
REFRESH_URL: 'https://prod.{{region}}.auth.desktop.kiro.dev/refreshToken',
REFRESH_IDC_URL: 'https://oidc.{{region}}.amazonaws.com/token',
CONTENT_TYPE_JSON: 'application/json',
AUTH_METHOD_SOCIAL: 'social',
DEFAULT_PROVIDER: 'Google',
REQUEST_TIMEOUT: 30000,
DEFAULT_REGION: 'us-east-1'
};
/**
* 通过 refreshToken 获取 accessToken
* @param {string} refreshToken - Kiro 的 refresh token
* @param {string} region - AWS 区域 (默认: us-east-1)
* @returns {Promise<Object>} 包含 accessToken 等信息的对象
*/
async function refreshKiroToken(refreshToken, region = KIRO_REFRESH_CONSTANTS.DEFAULT_REGION) {
const refreshUrl = KIRO_REFRESH_CONSTANTS.REFRESH_URL.replace('{{region}}', region);
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), KIRO_REFRESH_CONSTANTS.REQUEST_TIMEOUT);
try {
const response = await fetchWithProxy(refreshUrl, {
method: 'POST',
headers: {
'Content-Type': KIRO_REFRESH_CONSTANTS.CONTENT_TYPE_JSON
},
body: JSON.stringify({ refreshToken }),
signal: controller.signal
}, 'claude-kiro-oauth');
clearTimeout(timeoutId);
if (!response.ok) {
const errorText = await response.text();
throw new Error(`HTTP ${response.status}: ${errorText}`);
}
const data = await response.json();
if (!data.accessToken) {
throw new Error('Invalid refresh response: Missing accessToken');
}
const expiresIn = data.expiresIn || 3600;
const expiresAt = new Date(Date.now() + expiresIn * 1000).toISOString();
return {
accessToken: data.accessToken,
refreshToken: data.refreshToken || refreshToken,
profileArn: data.profileArn || '',
expiresAt: expiresAt,
authMethod: KIRO_REFRESH_CONSTANTS.AUTH_METHOD_SOCIAL,
provider: KIRO_REFRESH_CONSTANTS.DEFAULT_PROVIDER,
region: region
};
} catch (error) {
clearTimeout(timeoutId);
if (error.name === 'AbortError') {
throw new Error('Request timeout');
}
throw error;
}
}
/**
* 检查 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, 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();
if (!refreshToken) {
results.details.push({
index: i + 1,
success: false,
error: 'Empty token'
});
results.failed++;
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...`);
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}`);
results.details.push({
index: i + 1,
success: true,
path: relativePath,
expiresAt: tokenData.expiresAt
});
results.success++;
} catch (error) {
console.error(`${KIRO_OAUTH_CONFIG.logPrefix} Token ${i + 1} 刷新失败:`, error.message);
results.details.push({
index: i + 1,
success: false,
error: error.message
});
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;
}
/**
* 批量导入 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 fetchWithProxy(refreshUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
refreshToken: credentials.refreshToken,
clientId: credentials.clientId,
clientSecret: credentials.clientSecret,
grantType: 'refresh_token'
})
}, 'claude-kiro-oauth');
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
};
}
}
// ============================================================================
// Orchids OAuth 配置和处理函数
// ============================================================================
/**
* Orchids OAuth 配置
*/
const ORCHIDS_OAUTH_CONFIG = {
// Clerk Token 端点
clerkTokenEndpoint: 'https://clerk.orchids.app/v1/client/sessions/{sessionId}/tokens',
clerkJsVersion: '5.114.0',
// 凭据存储
credentialsDir: 'orchids',
credentialsFile: 'orchids_creds.json',
// 日志前缀
logPrefix: '[Orchids Auth]'
};
/**
* 解析 Orchids 凭据字符串(简化版)
* 只需要 __client JWT 即可,其他参数通过 Clerk API 自动获取
*
* 支持的格式:
* 1. 纯 JWT 字符串: "eyJhbGciOiJSUzI1NiJ9..." (从 payload 中提取 rotating_token)
* 2. __client=xxx 格式: "__client=eyJhbGciOiJSUzI1NiJ9..."
* 3. 完整 Cookies 格式(兼容旧版): "__client=xxx; __session=xxx"
* 4. JWT|xxx 格式(兼容旧版)
*
* @param {string} inputString - 输入字符串
* @returns {Object} 解析后的凭据数据
*/
function parseOrchidsCredentials(inputString) {
if (!inputString || typeof inputString !== 'string') {
throw new Error('Invalid input string');
}
const trimmedInput = inputString.trim();
// 格式1: 纯 JWT 字符串(三段式,以点分隔)
if (trimmedInput.split('.').length === 3 && !trimmedInput.includes('=') && !trimmedInput.includes('|')) {
console.log('[Orchids Auth] Detected pure JWT format');
// 尝试从 JWT payload 中提取 rotating_token
let rotatingToken = null;
try {
const parts = trimmedInput.split('.');
if (parts.length === 3) {
// 解码 JWT payload (Base64URL -> Base64 -> JSON)
let payloadBase64 = parts[1].replace(/-/g, '+').replace(/_/g, '/');
// 添加 padding
while (payloadBase64.length % 4) {
payloadBase64 += '=';
}
const payloadJson = Buffer.from(payloadBase64, 'base64').toString('utf8');
const payload = JSON.parse(payloadJson);
if (payload.rotating_token) {
rotatingToken = payload.rotating_token;
console.log('[Orchids Auth] Extracted rotating_token from JWT payload');
}
}
} catch (e) {
console.warn('[Orchids Auth] Failed to extract rotating_token from JWT payload:', e.message);
}
return {
type: 'jwt',
clientJwt: trimmedInput,
rotatingToken: rotatingToken
};
}
// 格式2: __client=xxx 格式(可能包含或不包含 __session
if (trimmedInput.includes('__client=')) {
const clientMatch = trimmedInput.match(/__client=([^;]+)/);
if (clientMatch) {
const clientValue = clientMatch[1].trim();
// 处理可能的 | 分隔符(如 JWT|rotating_token
let jwtPart = clientValue;
let rotatingToken = null;
if (clientValue.includes('|')) {
const parts = clientValue.split('|');
jwtPart = parts[0];
rotatingToken = parts[1] || null;
}
if (jwtPart.split('.').length === 3) {
console.log('[Orchids Auth] Detected __client cookie format');
return {
type: 'jwt',
clientJwt: jwtPart,
rotatingToken: rotatingToken
};
}
}
throw new Error('Invalid __client value. Expected a valid JWT.');
}
// 格式3: JWT|rotating_token 格式
if (trimmedInput.includes('|')) {
const parts = trimmedInput.split('|');
if (parts.length >= 1) {
const jwtPart = parts[0].trim();
const rotatingToken = parts.length >= 2 ? parts[1].trim() : null;
if (jwtPart.split('.').length === 3) {
console.log('[Orchids Auth] Detected JWT|rotating_token format');
return {
type: 'jwt',
clientJwt: jwtPart,
rotatingToken: rotatingToken
};
}
}
}
throw new Error('Invalid format. Please provide the __client cookie value (JWT format). Example: eyJhbGciOiJSUzI1NiJ9...');
}
/**
* 解析 Orchids JWT Token 字符串 (保留用于向后兼容)
* @deprecated 请使用 parseOrchidsCredentials
* 格式: JWT|rotating_token
* JWT 包含 id (client_id) 和 rotating_token
* @param {string} tokenString - 完整的 token 字符串
* @returns {Object} 解析后的 token 数据
*/
function parseOrchidsToken(tokenString) {
const result = parseOrchidsCredentials(tokenString);
if (result.type === 'legacy') {
return {
clientId: result.clientId,
rotatingToken: result.rotatingToken,
jwt: result.jwt,
rawPayload: result.rawPayload
};
}
// 对于新格式,返回兼容的结构
return {
clientId: null,
rotatingToken: result.clientValue,
jwt: null,
rawPayload: null
};
}
/**
* 从 Clerk 获取 session token
* @param {string} sessionId - Clerk session ID
* @param {string} cookies - Cookie 字符串
* @returns {Promise<string>} JWT token
*/
async function getClerkSessionToken(sessionId, cookies) {
const tokenUrl = ORCHIDS_OAUTH_CONFIG.clerkTokenEndpoint
.replace('{sessionId}', sessionId) +
`?_clerk_js_version=${ORCHIDS_OAUTH_CONFIG.clerkJsVersion}`;
const response = await fetch(tokenUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'Cookie': cookies,
'Origin': 'https://www.orchids.app'
}
});
if (!response.ok) {
const errorText = await response.text();
throw new Error(`Clerk token request failed: ${response.status} ${errorText}`);
}
const data = await response.json();
return data.jwt;
}
/**
* 导入 Orchids 凭据并生成凭据文件(简化版)
* 只需要 __client JWT其他参数在运行时通过 Clerk API 自动获取
*
* @param {string} inputString - __client JWT 字符串
* @param {Object} options - 额外选项
* - workingDir: 默认工作目录
* @returns {Promise<Object>} 导入结果
*/
export async function importOrchidsToken(inputString, options = {}) {
try {
console.log(`${ORCHIDS_OAUTH_CONFIG.logPrefix} Parsing Orchids credentials (simplified)...`);
// 解析凭据 - 只提取 clientJwt
const credData = parseOrchidsCredentials(inputString);
if (!credData.clientJwt) {
throw new Error('Failed to extract clientJwt from input');
}
// 凭据数据 - 保存 clientJwt 和可选的 rotatingToken
const credentialsData = {
// 核心字段__client JWT必需的凭据
clientJwt: credData.clientJwt,
// 导入时间
importedAt: new Date().toISOString()
};
// 如果存在 rotatingToken也保存它可选备用
if (credData.rotatingToken) {
credentialsData.rotatingToken = credData.rotatingToken;
console.log(`${ORCHIDS_OAUTH_CONFIG.logPrefix} rotatingToken also saved for future use.`);
}
// 生成文件路径: configs/orchids/{timestamp}_orchids_creds/{timestamp}_orchids_creds.json
const timestamp = Date.now();
const folderName = `${timestamp}_orchids_creds`;
const targetDir = path.join(process.cwd(), 'configs', ORCHIDS_OAUTH_CONFIG.credentialsDir, folderName);
await fs.promises.mkdir(targetDir, { recursive: true });
const filename = `${folderName}.json`;
const credPath = path.join(targetDir, filename);
await fs.promises.writeFile(credPath, JSON.stringify(credentialsData, null, 2));
const relativePath = path.relative(process.cwd(), credPath);
console.log(`${ORCHIDS_OAUTH_CONFIG.logPrefix} Credentials saved to: ${relativePath}`);
console.log(`${ORCHIDS_OAUTH_CONFIG.logPrefix} Only clientJwt is stored. Session info will be fetched at runtime.`);
// 广播事件
broadcastEvent('oauth_success', {
provider: 'claude-orchids-oauth',
relativePath: relativePath,
timestamp: new Date().toISOString()
});
// 自动关联新生成的凭据到 Pools
await autoLinkProviderConfigs(CONFIG);
return {
success: true,
path: relativePath,
message: 'Credentials imported successfully. Session info will be fetched at runtime via Clerk API.'
};
} catch (error) {
console.error(`${ORCHIDS_OAUTH_CONFIG.logPrefix} Token import failed:`, error);
return {
success: false,
error: error.message
};
}
}
/**
* 处理 Orchids OAuth手动导入模式 - 简化版)
* 只需要 __client JWT其他参数自动获取
* @param {Object} currentConfig - 当前配置对象
* @param {Object} options - 额外选项
* @returns {Promise<Object>} 返回导入说明
*/
export async function handleOrchidsOAuth(currentConfig, options = {}) {
// Orchids 使用简化的手动导入模式
// 只需要 __client cookie 的值
return {
authUrl: null,
authInfo: {
provider: 'claude-orchids-oauth',
method: 'manual-import',
instructions: [
'1. 登录 Orchids 平台 (https://orchids.app)',
'2. 打开浏览器开发者工具 (F12)',
'3. 切换到 Application > Cookies > https://orchids.app',
'4. 找到 __client 并复制其值(一个长的 JWT 字符串)',
'5. 使用 "导入 Token" 功能粘贴该值'
],
tokenFormat: 'eyJhbGciOiJSUzI1NiJ9...',
example: 'eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6ImNsaWVudF8uLi4',
note: '只需要 __client 的值即可sessionId 等参数会自动获取'
}
};
}