feat(orchids): 添加 Orchids OAuth 提供商集成
1. 新增 Orchids OAuth 提供商支持,包括凭据导入和 API 服务 2. 添加 claude-orchids.js 核心服务实现 WebSocket 通信 3. 实现 JWT Token 解析和凭据管理功能 4. 更新 UI 界面支持 Orchids Token 导入和配置管理 5. 添加 i18n 多语言支持(中文/英文) 6. 更新 .gitignore 忽略 Orchids 凭据文件
This commit is contained in:
parent
3676c5330b
commit
d7cb0103b8
17 changed files with 1062 additions and 13 deletions
6
.gitignore
vendored
6
.gitignore
vendored
|
|
@ -12,4 +12,8 @@ usage-cache.json
|
|||
*_oauth_creds.json
|
||||
*-auth-token.json
|
||||
api-potluck-keys.json
|
||||
api-potluck-data.json
|
||||
api-potluck-data.json
|
||||
# Orchids credentials
|
||||
configs/orchids/*_orchids_creds/
|
||||
configs/orchids/*.json
|
||||
!configs/orchids/*.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节点",
|
||||
|
|
|
|||
26
package-lock.json
generated
26
package-lock.json
generated
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -1966,3 +1966,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<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 等参数会自动获取'
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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}`);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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': [
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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}` };
|
||||
}
|
||||
}
|
||||
|
|
@ -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',
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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']
|
||||
}
|
||||
];
|
||||
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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 `
|
||||
<button class="generate-auth-btn" title="导入 Orchids Token">
|
||||
<i class="fas fa-seedling" style="color: #10b981;"></i>
|
||||
<span data-i18n="providers.auth.importToken">${t('providers.auth.importToken') || '导入 Token'}</span>
|
||||
</button>
|
||||
`;
|
||||
}
|
||||
|
||||
return `
|
||||
<button class="generate-auth-btn" title="生成OAuth授权链接">
|
||||
<i class="fas fa-key"></i>
|
||||
|
|
@ -447,10 +458,218 @@ async function handleGenerateAuthUrl(providerType) {
|
|||
showKiroAuthMethodSelector(providerType);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// 如果是 Orchids OAuth,显示 Cookie 导入对话框
|
||||
if (providerType === 'claude-orchids-oauth') {
|
||||
showOrchidsCookieImportDialog(providerType);
|
||||
return;
|
||||
}
|
||||
|
||||
await executeGenerateAuthUrl(providerType, {});
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示 Orchids Token 导入对话框
|
||||
* 支持两种格式:
|
||||
* 1. 纯 JWT 格式:eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...(JWT payload 中包含 rotating_token)
|
||||
* 2. JWT|rotating_token 格式:eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...|W7lqx1t8HIxMh0ScDZUB
|
||||
* @param {string} providerType - 提供商类型
|
||||
*/
|
||||
function showOrchidsCookieImportDialog(providerType) {
|
||||
const modal = document.createElement('div');
|
||||
modal.className = 'modal-overlay';
|
||||
modal.style.display = 'flex';
|
||||
|
||||
modal.innerHTML = `
|
||||
<div class="modal-content" style="max-width: 750px;">
|
||||
<div class="modal-header">
|
||||
<h3><i class="fas fa-seedling" style="color: #10b981;"></i> <span>${t('oauth.orchids.title')}</span></h3>
|
||||
<button class="modal-close">×</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<!-- JWT Token 格式说明 -->
|
||||
<div id="orchidsTokenInstructions" class="orchids-import-instructions" style="margin-bottom: 16px; padding: 12px; background: #ecfdf5; border: 1px solid #a7f3d0; border-radius: 8px;">
|
||||
<p style="margin: 0; font-size: 14px; color: #065f46;">
|
||||
<i class="fas fa-info-circle"></i>
|
||||
${t('oauth.orchids.tokenInstructions')}
|
||||
</p>
|
||||
<div style="margin-top: 12px; padding: 10px; background: #d1fae5; border-radius: 6px; font-size: 13px;">
|
||||
<p style="margin: 0 0 8px 0; font-weight: 600; color: #047857;">
|
||||
<i class="fas fa-lightbulb"></i> ${t('oauth.orchids.getSteps')}
|
||||
</p>
|
||||
<ol style="margin: 0; padding-left: 20px; color: #065f46;">
|
||||
<li>${t('oauth.orchids.tokenStep1')}</li>
|
||||
<li>${t('oauth.orchids.tokenStep2')}</li>
|
||||
<li>${t('oauth.orchids.tokenStep3')}</li>
|
||||
<li>${t('oauth.orchids.tokenStep4')}</li>
|
||||
<li>${t('oauth.orchids.tokenStep5')}</li>
|
||||
</ol>
|
||||
</div>
|
||||
<div style="margin-top: 8px; padding: 8px; background: #d1fae5; border-radius: 6px; font-size: 11px; font-family: monospace; word-break: break-all; color: #047857;">
|
||||
${t('oauth.orchids.tokenFormat')}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group" style="margin-bottom: 16px;">
|
||||
<label id="orchidsInputLabel" style="display: block; margin-bottom: 8px; font-weight: 600; color: #374151;">
|
||||
${t('oauth.orchids.tokenLabel')} <span style="color: #ef4444;">*</span>
|
||||
</label>
|
||||
<textarea id="orchidsTokenInput" rows="4"
|
||||
style="width: 100%; padding: 10px 12px; border: 1px solid #d1d5db; border-radius: 8px; font-family: monospace; font-size: 12px; resize: vertical;"
|
||||
placeholder="${t('oauth.orchids.tokenPlaceholder')}"
|
||||
></textarea>
|
||||
</div>
|
||||
|
||||
<div id="orchidsValidationResult" style="display: none; margin-bottom: 16px; padding: 12px; border-radius: 8px;"></div>
|
||||
|
||||
<!-- 解析预览 -->
|
||||
<div id="orchidsParsePreview" style="display: none; margin-bottom: 16px; padding: 12px; background: #f0fdf4; border: 1px solid #bbf7d0; border-radius: 8px;">
|
||||
<p style="margin: 0 0 8px 0; font-weight: 600; color: #166534;">
|
||||
<i class="fas fa-check-circle"></i> ${t('oauth.orchids.parseSuccess')}
|
||||
</p>
|
||||
<div id="orchidsParseDetails" style="font-size: 12px; color: #065f46;"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="modal-cancel">${t('modal.provider.cancel')}</button>
|
||||
<button class="modal-submit" id="orchidsSubmitBtn" style="background: #10b981; color: white;">
|
||||
<i class="fas fa-check"></i>
|
||||
<span>${t('oauth.orchids.confirmImport')}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
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 = '<i class="fas fa-check-circle"></i> ' + t('oauth.orchids.detectedToken');
|
||||
|
||||
parsePreview.style.display = 'block';
|
||||
parseDetails.innerHTML = `
|
||||
<div style="display: grid; gap: 4px;">
|
||||
<div><strong>${t('oauth.orchids.clientId')}:</strong> <code style="background: #d1fae5; padding: 1px 4px; border-radius: 2px;">${payload.id || 'N/A'}</code></div>
|
||||
<div><strong>${t('oauth.orchids.rotatingToken')}:</strong> <code style="background: #d1fae5; padding: 1px 4px; border-radius: 2px;">${rotatingToken.substring(0, 30)}...</code></div>
|
||||
</div>
|
||||
`;
|
||||
} else {
|
||||
validationResult.style.cssText = 'display: block; background: #fef2f2; border: 1px solid #fecaca; color: #991b1b;';
|
||||
validationResult.innerHTML = '<i class="fas fa-exclamation-triangle"></i> ' + 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 = '<i class="fas fa-exclamation-triangle"></i> ' + 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 = '<i class="fas fa-exclamation-triangle"></i> ' + 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 = '<i class="fas fa-exclamation-triangle"></i> ' + 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 = '<i class="fas fa-exclamation-triangle"></i> ' + t('oauth.orchids.errorTokenInvalid');
|
||||
return;
|
||||
}
|
||||
|
||||
// 禁用按钮
|
||||
submitBtn.disabled = true;
|
||||
submitBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> <span>' + t('oauth.orchids.importing') + '</span>';
|
||||
|
||||
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 = '<i class="fas fa-times-circle"></i> ' + (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 = '<i class="fas fa-times-circle"></i> ' + t('oauth.orchids.importFailed') + ': ' + error.message;
|
||||
} finally {
|
||||
submitBtn.disabled = false;
|
||||
submitBtn.innerHTML = '<i class="fas fa-check"></i> <span>' + t('oauth.orchids.confirmImport') + '</span>';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示 Kiro OAuth 认证方式选择对话框
|
||||
* @param {string} providerType - 提供商类型
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
}
|
||||
];
|
||||
|
||||
|
|
|
|||
|
|
@ -58,6 +58,10 @@
|
|||
<i class="fas fa-stream"></i>
|
||||
<span>iFlow OAuth</span>
|
||||
</button>
|
||||
<button type="button" class="provider-tag" data-value="claude-orchids-oauth">
|
||||
<i class="fas fa-seedling"></i>
|
||||
<span>Orchids OAuth</span>
|
||||
</button>
|
||||
</div>
|
||||
<small class="form-text" data-i18n="config.modelProviderHelp">点击选择启动时初始化的模型提供商 (必须至少选择一个)</small>
|
||||
</div>
|
||||
|
|
@ -109,6 +113,10 @@
|
|||
<i class="fas fa-stream"></i>
|
||||
<span>iFlow OAuth</span>
|
||||
</button>
|
||||
<button type="button" class="provider-tag" data-value="claude-orchids-oauth">
|
||||
<i class="fas fa-seedling"></i>
|
||||
<span>Orchids OAuth</span>
|
||||
</button>
|
||||
</div>
|
||||
<small class="form-text" data-i18n="config.proxy.enabledProvidersNote">点击选择需要通过代理访问的提供商,未选中的提供商将直接连接</small>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -497,6 +497,58 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="routing-example-card" data-provider="claude-orchids-oauth-card">
|
||||
<div class="routing-card-header">
|
||||
<i class="fas fa-seedling"></i>
|
||||
<h4 data-i18n="dashboard.routing.nodeName.orchids">Orchids OAuth</h4>
|
||||
<span class="provider-badge oauth" data-i18n="dashboard.routing.free">突破限制/免费使用</span>
|
||||
</div>
|
||||
<div class="routing-card-content">
|
||||
<!-- 协议标签切换 -->
|
||||
<div class="protocol-tabs">
|
||||
<button class="protocol-tab" data-protocol="openai" data-i18n="dashboard.routing.openai">OpenAI协议</button>
|
||||
<button class="protocol-tab active" data-protocol="claude" data-i18n="dashboard.routing.claude">Claude协议</button>
|
||||
</div>
|
||||
|
||||
<!-- OpenAI协议示例 -->
|
||||
<div class="protocol-content" data-protocol="openai">
|
||||
<div class="endpoint-info">
|
||||
<label data-i18n="dashboard.routing.endpoint">端点路径:</label>
|
||||
<code class="endpoint-path">/claude-orchids-oauth/v1/chat/completions</code>
|
||||
</div>
|
||||
<div class="usage-example">
|
||||
<label data-i18n="dashboard.routing.exampleOpenAI">使用示例 (OpenAI格式):</label>
|
||||
<pre><code>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
|
||||
}'</code></pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Claude协议示例 -->
|
||||
<div class="protocol-content active" data-protocol="claude">
|
||||
<div class="endpoint-info">
|
||||
<label data-i18n="dashboard.routing.endpoint">端点路径:</label>
|
||||
<code class="endpoint-path">/claude-orchids-oauth/v1/messages</code>
|
||||
</div>
|
||||
<div class="usage-example">
|
||||
<label data-i18n="dashboard.routing.exampleClaude">使用示例 (Claude格式):</label>
|
||||
<pre><code>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!"}]
|
||||
}'</code></pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="routing-tips">
|
||||
|
|
|
|||
Loading…
Reference in a new issue