diff --git a/.gitignore b/.gitignore index 80a7842..e644c04 100644 --- a/.gitignore +++ b/.gitignore @@ -12,4 +12,8 @@ usage-cache.json *_oauth_creds.json *-auth-token.json api-potluck-keys.json -api-potluck-data.json \ No newline at end of file +api-potluck-data.json +# Orchids credentials +configs/orchids/*_orchids_creds/ +configs/orchids/*.json +!configs/orchids/*.example diff --git a/configs/provider_pools.json.example b/configs/provider_pools.json.example index 98e5643..f1230ab 100644 --- a/configs/provider_pools.json.example +++ b/configs/provider_pools.json.example @@ -135,6 +135,21 @@ "lastErrorTime": null } ], + "claude-orchids-oauth": [ + { + "customName": "Orchids节点1", + "ORCHIDS_CREDS_FILE_PATH": "./configs/orchids/orchids_creds.json", + "uuid": "xxx-xxx-xxx", + "checkModelName": "claude-sonnet-4-5", + "checkHealth": false, + "isHealthy": true, + "isDisabled": false, + "lastUsed": null, + "usageCount": 0, + "errorCount": 0, + "lastErrorTime": null + } + ], "openai-qwen-oauth": [ { "customName": "Qwen OAuth节点", diff --git a/package-lock.json b/package-lock.json index 3d678ab..bfecc3e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,7 +18,8 @@ "open": "^10.2.0", "socks-proxy-agent": "^8.0.5", "undici": "^7.12.0", - "uuid": "^11.1.0" + "uuid": "^11.1.0", + "ws": "^8.19.0" }, "devDependencies": { "@babel/preset-env": "^7.28.0", @@ -100,6 +101,7 @@ "integrity": "sha512-UlLAnTPrFdNGoFtbSXwcGFQBtQZJCNjaN6hQNP3UPvuNXT1i82N26KL3dZeIpNalWywr9IuQuncaAfUaS1g6sQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.27.1", @@ -2958,6 +2960,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "caniuse-lite": "^1.0.30001726", "electron-to-chromium": "^1.5.173", @@ -6590,6 +6593,27 @@ "node": "^12.13.0 || ^14.15.0 || >=16.0.0" } }, + "node_modules/ws": { + "version": "8.19.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz", + "integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/wsl-utils": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/wsl-utils/-/wsl-utils-0.1.0.tgz", diff --git a/package.json b/package.json index 1d8c22d..a967544 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,8 @@ "open": "^10.2.0", "socks-proxy-agent": "^8.0.5", "undici": "^7.12.0", - "uuid": "^11.1.0" + "uuid": "^11.1.0", + "ws": "^8.19.0" }, "devDependencies": { "@babel/preset-env": "^7.28.0", diff --git a/src/auth/oauth-handlers.js b/src/auth/oauth-handlers.js index 051df54..c4a241f 100644 --- a/src/auth/oauth-handlers.js +++ b/src/auth/oauth-handlers.js @@ -2001,3 +2001,282 @@ export async function importAwsCredentials(credentials, skipDuplicateCheck = fal } } +// ============================================================================ +// 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} 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} 导入结果 + */ +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} 返回导入说明 + */ +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 等参数会自动获取' + } + }; +} + diff --git a/src/providers/adapter.js b/src/providers/adapter.js index 44afdd9..d4cb0a7 100644 --- a/src/providers/adapter.js +++ b/src/providers/adapter.js @@ -4,6 +4,7 @@ import { AntigravityApiService } from './gemini/antigravity-core.js'; // 导入A import { OpenAIApiService } from './openai/openai-core.js'; // 导入OpenAIApiService import { ClaudeApiService } from './claude/claude-core.js'; // 导入ClaudeApiService import { KiroApiService } from './claude/claude-kiro.js'; // 导入KiroApiService +import { OrchidsApiService } from './claude/claude-orchids.js'; // 导入OrchidsApiService import { QwenApiService } from './openai/qwen-core.js'; // 导入QwenApiService import { IFlowApiService } from './openai/iflow-core.js'; // 导入IFlowApiService import { MODEL_PROVIDER } from '../utils/common.js'; // 导入 MODEL_PROVIDER @@ -318,6 +319,50 @@ export class KiroApiServiceAdapter extends ApiServiceAdapter { } } +// Orchids API 服务适配器 +export class OrchidsApiServiceAdapter extends ApiServiceAdapter { + constructor(config) { + super(); + this.orchidsApiService = new OrchidsApiService(config); + } + + async generateContent(model, requestBody) { + if (!this.orchidsApiService.isInitialized) { + await this.orchidsApiService.initialize(); + } + return this.orchidsApiService.generateContent(model, requestBody); + } + + async *generateContentStream(model, requestBody) { + if (!this.orchidsApiService.isInitialized) { + await this.orchidsApiService.initialize(); + } + yield* this.orchidsApiService.generateContentStream(model, requestBody); + } + + async listModels() { + return this.orchidsApiService.listModels(); + } + + async refreshToken() { + if (this.orchidsApiService.isExpiryDateNear()) { + return this.orchidsApiService.initializeAuth(true); + } + return Promise.resolve(); + } + + async getUsageLimits() { + if (!this.orchidsApiService.isInitialized) { + await this.orchidsApiService.initialize(); + } + return this.orchidsApiService.getUsageLimits(); + } + + countTokens(requestBody) { + return this.orchidsApiService.countTokens(requestBody); + } +} + // Qwen API 服务适配器 export class QwenApiServiceAdapter extends ApiServiceAdapter { constructor(config) { @@ -434,6 +479,9 @@ export function getServiceAdapter(config) { case MODEL_PROVIDER.IFLOW_API: serviceInstances[providerKey] = new IFlowApiServiceAdapter(config); break; + case MODEL_PROVIDER.ORCHIDS_API: + serviceInstances[providerKey] = new OrchidsApiServiceAdapter(config); + break; default: throw new Error(`Unsupported model provider: ${provider}`); } diff --git a/src/providers/provider-models.js b/src/providers/provider-models.js index 69d037a..7d7c605 100644 --- a/src/providers/provider-models.js +++ b/src/providers/provider-models.js @@ -33,6 +33,14 @@ export const PROVIDER_MODELS = { 'claude-sonnet-4-20250514', 'claude-3-7-sonnet-20250219' ], + 'claude-orchids-oauth': [ + 'claude-sonnet-4-5', + 'claude-opus-4-5', + 'claude-haiku-4-5', + 'gemini-3', + 'gemini-3-flash', + 'gpt-5.2' + ], 'openai-custom': [], 'openaiResponses-custom': [], 'openai-qwen-oauth': [ diff --git a/src/services/ui-manager.js b/src/services/ui-manager.js index 2fa9f93..496d9a5 100644 --- a/src/services/ui-manager.js +++ b/src/services/ui-manager.js @@ -268,6 +268,11 @@ export async function handleUIApiRequests(method, pathParam, req, res, currentCo return await oauthApi.handleImportAwsCredentials(req, res); } + // Import Orchids token + if (method === 'POST' && pathParam === '/api/orchids/import-token') { + return await oauthApi.handleImportOrchidsToken(req, res); + } + // Get plugins list if (method === 'GET' && pathParam === '/api/plugins') { return await pluginApi.handleGetPlugins(req, res); diff --git a/src/ui-modules/config-scanner.js b/src/ui-modules/config-scanner.js index e9021d0..4b04137 100644 --- a/src/ui-modules/config-scanner.js +++ b/src/ui-modules/config-scanner.js @@ -28,6 +28,7 @@ export async function scanConfigFiles(currentConfig, providerPoolManager) { addToUsedPaths(usedPaths, currentConfig.QWEN_OAUTH_CREDS_FILE_PATH); addToUsedPaths(usedPaths, currentConfig.ANTIGRAVITY_OAUTH_CREDS_FILE_PATH); addToUsedPaths(usedPaths, currentConfig.IFLOW_TOKEN_FILE_PATH); + addToUsedPaths(usedPaths, currentConfig.ORCHIDS_CREDS_FILE_PATH); // 使用最新的提供商池数据 let providerPools = currentConfig.providerPools; @@ -44,6 +45,7 @@ export async function scanConfigFiles(currentConfig, providerPoolManager) { addToUsedPaths(usedPaths, provider.QWEN_OAUTH_CREDS_FILE_PATH); addToUsedPaths(usedPaths, provider.ANTIGRAVITY_OAUTH_CREDS_FILE_PATH); addToUsedPaths(usedPaths, provider.IFLOW_TOKEN_FILE_PATH); + addToUsedPaths(usedPaths, provider.ORCHIDS_CREDS_FILE_PATH); } } } @@ -214,6 +216,17 @@ function getFileUsageInfo(relativePath, fileName, usedPaths, currentConfig) { }); } + if (currentConfig.ORCHIDS_CREDS_FILE_PATH && + (pathsEqual(relativePath, currentConfig.ORCHIDS_CREDS_FILE_PATH) || + pathsEqual(relativePath, currentConfig.ORCHIDS_CREDS_FILE_PATH.replace(/\\/g, '/')))) { + usageInfo.usageType = 'main_config'; + usageInfo.usageDetails.push({ + type: 'Main Config', + location: 'Orchids OAuth credentials file path', + configKey: 'ORCHIDS_CREDS_FILE_PATH' + }); + } + // 检查提供商池中的使用情况 if (currentConfig.providerPools) { // 使用 flatMap 将双重循环优化为单层循环 O(n) @@ -284,6 +297,18 @@ function getFileUsageInfo(relativePath, fileName, usedPaths, currentConfig) { configKey: 'IFLOW_TOKEN_FILE_PATH' }); } + + if (provider.ORCHIDS_CREDS_FILE_PATH && + (pathsEqual(relativePath, provider.ORCHIDS_CREDS_FILE_PATH) || + pathsEqual(relativePath, provider.ORCHIDS_CREDS_FILE_PATH.replace(/\\/g, '/')))) { + providerUsages.push({ + type: 'Provider Pool', + location: `Orchids OAuth credentials (node ${index + 1})`, + providerType: providerType, + providerIndex: index, + configKey: 'ORCHIDS_CREDS_FILE_PATH' + }); + } if (providerUsages.length > 0) { usageInfo.usageType = 'provider_pool'; diff --git a/src/ui-modules/oauth-api.js b/src/ui-modules/oauth-api.js index f16704c..34ebb28 100644 --- a/src/ui-modules/oauth-api.js +++ b/src/ui-modules/oauth-api.js @@ -1,12 +1,14 @@ import { getRequestBody } from '../utils/common.js'; -import { - handleGeminiCliOAuth, - handleGeminiAntigravityOAuth, - handleQwenOAuth, - handleKiroOAuth, - handleIFlowOAuth, - batchImportKiroRefreshTokensStream, - importAwsCredentials +import { + handleGeminiCliOAuth, + handleGeminiAntigravityOAuth, + handleQwenOAuth, + handleKiroOAuth, + handleIFlowOAuth, + handleOrchidsOAuth, + batchImportKiroRefreshTokensStream, + importAwsCredentials, + importOrchidsToken } from '../auth/oauth-handlers.js'; /** @@ -49,6 +51,11 @@ export async function handleGenerateAuthUrl(req, res, currentConfig, providerTyp 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 { res.writeHead(400, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ @@ -303,4 +310,286 @@ export async function handleImportAwsCredentials(req, res) { })); 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}` }; + } } \ No newline at end of file diff --git a/src/utils/common.js b/src/utils/common.js index 1ea4439..3e526d3 100644 --- a/src/utils/common.js +++ b/src/utils/common.js @@ -65,6 +65,7 @@ export const MODEL_PROVIDER = { OPENAI_CUSTOM_RESPONSES: 'openaiResponses-custom', CLAUDE_CUSTOM: 'claude-custom', KIRO_API: 'claude-kiro-oauth', + ORCHIDS_API: 'claude-orchids-oauth', QWEN_API: 'openai-qwen-oauth', IFLOW_API: 'openai-iflow', } diff --git a/src/utils/provider-utils.js b/src/utils/provider-utils.js index 0c304fc..9cc48de 100644 --- a/src/utils/provider-utils.js +++ b/src/utils/provider-utils.js @@ -65,6 +65,17 @@ export const PROVIDER_MAPPINGS = [ displayName: 'iFlow API', needsProjectId: false, urlKeys: ['IFLOW_BASE_URL'] + }, + { + // Orchids OAuth 配置 + dirName: 'orchids', + patterns: ['configs/orchids/', '/orchids/'], + providerType: 'claude-orchids-oauth', + credPathKey: 'ORCHIDS_CREDS_FILE_PATH', + defaultCheckModel: 'claude-sonnet-4-5', + displayName: 'Orchids OAuth', + needsProjectId: false, + urlKeys: ['ORCHIDS_BASE_URL'] } ]; diff --git a/static/app/i18n.js b/static/app/i18n.js index 56296e7..c940cf1 100644 --- a/static/app/i18n.js +++ b/static/app/i18n.js @@ -181,6 +181,32 @@ const translations = { 'oauth.iflow.step2': '使用您的 iFlow 账号登录并授权', 'oauth.iflow.step3': '授权完成后,系统会自动获取 API Key', 'oauth.iflow.step4': '凭据文件可在上传配置管理中查看和管理', + + // Orchids OAuth + 'oauth.orchids.title': 'Orchids 凭据导入', + 'oauth.orchids.formatToken': 'JWT Token 格式', + 'oauth.orchids.tokenLabel': 'JWT Token 字符串', + 'oauth.orchids.tokenPlaceholder': 'eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6ImNsaWVudF94eHgiLCJyb3RhdGluZ190b2tlbiI6Inh4eCJ9.xxx', + 'oauth.orchids.tokenInstructions': '粘贴 JWT Token 字符串(支持纯 JWT 格式,rotating_token 会从 JWT payload 中自动提取)', + 'oauth.orchids.getSteps': '获取步骤:', + 'oauth.orchids.tokenStep1': '访问 www.orchids.app 并登录', + 'oauth.orchids.tokenStep2': '按 F12 打开开发者工具 → Application 标签', + 'oauth.orchids.tokenStep3': '在左侧找到 Cookies → www.orchids.app', + 'oauth.orchids.tokenStep4': '找到 __client cookie 的值', + 'oauth.orchids.tokenStep5': '复制完整的 JWT 值(以 eyJ 开头的字符串)', + 'oauth.orchids.tokenFormat': '格式:eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6ImNsaWVudF94eHgiLCJyb3RhdGluZ190b2tlbiI6Inh4eCJ9.xxx', + 'oauth.orchids.confirmImport': '确认导入', + 'oauth.orchids.importing': '导入中...', + 'oauth.orchids.success': 'Orchids 凭据导入成功', + 'oauth.orchids.parseSuccess': 'Token 解析成功', + 'oauth.orchids.detectedToken': '检测到有效的 JWT Token', + 'oauth.orchids.errorEmpty': '请输入凭据', + 'oauth.orchids.errorTokenInvalid': 'Token 格式错误,请输入有效的 JWT Token', + 'oauth.orchids.errorJwtParse': 'JWT 解析失败', + 'oauth.orchids.errorMissingRotating': 'JWT payload 中缺少 rotating_token 字段', + 'oauth.orchids.importFailed': '导入失败', + 'oauth.orchids.clientId': 'Client ID', + 'oauth.orchids.rotatingToken': 'Rotating Token', // Config 'config.title': '配置管理', @@ -348,6 +374,7 @@ const translations = { 'providers.stat.usageCount': '使用次数', 'providers.stat.errorCount': '错误次数', 'providers.auth.generate': '生成授权', + 'providers.auth.importToken': '导入 Token', // Modal Provider Manager 'modal.provider.manage': '管理 {type} 提供商配置', @@ -676,6 +703,32 @@ const translations = { 'oauth.iflow.step2': 'Log in with your iFlow account and authorize', 'oauth.iflow.step3': 'After authorization, the system will automatically fetch the API Key', 'oauth.iflow.step4': 'Credentials files can be viewed and managed in Upload Config', + + // Orchids OAuth + 'oauth.orchids.title': 'Orchids Credential Import', + 'oauth.orchids.formatToken': 'JWT Token Format', + 'oauth.orchids.tokenLabel': 'JWT Token String', + 'oauth.orchids.tokenPlaceholder': 'eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6ImNsaWVudF94eHgiLCJyb3RhdGluZ190b2tlbiI6Inh4eCJ9.xxx', + 'oauth.orchids.tokenInstructions': 'Paste JWT Token string (rotating_token will be automatically extracted from JWT payload)', + 'oauth.orchids.getSteps': 'How to get:', + 'oauth.orchids.tokenStep1': 'Visit www.orchids.app and log in', + 'oauth.orchids.tokenStep2': 'Press F12 to open Developer Tools → Application tab', + 'oauth.orchids.tokenStep3': 'Find Cookies → www.orchids.app on the left', + 'oauth.orchids.tokenStep4': 'Find the __client cookie value', + 'oauth.orchids.tokenStep5': 'Copy the full JWT value (string starting with eyJ)', + 'oauth.orchids.tokenFormat': 'Format: eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6ImNsaWVudF94eHgiLCJyb3RhdGluZ190b2tlbiI6Inh4eCJ9.xxx', + 'oauth.orchids.confirmImport': 'Confirm Import', + 'oauth.orchids.importing': 'Importing...', + 'oauth.orchids.success': 'Orchids credentials imported successfully', + 'oauth.orchids.parseSuccess': 'Token parsed successfully', + 'oauth.orchids.detectedToken': 'Valid JWT Token detected', + 'oauth.orchids.errorEmpty': 'Please enter credentials', + 'oauth.orchids.errorTokenInvalid': 'Token format error, please enter a valid JWT Token', + 'oauth.orchids.errorJwtParse': 'JWT parse failed', + 'oauth.orchids.errorMissingRotating': 'Missing rotating_token field in JWT payload', + 'oauth.orchids.importFailed': 'Import failed', + 'oauth.orchids.clientId': 'Client ID', + 'oauth.orchids.rotatingToken': 'Rotating Token', // Config 'config.title': 'Configuration Management', @@ -843,6 +896,7 @@ const translations = { 'providers.stat.usageCount': 'Usage Count', 'providers.stat.errorCount': 'Error Count', 'providers.auth.generate': 'Gen Auth', + 'providers.auth.importToken': 'Import Token', // Modal Provider Manager 'modal.provider.manage': 'Manage {type} Provider Config', diff --git a/static/app/provider-manager.js b/static/app/provider-manager.js index ce2f4eb..f27662e 100644 --- a/static/app/provider-manager.js +++ b/static/app/provider-manager.js @@ -212,6 +212,7 @@ function renderProviders(providers) { 'openai-custom', 'claude-custom', 'claude-kiro-oauth', + 'claude-orchids-oauth', 'openai-qwen-oauth', 'openaiResponses-custom', 'openai-iflow' @@ -423,12 +424,22 @@ async function openProviderManager(providerType) { */ function generateAuthButton(providerType) { // 只为支持OAuth的提供商显示授权按钮 - const oauthProviders = ['gemini-cli-oauth', 'gemini-antigravity', 'openai-qwen-oauth', 'claude-kiro-oauth', 'openai-iflow']; + const oauthProviders = ['gemini-cli-oauth', 'gemini-antigravity', 'openai-qwen-oauth', 'claude-kiro-oauth', 'claude-orchids-oauth', 'openai-iflow']; if (!oauthProviders.includes(providerType)) { return ''; } + // Orchids 提供商使用不同的按钮文本 + if (providerType === 'claude-orchids-oauth') { + return ` + + `; + } + return ` + + + + + `; + + document.body.appendChild(modal); + + const closeBtn = modal.querySelector('.modal-close'); + const cancelBtn = modal.querySelector('.modal-cancel'); + const submitBtn = modal.querySelector('#orchidsSubmitBtn'); + const tokenInput = modal.querySelector('#orchidsTokenInput'); + const validationResult = modal.querySelector('#orchidsValidationResult'); + const parsePreview = modal.querySelector('#orchidsParsePreview'); + const parseDetails = modal.querySelector('#orchidsParseDetails'); + + // 实时验证输入 + tokenInput.addEventListener('input', () => { + const inputValue = tokenInput.value.trim(); + if (!inputValue) { + validationResult.style.display = 'none'; + parsePreview.style.display = 'none'; + return; + } + + // 检测 JWT 格式 + if (inputValue.startsWith('eyJ') && inputValue.split('.').length === 3) { + // 尝试解析 JWT + try { + let jwt = inputValue; + let rotatingTokenFromSeparator = null; + + // 检查是否有 | 分隔符 + if (inputValue.includes('|')) { + const parts = inputValue.split('|'); + jwt = parts[0]; + rotatingTokenFromSeparator = parts[1]; + } + + const jwtParts = jwt.split('.'); + if (jwtParts.length === 3) { + let payloadBase64 = jwtParts[1].replace(/-/g, '+').replace(/_/g, '/'); + // 添加 padding + while (payloadBase64.length % 4) { + payloadBase64 += '='; + } + const payloadJson = atob(payloadBase64); + const payload = JSON.parse(payloadJson); + + // 检查是否有 rotating_token(从 payload 或分隔符后) + const rotatingToken = payload.rotating_token || rotatingTokenFromSeparator; + + if (rotatingToken) { + validationResult.style.cssText = 'display: block; background: #f0fdf4; border: 1px solid #bbf7d0; color: #166534;'; + validationResult.innerHTML = ' ' + t('oauth.orchids.detectedToken'); + + parsePreview.style.display = 'block'; + parseDetails.innerHTML = ` +
+
${t('oauth.orchids.clientId')}: ${payload.id || 'N/A'}
+
${t('oauth.orchids.rotatingToken')}: ${rotatingToken.substring(0, 30)}...
+
+ `; + } else { + validationResult.style.cssText = 'display: block; background: #fef2f2; border: 1px solid #fecaca; color: #991b1b;'; + validationResult.innerHTML = ' ' + t('oauth.orchids.errorMissingRotating'); + parsePreview.style.display = 'none'; + } + } + } catch (e) { + validationResult.style.cssText = 'display: block; background: #fef2f2; border: 1px solid #fecaca; color: #991b1b;'; + validationResult.innerHTML = ' ' + t('oauth.orchids.errorJwtParse') + ': ' + e.message; + parsePreview.style.display = 'none'; + } + } else { + validationResult.style.cssText = 'display: block; background: #fef2f2; border: 1px solid #fecaca; color: #991b1b;'; + validationResult.innerHTML = ' ' + t('oauth.orchids.errorTokenInvalid'); + parsePreview.style.display = 'none'; + } + }); + + // 关闭按钮事件 + [closeBtn, cancelBtn].forEach(btn => { + btn.addEventListener('click', () => modal.remove()); + }); + + // 提交按钮事件 + submitBtn.addEventListener('click', async () => { + const inputValue = tokenInput.value.trim(); + + // 验证输入 + if (!inputValue) { + validationResult.style.cssText = 'display: block; background: #fef2f2; border: 1px solid #fecaca; color: #991b1b;'; + validationResult.innerHTML = ' ' + t('oauth.orchids.errorEmpty'); + return; + } + + // JWT 格式验证(支持纯 JWT 和 JWT|rotating_token 格式) + let jwt = inputValue; + if (inputValue.includes('|')) { + jwt = inputValue.split('|')[0]; + } + + if (!jwt.startsWith('eyJ') || jwt.split('.').length !== 3) { + validationResult.style.cssText = 'display: block; background: #fef2f2; border: 1px solid #fecaca; color: #991b1b;'; + validationResult.innerHTML = ' ' + t('oauth.orchids.errorTokenInvalid'); + return; + } + + // 禁用按钮 + submitBtn.disabled = true; + submitBtn.innerHTML = ' ' + t('oauth.orchids.importing') + ''; + + try { + const response = await window.apiClient.post('/orchids/import-token', { token: inputValue }); + + if (response.success) { + showToast(t('common.success'), t('oauth.orchids.success'), 'success'); + modal.remove(); + loadProviders(); + loadConfigList(); + } else { + validationResult.style.cssText = 'display: block; background: #fef2f2; border: 1px solid #fecaca; color: #991b1b;'; + validationResult.innerHTML = ' ' + (response.error || t('oauth.orchids.importFailed')); + } + } catch (error) { + console.error('Orchids import failed:', error); + validationResult.style.cssText = 'display: block; background: #fef2f2; border: 1px solid #fecaca; color: #991b1b;'; + validationResult.innerHTML = ' ' + t('oauth.orchids.importFailed') + ': ' + error.message; + } finally { + submitBtn.disabled = false; + submitBtn.innerHTML = ' ' + t('oauth.orchids.confirmImport') + ''; + } + }); +} + /** * 显示 Kiro OAuth 认证方式选择对话框 * @param {string} providerType - 提供商类型 diff --git a/static/app/upload-config-manager.js b/static/app/upload-config-manager.js index 494a73f..f802112 100644 --- a/static/app/upload-config-manager.js +++ b/static/app/upload-config-manager.js @@ -811,6 +811,12 @@ function detectProviderFromPath(filePath) { providerType: 'gemini-antigravity', displayName: 'Gemini Antigravity', shortName: 'antigravity' + }, + { + patterns: ['configs/orchids/', '/orchids/'], + providerType: 'claude-orchids-oauth', + displayName: 'Orchids OAuth', + shortName: 'orchids-oauth' } ]; diff --git a/static/components/section-config.html b/static/components/section-config.html index 3f2412b..12a7361 100644 --- a/static/components/section-config.html +++ b/static/components/section-config.html @@ -58,6 +58,10 @@ iFlow OAuth + 点击选择启动时初始化的模型提供商 (必须至少选择一个) @@ -109,6 +113,10 @@ iFlow OAuth + 点击选择需要通过代理访问的提供商,未选中的提供商将直接连接 diff --git a/static/components/section-dashboard.html b/static/components/section-dashboard.html index 18fde1a..6de82da 100644 --- a/static/components/section-dashboard.html +++ b/static/components/section-dashboard.html @@ -497,6 +497,58 @@ +
+
+ +

Orchids OAuth

+ 突破限制/免费使用 +
+
+ +
+ + +
+ + +
+
+ + /claude-orchids-oauth/v1/chat/completions +
+
+ +
curl http://localhost:3000/claude-orchids-oauth/v1/chat/completions \
+  -H "Content-Type: application/json" \
+  -H "Authorization: Bearer YOUR_API_KEY" \
+  -d '{
+    "model": "claude-sonnet-4-5",
+    "messages": [{"role": "user", "content": "Hello!"}],
+    "max_tokens": 8192
+  }'
+
+
+ + +
+
+ + /claude-orchids-oauth/v1/messages +
+
+ +
curl http://localhost:3000/claude-orchids-oauth/v1/messages \
+  -H "Content-Type: application/json" \
+  -H "X-API-Key: YOUR_API_KEY" \
+  -d '{
+    "model": "claude-sonnet-4-5",
+    "max_tokens": 8192,
+    "messages": [{"role": "user", "content": "Hello!"}]
+  }'
+
+
+
+