refactor: 优化Claude-》Gemini的转换逻辑,增加tools适配
This commit is contained in:
parent
ad2432a37c
commit
8afe41870d
27 changed files with 57 additions and 1106 deletions
5
.gitignore
vendored
5
.gitignore
vendored
|
|
@ -15,7 +15,4 @@ api-potluck-keys.json
|
|||
api-potluck-data.json
|
||||
# Codex credentials
|
||||
configs/codex/
|
||||
# Orchids credentials
|
||||
configs/orchids/*_orchids_creds/
|
||||
configs/orchids/*.json
|
||||
!configs/orchids/*.example
|
||||
|
||||
|
|
|
|||
|
|
@ -34,7 +34,6 @@
|
|||
> <details>
|
||||
> <summary>クリックして詳細なバージョン履歴を展開</summary>
|
||||
>
|
||||
> - **2026.01.22** - Orchidsプロトコルサポートの追加、Clerk OAuth認証でClaude Sonnet 4.5、Claude Opus 4.5、Gemini 3 Flash、GPT-5.2などのモデルにアクセス可能、WebSocketストリーミングとツール呼び出しをサポート
|
||||
> - **2026.01.15** - プロバイダープールマネージャーの最適化:非同期リフレッシュキューメカニズム、バッファキュー重複排除、グローバル並行制御、ノードウォームアップと自動期限切れ検出を追加
|
||||
> - **2026.01.07** - iFlowプロトコルサポートの追加、OAuth認証方式でQwen、Kimi、DeepSeek、GLMシリーズモデルにアクセス可能、自動トークンリフレッシュ機能をサポート
|
||||
> - **2026.01.03** - テーマ切替機能を追加し、プロバイダープール初期化を最適化、プロバイダーのデフォルト設定を使用するフォールバック戦略を削除
|
||||
|
|
@ -249,13 +248,6 @@ Web UI管理インターフェースでは、極めて迅速に認証設定を
|
|||
3. **ベストプラクティス**:**Claude Code**との併用を推奨、最適な体験を得られる
|
||||
4. **重要なお知らせ**:Kiroサービス使用ポリシーが更新されました、最新の使用制限と条件については公式ウェブサイトをご確認ください。
|
||||
|
||||
#### Orchids OAuth設定
|
||||
1. **プラットフォームにログイン**:[Orchidsプラットフォーム](https://orchids.app)にアクセスしてアカウントにログイン
|
||||
2. **認証情報を取得**:ブラウザの開発者ツール(F12)を開き、Application > Cookies > https://orchids.app に移動
|
||||
3. **トークンをコピー**:`__client` を見つけてその値(長いJWT文字列)をコピー
|
||||
4. **認証情報をインポート**:Web UIの「トークンをインポート」機能を使用して値を貼り付け
|
||||
5. **サポートモデル**:Claude Sonnet 4.5、Claude Opus 4.5、Gemini 3 Flash、GPT-5.2など
|
||||
|
||||
#### iFlow OAuth設定
|
||||
1. **初回認証**:Web UIの「設定管理」または「プロバイダープール」ページで、iFlowの「認証生成」ボタンをクリック
|
||||
2. **電話番号ログイン**:システムがiFlow認証ページを開き、電話番号でログイン認証を完了
|
||||
|
|
@ -284,7 +276,6 @@ Web UI管理インターフェースでは、極めて迅速に認証設定を
|
|||
| **Kiro** | `~/.aws/sso/cache/kiro-auth-token.json` | Kiro認証トークン |
|
||||
| **Qwen** | `~/.qwen/oauth_creds.json` | Qwen OAuth認証情報 |
|
||||
| **Antigravity** | `~/.antigravity/oauth_creds.json` | Antigravity OAuth認証情報 (Claude 4.5 Opus サポート) |
|
||||
| **Orchids** | `configs/orchids/{timestamp}_orchids_creds.json` | Orchids Clerk JWT認証情報 (Claude 4.5、GPT-5.2 サポート) |
|
||||
| **iFlow** | `~/.iflow/oauth_creds.json` | iFlow OAuth認証情報 (Qwen、Kimi、DeepSeek、GLM サポート) |
|
||||
|
||||
> **説明**:`~`はユーザーホームディレクトリを表します(Windows: `C:\Users\ユーザー名`、Linux/macOS: `/home/ユーザー名`または`/Users/ユーザー名`)
|
||||
|
|
|
|||
|
|
@ -34,7 +34,6 @@
|
|||
> <details>
|
||||
> <summary>点击展开查看详细版本历史</summary>
|
||||
>
|
||||
> - **2026.01.22** - 新增 Orchids 协议支持,通过 Clerk OAuth 认证访问 Claude Sonnet 4.5、Claude Opus 4.5、Gemini 3 Flash、GPT-5.2 等模型,支持 WebSocket 流式传输和工具调用
|
||||
> - **2026.01.15** - 优化提供商池管理器:新增异步刷新队列机制、缓冲队列去重、全局并发控制,支持节点预热和自动过期检测
|
||||
> - **2026.01.07** - 新增 iFlow 协议支持,通过 OAuth 认证方式访问 Qwen、Kimi、DeepSeek 和 GLM 系列模型,支持自动 token 刷新功能
|
||||
> - **2026.01.03** - 新增主题切换功能并优化提供商池初始化,移除使用提供商默认配置的降级策略
|
||||
|
|
@ -248,13 +247,6 @@ docker compose up -d
|
|||
3. **最佳实践**:推荐配合 **Claude Code** 使用,可获得最优体验
|
||||
4. **重要提示**:Kiro 服务使用政策已更新,请访问官方网站查看最新使用限制和条款
|
||||
|
||||
#### Orchids OAuth 配置
|
||||
1. **登录平台**:访问 [Orchids 平台](https://orchids.app) 并登录账号
|
||||
2. **获取凭据**:打开浏览器开发者工具 (F12),切换到 Application > Cookies > https://orchids.app
|
||||
3. **复制 Token**:找到 `__client` 并复制其值(一个长的 JWT 字符串)
|
||||
4. **导入凭据**:在 Web UI 中使用"导入 Token"功能粘贴该值
|
||||
5. **支持模型**:Claude Sonnet 4.5、Claude Opus 4.5、Gemini 3 Flash、GPT-5.2 等
|
||||
|
||||
#### iFlow OAuth 配置
|
||||
1. **首次授权**:在 Web UI 的"配置管理"或"提供商池"页面,点击 iFlow 的"生成授权"按钮
|
||||
2. **手机登录**:系统将打开 iFlow 授权页面,使用手机号完成登录验证
|
||||
|
|
@ -283,7 +275,6 @@ docker compose up -d
|
|||
| **Kiro** | `~/.aws/sso/cache/kiro-auth-token.json` | Kiro 认证令牌 |
|
||||
| **Qwen** | `~/.qwen/oauth_creds.json` | Qwen OAuth 凭据 |
|
||||
| **Antigravity** | `~/.antigravity/oauth_creds.json` | Antigravity OAuth 凭据 (支持 Claude 4.5 Opus) |
|
||||
| **Orchids** | `configs/orchids/{timestamp}_orchids_creds.json` | Orchids Clerk JWT 凭据 (支持 Claude 4.5、GPT-5.2) |
|
||||
| **iFlow** | `~/.iflow/oauth_creds.json` | iFlow OAuth 凭据 (支持 Qwen、Kimi、DeepSeek、GLM) |
|
||||
|
||||
> **说明**:`~` 表示用户主目录(Windows: `C:\Users\用户名`,Linux/macOS: `/home/用户名` 或 `/Users/用户名`)
|
||||
|
|
|
|||
|
|
@ -34,7 +34,6 @@
|
|||
> <details>
|
||||
> <summary>Click to expand detailed version history</summary>
|
||||
>
|
||||
> - **2026.01.22** - Added Orchids protocol support, accessing Claude Sonnet 4.5, Claude Opus 4.5, Gemini 3 Flash, GPT-5.2 and more via Clerk OAuth authentication, with WebSocket streaming and tool calling support
|
||||
> - **2026.01.15** - Optimized provider pool manager: added async refresh queue mechanism, buffer queue deduplication, global concurrency control, node warmup and automatic expiry detection
|
||||
> - **2026.01.07** - Added iFlow protocol support, enabling access to Qwen, Kimi, DeepSeek, and GLM series models via OAuth authentication with automatic token refresh
|
||||
> - **2026.01.03** - Added theme switching functionality and optimized provider pool initialization, removed the fallback strategy of using provider default configuration
|
||||
|
|
@ -249,13 +248,6 @@ In the Web UI management interface, you can complete authorization configuration
|
|||
3. **Best Practice**: Recommended to use with **Claude Code** for optimal experience
|
||||
4. **Important Notice**: Kiro service usage policy has been updated, please visit the official website for the latest usage restrictions and terms
|
||||
|
||||
#### Orchids OAuth Configuration
|
||||
1. **Login to Platform**: Visit [Orchids Platform](https://orchids.app) and log in to your account
|
||||
2. **Get Credentials**: Open browser developer tools (F12), navigate to Application > Cookies > https://orchids.app
|
||||
3. **Copy Token**: Find `__client` and copy its value (a long JWT string)
|
||||
4. **Import Credentials**: Use the "Import Token" function in Web UI to paste the value
|
||||
5. **Supported Models**: Claude Sonnet 4.5, Claude Opus 4.5, Gemini 3 Flash, GPT-5.2, etc.
|
||||
|
||||
#### iFlow OAuth Configuration
|
||||
1. **First Authorization**: In Web UI's "Configuration" or "Provider Pools" page, click the "Generate Authorization" button for iFlow
|
||||
2. **Phone Login**: The system will open the iFlow authorization page, complete login verification using your phone number
|
||||
|
|
@ -284,7 +276,6 @@ Default storage locations for authorization credential files of each service:
|
|||
| **Kiro** | `~/.aws/sso/cache/kiro-auth-token.json` | Kiro authentication token |
|
||||
| **Qwen** | `~/.qwen/oauth_creds.json` | Qwen OAuth credentials |
|
||||
| **Antigravity** | `~/.antigravity/oauth_creds.json` | Antigravity OAuth credentials (supports Claude 4.5 Opus) |
|
||||
| **Orchids** | `configs/orchids/{timestamp}_orchids_creds.json` | Orchids Clerk JWT credentials (supports Claude 4.5, GPT-5.2) |
|
||||
| **iFlow** | `~/.iflow/oauth_creds.json` | iFlow OAuth credentials (supports Qwen, Kimi, DeepSeek, GLM) |
|
||||
|
||||
> **Note**: `~` represents the user home directory (Windows: `C:\Users\username`, Linux/macOS: `/home/username` or `/Users/username`)
|
||||
|
|
|
|||
|
|
@ -135,21 +135,6 @@
|
|||
"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节点",
|
||||
|
|
|
|||
|
|
@ -30,9 +30,3 @@ export {
|
|||
handleIFlowOAuth,
|
||||
refreshIFlowTokens
|
||||
} from './iflow-oauth.js';
|
||||
|
||||
// Orchids OAuth
|
||||
export {
|
||||
importOrchidsToken,
|
||||
handleOrchidsOAuth
|
||||
} from './orchids-oauth.js';
|
||||
|
|
@ -21,7 +21,4 @@ export {
|
|||
// iFlow OAuth
|
||||
handleIFlowOAuth,
|
||||
refreshIFlowTokens,
|
||||
// Orchids OAuth
|
||||
importOrchidsToken,
|
||||
handleOrchidsOAuth
|
||||
} from './index.js';
|
||||
|
|
@ -1,280 +0,0 @@
|
|||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { broadcastEvent } from '../services/ui-manager.js';
|
||||
import { autoLinkProviderConfigs } from '../services/service-manager.js';
|
||||
import { CONFIG } from '../core/config-manager.js';
|
||||
|
||||
/**
|
||||
* 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 等参数会自动获取'
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
@ -585,11 +585,14 @@ export class GeminiConverter extends BaseConverter {
|
|||
}
|
||||
|
||||
const candidate = geminiResponse.candidates[0];
|
||||
const content = this.processGeminiResponseToClaudeContent(geminiResponse);
|
||||
const { content, hasToolUse } = this.processGeminiResponseToClaudeContent(geminiResponse);
|
||||
const finishReason = candidate.finishReason;
|
||||
let stopReason = "end_turn";
|
||||
|
||||
if (finishReason) {
|
||||
// [FIX] 参考 ag/response.rs - 如果有工具调用,stop_reason 应该是 "tool_use"
|
||||
if (hasToolUse) {
|
||||
stopReason = 'tool_use';
|
||||
} else if (finishReason) {
|
||||
switch (finishReason) {
|
||||
case 'STOP':
|
||||
stopReason = 'end_turn';
|
||||
|
|
@ -644,6 +647,7 @@ export class GeminiConverter extends BaseConverter {
|
|||
// [FIX] 参考 ag/streaming.rs 处理 thinking 和 text 块
|
||||
if (parts && Array.isArray(parts)) {
|
||||
const results = [];
|
||||
let hasToolUse = false;
|
||||
|
||||
for (const part of parts) {
|
||||
if (!part) continue;
|
||||
|
|
@ -662,8 +666,10 @@ export class GeminiConverter extends BaseConverter {
|
|||
results.push(thinkingResult);
|
||||
|
||||
// 如果有签名,发送 signature_delta
|
||||
if (part.thoughtSignature) {
|
||||
let signature = part.thoughtSignature;
|
||||
// [FIX] 同时检查 thoughtSignature 和 thought_signature
|
||||
const rawSignature = part.thoughtSignature || part.thought_signature;
|
||||
if (rawSignature) {
|
||||
let signature = rawSignature;
|
||||
try {
|
||||
const decoded = Buffer.from(signature, 'base64').toString('utf-8');
|
||||
if (decoded && decoded.length > 0 && !decoded.includes('\ufffd')) {
|
||||
|
|
@ -696,6 +702,7 @@ export class GeminiConverter extends BaseConverter {
|
|||
|
||||
// [FIX] 处理 functionCall
|
||||
if (part.functionCall) {
|
||||
hasToolUse = true;
|
||||
// [FIX] 规范化工具名称和参数映射
|
||||
const toolName = normalizeToolName(part.functionCall.name);
|
||||
const remappedArgs = remapFunctionCallArgs(toolName, part.functionCall.args || {});
|
||||
|
|
@ -724,6 +731,25 @@ export class GeminiConverter extends BaseConverter {
|
|||
}
|
||||
}
|
||||
|
||||
// [FIX] 如果有工具调用,添加 message_delta 事件设置 stop_reason 为 tool_use
|
||||
if (hasToolUse && candidate.finishReason) {
|
||||
const messageDelta = {
|
||||
type: "message_delta",
|
||||
delta: {
|
||||
stop_reason: 'tool_use'
|
||||
}
|
||||
};
|
||||
if (geminiChunk.usageMetadata) {
|
||||
messageDelta.usage = {
|
||||
input_tokens: geminiChunk.usageMetadata.promptTokenCount || 0,
|
||||
cache_creation_input_tokens: 0,
|
||||
cache_read_input_tokens: geminiChunk.usageMetadata.cachedContentTokenCount || 0,
|
||||
output_tokens: geminiChunk.usageMetadata.candidatesTokenCount || 0
|
||||
};
|
||||
}
|
||||
results.push(messageDelta);
|
||||
}
|
||||
|
||||
// 如果有多个结果,返回数组;否则返回单个或 null
|
||||
if (results.length > 1) {
|
||||
return results;
|
||||
|
|
@ -802,6 +828,7 @@ export class GeminiConverter extends BaseConverter {
|
|||
|
||||
// [FIX] 参考 ag/response.rs 处理 thinking 块
|
||||
// Gemini 使用 thought: true 和 thoughtSignature 表示思考内容
|
||||
// [FIX] 同时支持 thoughtSignature 和 thought_signature(Gemini CLI 可能使用下划线格式)
|
||||
if (part.text) {
|
||||
if (part.thought === true) {
|
||||
// 这是一个 thinking 块
|
||||
|
|
@ -810,8 +837,10 @@ export class GeminiConverter extends BaseConverter {
|
|||
thinking: part.text
|
||||
};
|
||||
// 处理签名 - 可能是 Base64 编码的
|
||||
if (part.thoughtSignature) {
|
||||
let signature = part.thoughtSignature;
|
||||
// [FIX] 同时检查 thoughtSignature 和 thought_signature
|
||||
const rawSignature = part.thoughtSignature || part.thought_signature;
|
||||
if (rawSignature) {
|
||||
let signature = rawSignature;
|
||||
// 尝试 Base64 解码
|
||||
try {
|
||||
const decoded = Buffer.from(signature, 'base64').toString('utf-8');
|
||||
|
|
@ -858,8 +887,10 @@ export class GeminiConverter extends BaseConverter {
|
|||
input: remappedArgs
|
||||
};
|
||||
// [FIX] 如果有签名,添加到 tool_use 块
|
||||
if (part.thoughtSignature) {
|
||||
let signature = part.thoughtSignature;
|
||||
// [FIX] 同时检查 thoughtSignature 和 thought_signature
|
||||
const rawSignature = part.thoughtSignature || part.thought_signature;
|
||||
if (rawSignature) {
|
||||
let signature = rawSignature;
|
||||
try {
|
||||
const decoded = Buffer.from(signature, 'base64').toString('utf-8');
|
||||
if (decoded && decoded.length > 0 && !decoded.includes('\ufffd')) {
|
||||
|
|
@ -893,9 +924,12 @@ export class GeminiConverter extends BaseConverter {
|
|||
|
||||
/**
|
||||
* 处理Gemini响应到Claude内容
|
||||
* @returns {{ content: Array, hasToolUse: boolean }}
|
||||
*/
|
||||
processGeminiResponseToClaudeContent(geminiResponse) {
|
||||
if (!geminiResponse || !geminiResponse.candidates || geminiResponse.candidates.length === 0) return [];
|
||||
if (!geminiResponse || !geminiResponse.candidates || geminiResponse.candidates.length === 0) {
|
||||
return { content: [], hasToolUse: false };
|
||||
}
|
||||
|
||||
const content = [];
|
||||
let hasToolUse = false;
|
||||
|
|
@ -922,8 +956,10 @@ export class GeminiConverter extends BaseConverter {
|
|||
thinking: part.text
|
||||
};
|
||||
// 处理签名
|
||||
if (part.thoughtSignature) {
|
||||
let signature = part.thoughtSignature;
|
||||
// [FIX] 同时检查 thoughtSignature 和 thought_signature
|
||||
const rawSignature = part.thoughtSignature || part.thought_signature;
|
||||
if (rawSignature) {
|
||||
let signature = rawSignature;
|
||||
try {
|
||||
const decoded = Buffer.from(signature, 'base64').toString('utf-8');
|
||||
if (decoded && decoded.length > 0 && !decoded.includes('\ufffd')) {
|
||||
|
|
@ -965,8 +1001,10 @@ export class GeminiConverter extends BaseConverter {
|
|||
input: remappedArgs
|
||||
};
|
||||
// 添加签名(如果存在)
|
||||
if (part.thoughtSignature) {
|
||||
let signature = part.thoughtSignature;
|
||||
// [FIX] 同时检查 thoughtSignature 和 thought_signature
|
||||
const rawSignature = part.thoughtSignature || part.thought_signature;
|
||||
if (rawSignature) {
|
||||
let signature = rawSignature;
|
||||
try {
|
||||
const decoded = Buffer.from(signature, 'base64').toString('utf-8');
|
||||
if (decoded && decoded.length > 0 && !decoded.includes('\ufffd')) {
|
||||
|
|
@ -983,7 +1021,7 @@ export class GeminiConverter extends BaseConverter {
|
|||
}
|
||||
}
|
||||
|
||||
return content;
|
||||
return { content, hasToolUse };
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
|
|
|
|||
|
|
@ -22,7 +22,6 @@ export const MODEL_PREFIX_MAP = {
|
|||
[MODEL_PROVIDER.QWEN_API]: '[Qwen CLI]',
|
||||
[MODEL_PROVIDER.OPENAI_CUSTOM_RESPONSES]: '[OpenAI Responses]',
|
||||
[MODEL_PROVIDER.ANTIGRAVITY]: '[Antigravity]',
|
||||
[MODEL_PROVIDER.ORCHIDS_API]: '[Orchids]',
|
||||
[MODEL_PROVIDER.IFLOW_API]: '[iFlow]',
|
||||
}
|
||||
|
||||
|
|
@ -249,7 +248,6 @@ function findProviderByModelName(modelName) {
|
|||
MODEL_PROVIDER.GEMINI_CLI,
|
||||
MODEL_PROVIDER.ANTIGRAVITY,
|
||||
MODEL_PROVIDER.KIRO_API,
|
||||
MODEL_PROVIDER.ORCHIDS_API,
|
||||
MODEL_PROVIDER.QWEN_API,
|
||||
MODEL_PROVIDER.IFLOW_API
|
||||
];
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ 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 { CodexApiService } from './openai/codex-core.js'; // 导入CodexApiService
|
||||
|
|
@ -407,65 +406,6 @@ 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.isInitialized) {
|
||||
await this.orchidsApiService.initialize();
|
||||
}
|
||||
if (this.isExpiryDateNear()) {
|
||||
return this.orchidsApiService.initializeAuth(true);
|
||||
}
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
async forceRefreshToken() {
|
||||
if (!this.orchidsApiService.isInitialized) {
|
||||
await this.orchidsApiService.initialize();
|
||||
}
|
||||
console.log(`[Orchids] Force refreshing token...`);
|
||||
return this.orchidsApiService.initializeAuth(true);
|
||||
}
|
||||
|
||||
isExpiryDateNear() {
|
||||
return this.orchidsApiService.isExpiryDateNear();
|
||||
}
|
||||
|
||||
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) {
|
||||
|
|
@ -663,9 +603,6 @@ 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;
|
||||
case MODEL_PROVIDER.CODEX_API:
|
||||
serviceInstances[providerKey] = new CodexApiServiceAdapter(config);
|
||||
break;
|
||||
|
|
|
|||
|
|
@ -33,13 +33,6 @@ 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-flash',
|
||||
'gpt-5.2'
|
||||
],
|
||||
'openai-custom': [],
|
||||
'openaiResponses-custom': [],
|
||||
'openai-qwen-oauth': [
|
||||
|
|
|
|||
|
|
@ -17,7 +17,6 @@ export class ProviderPoolManager {
|
|||
'openai-custom': 'gpt-3.5-turbo',
|
||||
'claude-custom': 'claude-3-7-sonnet-20250219',
|
||||
'claude-kiro-oauth': 'claude-haiku-4-5',
|
||||
'claude-orchids-oauth': 'claude-haiku-4-5',
|
||||
'openai-qwen-oauth': 'qwen3-coder-flash',
|
||||
'openai-iflow': 'qwen3-coder-plus',
|
||||
'openai-codex-oauth': 'gpt-5-codex-mini',
|
||||
|
|
@ -98,8 +97,6 @@ export class ProviderPoolManager {
|
|||
configPath = config.IFLOW_OAUTH_CREDS_FILE_PATH;
|
||||
} else if (providerType.startsWith('openai-codex')) {
|
||||
configPath = config.CODEX_OAUTH_CREDS_FILE_PATH;
|
||||
} else if (providerType.startsWith('claude-orchids')) {
|
||||
configPath = config.ORCHIDS_CREDS_FILE_PATH;
|
||||
}
|
||||
|
||||
// console.log(`Checking node ${providerStatus.uuid} (${providerType}) expiry date... configPath: ${configPath}`);
|
||||
|
|
|
|||
|
|
@ -300,11 +300,6 @@ 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,7 +28,6 @@ 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);
|
||||
addToUsedPaths(usedPaths, currentConfig.CODEX_OAUTH_CREDS_FILE_PATH);
|
||||
|
||||
// 使用最新的提供商池数据
|
||||
|
|
@ -46,7 +45,6 @@ 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);
|
||||
addToUsedPaths(usedPaths, provider.CODEX_OAUTH_CREDS_FILE_PATH);
|
||||
}
|
||||
}
|
||||
|
|
@ -218,17 +216,6 @@ 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.CODEX_OAUTH_CREDS_FILE_PATH &&
|
||||
(pathsEqual(relativePath, currentConfig.CODEX_OAUTH_CREDS_FILE_PATH) ||
|
||||
pathsEqual(relativePath, currentConfig.CODEX_OAUTH_CREDS_FILE_PATH.replace(/\\/g, '/')))) {
|
||||
|
|
@ -311,18 +298,6 @@ function getFileUsageInfo(relativePath, fileName, usedPaths, currentConfig) {
|
|||
});
|
||||
}
|
||||
|
||||
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 (provider.CODEX_OAUTH_CREDS_FILE_PATH &&
|
||||
(pathsEqual(relativePath, provider.CODEX_OAUTH_CREDS_FILE_PATH) ||
|
||||
pathsEqual(relativePath, provider.CODEX_OAUTH_CREDS_FILE_PATH.replace(/\\/g, '/')))) {
|
||||
|
|
|
|||
|
|
@ -5,11 +5,9 @@ import {
|
|||
handleQwenOAuth,
|
||||
handleKiroOAuth,
|
||||
handleIFlowOAuth,
|
||||
handleOrchidsOAuth,
|
||||
handleCodexOAuth,
|
||||
batchImportKiroRefreshTokensStream,
|
||||
importAwsCredentials,
|
||||
importOrchidsToken
|
||||
importAwsCredentials
|
||||
} from '../auth/oauth-handlers.js';
|
||||
|
||||
/**
|
||||
|
|
@ -52,11 +50,6 @@ 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 if (providerType === 'openai-codex-oauth') {
|
||||
// Codex OAuth(OAuth2 + PKCE)
|
||||
const result = await handleCodexOAuth(currentConfig, options);
|
||||
|
|
@ -328,285 +321,3 @@ 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}` };
|
||||
}
|
||||
}
|
||||
|
|
@ -66,7 +66,6 @@ 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',
|
||||
CODEX_API: 'openai-codex-oauth',
|
||||
|
|
|
|||
|
|
@ -66,17 +66,6 @@ export const PROVIDER_MAPPINGS = [
|
|||
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']
|
||||
},
|
||||
{
|
||||
// Codex OAuth 配置
|
||||
dirName: 'codex',
|
||||
|
|
|
|||
|
|
@ -89,7 +89,6 @@ const translations = {
|
|||
'dashboard.routing.nodeName.openai': 'OpenAI Custom',
|
||||
'dashboard.routing.nodeName.qwen': 'Qwen OAuth',
|
||||
'dashboard.routing.nodeName.iflow': 'iFlow OAuth',
|
||||
'dashboard.routing.nodeName.orchids': 'Orchids OAuth',
|
||||
'dashboard.routing.nodeName.codex': 'OpenAI Codex OAuth',
|
||||
'dashboard.contact.title': '联系与赞助',
|
||||
'dashboard.contact.wechat': '扫码进群,注明来意',
|
||||
|
|
@ -188,32 +187,6 @@ 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': '配置管理',
|
||||
|
|
@ -335,7 +308,6 @@ const translations = {
|
|||
'upload.providerFilter.gemini': 'Gemini OAuth',
|
||||
'upload.providerFilter.qwen': 'Qwen OAuth',
|
||||
'upload.providerFilter.antigravity': 'Antigravity',
|
||||
'upload.providerFilter.orchids': 'Orchids OAuth',
|
||||
'upload.providerFilter.codex': 'Codex OAuth',
|
||||
'upload.providerFilter.iflow': 'iFlow OAuth',
|
||||
'upload.providerFilter.other': '其他/未识别',
|
||||
|
|
@ -574,7 +546,6 @@ const translations = {
|
|||
'guide.providers.claude.desc': '使用 Claude 官方 API 或第三方代理访问 Claude 系列模型',
|
||||
'guide.providers.openai.desc': '使用 OpenAI 官方 API 或第三方代理访问 GPT 系列模型',
|
||||
'guide.providers.iflow.desc': '通过 iFlow OAuth 认证访问 Qwen、Kimi、DeepSeek、GLM 等模型',
|
||||
'guide.providers.orchids.desc': '通过 Orchids 平台免费使用 Claude Sonnet 4.5 等模型',
|
||||
'guide.client.title': '客户端配置指南',
|
||||
'guide.client.desc': '以下是常见 AI 客户端的配置方法,将 API 端点设置为本服务地址即可使用:',
|
||||
'guide.client.cherry.step1': '打开设置 → 模型服务商',
|
||||
|
|
@ -802,7 +773,6 @@ const translations = {
|
|||
'dashboard.routing.nodeName.openai': 'OpenAI Custom',
|
||||
'dashboard.routing.nodeName.qwen': 'Qwen OAuth',
|
||||
'dashboard.routing.nodeName.iflow': 'iFlow OAuth',
|
||||
'dashboard.routing.nodeName.orchids': 'Orchids OAuth',
|
||||
'dashboard.routing.nodeName.codex': 'OpenAI Codex OAuth',
|
||||
'dashboard.contact.title': 'Contact & Support',
|
||||
'dashboard.contact.wechat': 'Scan to Join Group',
|
||||
|
|
@ -901,32 +871,6 @@ 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',
|
||||
|
|
@ -1048,7 +992,6 @@ const translations = {
|
|||
'upload.providerFilter.gemini': 'Gemini OAuth',
|
||||
'upload.providerFilter.qwen': 'Qwen OAuth',
|
||||
'upload.providerFilter.antigravity': 'Antigravity',
|
||||
'upload.providerFilter.orchids': 'Orchids OAuth',
|
||||
'upload.providerFilter.codex': 'Codex OAuth',
|
||||
'upload.providerFilter.iflow': 'iFlow OAuth',
|
||||
'upload.providerFilter.other': 'Other/Unknown',
|
||||
|
|
@ -1287,7 +1230,6 @@ const translations = {
|
|||
'guide.providers.claude.desc': 'Access Claude models via official API or third-party proxy',
|
||||
'guide.providers.openai.desc': 'Access GPT models via official API or third-party proxy',
|
||||
'guide.providers.iflow.desc': 'Access Qwen, Kimi, DeepSeek, GLM via iFlow OAuth',
|
||||
'guide.providers.orchids.desc': 'Free access to Claude Sonnet 4.5 via Orchids platform',
|
||||
'guide.client.title': 'Client Configuration Guide',
|
||||
'guide.client.desc': 'Here are configuration methods for common AI clients. Set the API endpoint to this service address:',
|
||||
'guide.client.cherry.step1': 'Open Settings → Model Providers',
|
||||
|
|
|
|||
|
|
@ -166,7 +166,6 @@ function getProviderDisplayName(providerType) {
|
|||
'gemini-antigravity': 'Gemini Antigravity',
|
||||
'claude-custom': 'Claude Custom',
|
||||
'claude-kiro-oauth': 'Claude Kiro (OAuth)',
|
||||
'claude-orchids-oauth': 'Claude Orchids (OAuth)',
|
||||
'openai-custom': 'OpenAI Custom',
|
||||
'openaiResponses-custom': 'OpenAI Responses Custom',
|
||||
'openai-qwen-oauth': 'Qwen (OAuth)',
|
||||
|
|
|
|||
|
|
@ -212,7 +212,6 @@ function renderProviders(providers) {
|
|||
'openai-custom',
|
||||
'claude-custom',
|
||||
'claude-kiro-oauth',
|
||||
'claude-orchids-oauth',
|
||||
'openai-qwen-oauth',
|
||||
'openaiResponses-custom',
|
||||
'openai-iflow',
|
||||
|
|
@ -425,22 +424,12 @@ async function openProviderManager(providerType) {
|
|||
*/
|
||||
function generateAuthButton(providerType) {
|
||||
// 只为支持OAuth的提供商显示授权按钮
|
||||
const oauthProviders = ['gemini-cli-oauth', 'gemini-antigravity', 'openai-qwen-oauth', 'claude-kiro-oauth', 'claude-orchids-oauth', 'openai-iflow', 'openai-codex-oauth'];
|
||||
const oauthProviders = ['gemini-cli-oauth', 'gemini-antigravity', 'openai-qwen-oauth', 'claude-kiro-oauth', 'openai-iflow', 'openai-codex-oauth'];
|
||||
|
||||
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>
|
||||
`;
|
||||
}
|
||||
|
||||
// Codex 提供商使用特殊图标
|
||||
if (providerType === 'openai-codex-oauth') {
|
||||
return `
|
||||
|
|
@ -470,217 +459,9 @@ async function handleGenerateAuthUrl(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 - 提供商类型
|
||||
|
|
|
|||
|
|
@ -844,12 +844,6 @@ function detectProviderFromPath(filePath) {
|
|||
displayName: 'Gemini Antigravity',
|
||||
shortName: 'antigravity'
|
||||
},
|
||||
{
|
||||
patterns: ['configs/orchids/', '/orchids/'],
|
||||
providerType: 'claude-orchids-oauth',
|
||||
displayName: 'Orchids OAuth',
|
||||
shortName: 'orchids-oauth'
|
||||
},
|
||||
{
|
||||
patterns: ['configs/codex/', '/codex/'],
|
||||
providerType: 'openai-codex-oauth',
|
||||
|
|
|
|||
|
|
@ -7,8 +7,8 @@
|
|||
<i class="fas fa-bars"></i>
|
||||
</button>
|
||||
<div class="header-controls" id="headerControls">
|
||||
<a href="https://pay.ldxp.cn/shop/N0IK02WR" target="_blank" rel="noopener noreferrer" class="kiro-buy-link" title="KIRO账号购买">
|
||||
<i class="fas fa-shopping-cart"></i> <span>KIRO账号购买</span>
|
||||
<a href="https://pay.ldxp.cn/shop/N0IK02WR" target="_blank" rel="noopener noreferrer" class="kiro-buy-link" title="多渠道账号购买">
|
||||
<i class="fas fa-shopping-cart"></i> <span>多渠道账号购买</span>
|
||||
</a>
|
||||
<span class="status-badge" id="serverStatus">
|
||||
<i class="fas fa-circle"></i> <span class="status-text" data-i18n="header.status.connecting">连接中...</span>
|
||||
|
|
|
|||
|
|
@ -58,10 +58,6 @@
|
|||
<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>
|
||||
<button type="button" class="provider-tag" data-value="openai-codex-oauth">
|
||||
<i class="fas fa-code"></i>
|
||||
<span>OpenAI Codex OAuth</span>
|
||||
|
|
@ -117,10 +113,6 @@
|
|||
<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>
|
||||
<button type="button" class="provider-tag" data-value="openai-codex-oauth">
|
||||
<i class="fas fa-code"></i>
|
||||
<span>OpenAI Codex OAuth</span>
|
||||
|
|
|
|||
|
|
@ -497,59 +497,6 @@
|
|||
</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 class="routing-example-card" data-provider="openai-codex-oauth-card">
|
||||
<div class="routing-card-header">
|
||||
<i class="fas fa-code"></i>
|
||||
|
|
|
|||
|
|
@ -334,7 +334,6 @@
|
|||
.guide-provider-icon.claude { color: #d97706; }
|
||||
.guide-provider-icon.openai { color: #10a37f; }
|
||||
.guide-provider-icon.iflow { color: #3b82f6; }
|
||||
.guide-provider-icon.orchids { color: #22c55e; }
|
||||
|
||||
.guide-provider-name {
|
||||
font-weight: 600;
|
||||
|
|
|
|||
|
|
@ -23,7 +23,6 @@
|
|||
<option value="gemini-cli-oauth" data-i18n="upload.providerFilter.gemini">Gemini OAuth</option>
|
||||
<option value="openai-qwen-oauth" data-i18n="upload.providerFilter.qwen">Qwen OAuth</option>
|
||||
<option value="gemini-antigravity" data-i18n="upload.providerFilter.antigravity">Antigravity</option>
|
||||
<option value="claude-orchids-oauth" data-i18n="upload.providerFilter.orchids">Orchids OAuth</option>
|
||||
<option value="openai-codex-oauth" data-i18n="upload.providerFilter.codex">Codex OAuth</option>
|
||||
<option value="openai-iflow-oauth" data-i18n="upload.providerFilter.iflow">iFlow OAuth</option>
|
||||
<option value="other" data-i18n="upload.providerFilter.other">其他/未识别</option>
|
||||
|
|
|
|||
Loading…
Reference in a new issue