From 05df61df74e016e57c1a70c6f7b12f22146e5345 Mon Sep 17 00:00:00 2001 From: hex2077 Date: Tue, 3 Mar 2026 20:05:51 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=20TLS=20Sidecar=20?= =?UTF-8?q?=E6=94=AF=E6=8C=81=E5=B9=B6=E9=87=8D=E6=9E=84=E5=89=8D=E7=AB=AF?= =?UTF-8?q?=E6=8F=90=E4=BE=9B=E5=95=86=E9=85=8D=E7=BD=AE=E7=B3=BB=E7=BB=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 TLS Sidecar 功能文档,支持绕过 Grok 等服务的 Cloudflare 403 封锁 - 重构前端提供商配置管理,实现动态配置缓存和统一数据源 - 升级配置文件管理界面 UI,优化信息展示和交互体验 - 改进 Claude Kiro 工具调用流式响应,实时推送 content_block 事件 - 修复 Codex 配额重置时间格式问题 主要变更: - README 文档新增 TLS Sidecar 配置说明和使用指南 - 新增 tutorial-manager.js 模块,动态渲染 OAuth 授权路径 - routing-examples.js 支持动态生成路径路由示例卡片 - upload-config-manager.js 重构列表项布局,支持节点关联信息展示 - config-manager、models-manager、usage-manager 统一使用提供商配置缓存 - i18n 新增多语言翻译键,更新 Gemini 模型版本信息 --- README-JA.md | 43 ++ README-ZH.md | 43 ++ README.md | 43 ++ src/providers/claude/claude-kiro.js | 167 +++++-- src/providers/openai/codex-core.js | 2 +- src/ui-modules/config-scanner.js | 12 + static/app/app.js | 5 + static/app/config-manager.js | 105 ++-- static/app/i18n.js | 18 +- static/app/models-manager.js | 43 +- static/app/provider-manager.js | 45 +- static/app/routing-examples.js | 182 ++++++- static/app/tutorial-manager.js | 57 +++ static/app/upload-config-manager.js | 318 +++++++++--- static/app/usage-manager.js | 75 ++- static/app/utils.js | 109 ++++- static/components/section-dashboard.html | 479 +------------------ static/components/section-tutorial.html | 51 +- static/components/section-upload-config.css | 351 ++++++++++++-- static/components/section-upload-config.html | 8 +- 20 files changed, 1360 insertions(+), 796 deletions(-) create mode 100644 static/app/tutorial-manager.js diff --git a/README-JA.md b/README-JA.md index a7d0c08..66bdea7 100644 --- a/README-JA.md +++ b/README-JA.md @@ -68,6 +68,7 @@ ### 🚀 制限を突破、効率を向上 * **公式制限の回避**:OAuth認証メカニズムを利用して、Gemini、Antigravityなどの無料APIのレート制限と割り当て制限を効果的に突破 +* **TLS 指紋の回避**:内蔵の TLS Sidecar (Go uTLS) によりブラウザの特徴をシミュレートし、Grok などのサービスの Cloudflare 403 ブロックを効果的に回避 * **無料高度モデル**:Kiro APIモードでClaude Opus 4.5を無料使用、Qwen OAuthモードでQwen3 Coder Plusを使用し、使用コストを削減 * **インテリジェントアカウントプールスケジューリング**:マルチアカウントポーリング、自動フェイルオーバー、設定ダウングレードをサポートし、99.9%のサービス可用性を保証 @@ -151,6 +152,15 @@ docker compose up -d * ✅ デフォルトモデルプロバイダーのリアルタイム切り替え * ✅ 健全性ステータスとリアルタイムリクエストログの監視 +#### 4. ローカル環境の準備 (非 Docker ユーザー) +ローカルで直接実行(スクリプトまたは Node.js 経由)し、Grok などのサービスの TLS 検出を回避する必要がある場合は、以下を確認してください: +* ✅ **Go 言語のインストール**:[Go 公式サイト](https://go.dev/) からダウンロードしてインストール (1.20+)。 +* ✅ **Sidecar の手動ビルド**:以下のコマンドを実行して TLS プロキシコンポーネントをビルドします: + ```bash + cd tls-sidecar && go build -o tls-sidecar && cd .. + ``` + *注意:このバイナリファイルがビルドされていない場合、TLS Sidecar 機能は実行ファイルが見つからないため起動に失敗します。* + #### スクリプト実行例 ``` ======================================== @@ -521,6 +531,38 @@ curl http://localhost:3000/ollama/api/chat \ - フォールバックはプロトコル互換タイプ間でのみ発生します(例:`gemini-*` 間、`claude-*` 間) - システムは自動的にターゲットProvider Typeがリクエストされたモデルをサポートしているか確認します +#### 5. TLS Sidecar (Bypass 403/Cloudflare) + +Grok などの TLS 指紋(JA3/JA4)を厳密に検証するサービスに対して、本プロジェクトは Go uTLS ベースの Sidecar プロキシを統合しています。これにより、ブラウザの TLS 特徴をシミュレートし、403 Forbidden エラーを効果的に解決します。 + +**設定手順**: + +1. **バイナリのビルド**: + TLS シミュレーションには Go 言語のサポートが必要です。まず sidecar をビルドする必要があります: + ```bash + cd tls-sidecar + go build -o tls-sidecar + ``` + *Windows ユーザーは、ビルド後、生成された `tls-sidecar.exe` が `tls-sidecar/` またはルートディレクトリにあることを確認してください。* + +2. **設定の有効化**: + Web UI の「設定管理」ページで **TLS Sidecar** を有効にするか、`configs/config.json` を編集します: + ```json + { + "TLS_SIDECAR_ENABLED": true, + "TLS_SIDECAR_PORT": 9090 + } + ``` + +3. **動作原理**: + - 有効にすると、システムは自動的に Go プロセスを起動し管理します。 + - 特定のプロバイダー(Grok など)へのリクエストは、自動的に Sidecar にルーティングされます。 + - Sidecar は最新の Chrome 指紋を使用して TLS ハンドシェイクを行い、HTTP/2 の自動ネゴシエーションをサポートします。 + +**注意事項**: +- ローカル実行には Go 環境 (1.20+) が必要です。 +- **Docker ユーザー**: イメージには既にプリビルド済みのバイナリが含まれています。設定で有効にするだけでよく、手動でのビルドは不要です。 + --- @@ -664,6 +706,7 @@ kill -9 **問題の説明**:APIリクエストが403 Forbiddenエラーを返します。 **解決策**: +- **TLS Sidecar の有効化**:Grok などのサービスにおいて、403 エラーは TLS 指紋のブロックが原因であることが多いです。[高度な設定 - TLS Sidecar](#5-tls-sidecar-bypass-403cloudflare) を参照して Sidecar を有効にし、ビルドしてください。 - **ノード状態を確認**:Web UIの「プロバイダープール」ページでノード状態が正常(ヘルスチェック合格)であれば、このエラーは無視できます。システムが自動的に処理します - **アカウント権限を確認**:使用しているアカウントがリクエストされたモデルまたはサービスにアクセスする権限があることを確認 - **API Key権限を確認**:一部のプロバイダーのAPI Keyにはアクセス範囲の制限がある場合があります。Keyに十分な権限があることを確認 diff --git a/README-ZH.md b/README-ZH.md index b82d85d..3076146 100644 --- a/README-ZH.md +++ b/README-ZH.md @@ -67,6 +67,7 @@ ### 🚀 突破限制,提升效率 * **绕过官方限制**:利用 OAuth 授权机制,有效突破 Gemini, Antigravity 等服务的免费 API 速率和配额限制 +* **TLS 指纹绕过**:内置 TLS Sidecar (Go uTLS) 模拟浏览器特征,有效绕过 Grok 等服务的 Cloudflare 403 封锁 * **免费高级模型**:通过 Kiro API 模式免费使用 Claude Opus 4.5,通过 Qwen OAuth 模式使用 Qwen3 Coder Plus,降低使用成本 * **账号池智能调度**:支持多账号轮询、自动故障转移和配置降级,确保 99.9% 服务可用性 @@ -150,6 +151,15 @@ docker compose up -d * ✅ 实时切换默认模型提供商 * ✅ 监控健康状态和实时请求日志 +#### 4. 本地环境准备 (非 Docker 用户) +如果您是在本地直接运行(通过脚本或 Node.js),且需要绕过 Grok 等服务的 TLS 检测,请务必: +* ✅ **安装 Go 语言环境**:前往 [Go 官网](https://go.dev/) 下载并安装 (1.20+)。 +* ✅ **手动编译 Sidecar**:执行以下命令编译 TLS 代理组件: + ```bash + cd tls-sidecar && go build -o tls-sidecar && cd .. + ``` + *注意:若未编译此二进制文件,TLS Sidecar 功能将因找不到执行文件而无法启动。* + #### 脚本执行示例 ``` ======================================== @@ -520,6 +530,38 @@ curl http://localhost:3000/ollama/api/chat \ - Fallback 只会在协议兼容的类型之间进行(如 `gemini-*` 之间、`claude-*` 之间) - 系统会自动检查目标 Provider Type 是否支持当前请求的模型 +#### 5. TLS Sidecar (Bypass 403/Cloudflare) + +针对 Grok 等对 TLS 指纹(JA3/JA4)校验严格的服务,本项目集成了基于 Go uTLS 的 Sidecar 代理,通过模拟浏览器 TLS 特征有效解决 403 Forbidden 报错。 + +**配置说明**: + +1. **编译二进制文件**: + 由于 TLS 模拟需要 Go 语言支持,您需要先编译 sidecar: + ```bash + cd tls-sidecar + go build -o tls-sidecar + ``` + *Windows 用户编译后请确保生成的 `tls-sidecar.exe` 位于 `tls-sidecar/` 或根目录。* + +2. **启用配置**: + 在 Web UI 的“配置管理”中开启 **TLS Sidecar**,或修改 `configs/config.json`: + ```json + { + "TLS_SIDECAR_ENABLED": true, + "TLS_SIDECAR_PORT": 9090 + } + ``` + +3. **工作原理**: + - 开启后系统自动启动并管理该 Go 进程。 + - 针对特定提供商(如 Grok)的请求会自动路由至 Sidecar。 + - Sidecar 使用 Chrome 最新指纹进行 TLS 握手,支持 HTTP/2 自动协商。 + +**注意事项**: +- 本地运行需安装 Go 环境(1.20+)。 +- **Docker 用户**:镜像已内置编译好的二进制,只需在配置中开启即可,无需手动编译。 + --- @@ -663,6 +705,7 @@ kill -9 **问题描述**:API 请求返回 403 Forbidden 错误。 **解决方案**: +- **开启 TLS Sidecar**:针对 Grok 等服务,403 通常是因为 TLS 指纹被屏蔽。请参考 [高级配置 - TLS Sidecar](#5-tls-sidecar-bypass-403cloudflare) 开启并编译 Sidecar。 - **检查节点状态**:如果在 Web UI 的"提供商池"页面中看到节点状态正常(健康检查通过),则可以忽略此报错,系统会自动处理 - **检查账号权限**:确认使用的账号有权限访问请求的模型或服务 - **检查 API Key 权限**:某些提供商的 API Key 可能有访问范围限制,确保 Key 有足够权限 diff --git a/README.md b/README.md index 3b59662..28d7550 100644 --- a/README.md +++ b/README.md @@ -68,6 +68,7 @@ ### 🚀 Break Through Limitations, Improve Efficiency * **Bypass Official Restrictions**: Utilize OAuth authorization mechanism to effectively break through rate and quota limits of services like Gemini, Antigravity +* **TLS Fingerprint Bypass**: Built-in TLS Sidecar (Go uTLS) to simulate browser features, effectively bypassing Cloudflare 403 blocks for services like Grok * **Free Advanced Models**: Use Claude Opus 4.5 for free via Kiro API mode, use Qwen3 Coder Plus via Qwen OAuth mode, reducing usage costs * **Intelligent Account Pool Scheduling**: Support multi-account polling, automatic failover, and configuration degradation, ensuring 99.9% service availability @@ -151,6 +152,15 @@ Go to the **"Configuration"** page, you can: * ✅ Switch default model providers in real-time * ✅ Monitor health status and real-time request logs +#### 4. Local Environment Preparation (Non-Docker Users) +If you are running directly on your local machine (via script or Node.js) and need to bypass TLS detection for services like Grok, please ensure: +* ✅ **Install Go Language**: Go to the [official Go website](https://go.dev/) to download and install (1.20+). +* ✅ **Manually Compile Sidecar**: Execute the following command to compile the TLS proxy component: + ```bash + cd tls-sidecar && go build -o tls-sidecar && cd .. + ``` + *Note: If this binary file is not compiled, the TLS Sidecar feature will fail to start as it cannot find the executable.* + #### Script Execution Example ``` ======================================== @@ -521,6 +531,38 @@ When all accounts under a Provider Type (e.g., `gemini-cli-oauth`) are exhausted - Fallback only occurs between protocol-compatible types (e.g., between `gemini-*`, between `claude-*`) - The system automatically checks if the target Provider Type supports the requested model +#### 5. TLS Sidecar (Bypass 403/Cloudflare) + +For services like Grok that strictly validate TLS fingerprints (JA3/JA4), this project integrates a Sidecar proxy based on Go uTLS, which effectively solves 403 Forbidden errors by simulating browser TLS features. + +**Configuration Instructions**: + +1. **Compile the Binary**: + Since TLS simulation requires Go language support, you need to compile the sidecar first: + ```bash + cd tls-sidecar + go build -o tls-sidecar + ``` + *Windows users, after compiling, ensure the generated `tls-sidecar.exe` is located in the `tls-sidecar/` or the root directory.* + +2. **Enable Configuration**: + Enable **TLS Sidecar** in the "Configuration" page of the Web UI, or modify `configs/config.json`: + ```json + { + "TLS_SIDECAR_ENABLED": true, + "TLS_SIDECAR_PORT": 9090 + } + ``` + +3. **How It Works**: + - When enabled, the system automatically starts and manages the Go process. + - Requests for specific providers (like Grok) are automatically routed to the Sidecar. + - The Sidecar uses the latest Chrome fingerprint for TLS handshakes and supports automatic HTTP/2 negotiation. + +**Notes**: +- Local running requires a Go environment (1.20+). +- **Docker Users**: The image already includes the pre-compiled binary; just enable it in the configuration, no manual compilation required. + --- @@ -664,6 +706,7 @@ Or modify the port configuration in `configs/config.json` to use a different por **Problem Description**: API requests return 403 Forbidden error. **Solutions**: +- **Enable TLS Sidecar**: For services like Grok, 403 is often due to TLS fingerprint blocking. Please refer to [Advanced Configuration - TLS Sidecar](#5-tls-sidecar-bypass-403cloudflare) to enable and compile the Sidecar. - **Check Node Status**: If you see the node status is normal (health check passed) on the "Provider Pools" page in Web UI, you can ignore this error as the system will handle it automatically - **Check Account Permissions**: Confirm the account has permission to access the requested model or service - **Check API Key Permissions**: Some providers' API Keys may have access scope restrictions; ensure the Key has sufficient permissions diff --git a/src/providers/claude/claude-kiro.js b/src/providers/claude/claude-kiro.js index 74d9648..24c5921 100644 --- a/src/providers/claude/claude-kiro.js +++ b/src/providers/claude/claude-kiro.js @@ -2248,6 +2248,7 @@ async saveCredentialsToFile(filePath, newData) { let outputTokens = 0; const toolCalls = []; let currentToolCall = null; // 用于累积结构化工具调用 + const toolUseBlockIndexes = new Map(); // toolUseId -> content block index const estimatedInputTokens = this.estimateInputTokens(requestBody); @@ -2384,46 +2385,103 @@ async saveCredentialsToFile(filePath, newData) { yield* pushEvents(events); } else if (event.type === 'toolUse') { const tc = event.toolUse; + const toolEvents = []; + // 统计工具调用的内容到 totalContent(用于 token 计算) - if (tc.name) { - totalContent += tc.name; - } - if (tc.input) { - totalContent += tc.input; - } + if (tc.name) totalContent += tc.name; + if (tc.input) totalContent += tc.input; + // 工具调用事件(包含 name 和 toolUseId) if (tc.name && tc.toolUseId) { - // 检查是否是同一个工具调用的续传(相同 toolUseId) + // 遇到工具调用时,立即关闭文本块,避免前端等待到流结束才看到 content_block_stop + toolEvents.push(...stopBlock(streamState.textBlockIndex)); + + // 同一工具调用续传 if (currentToolCall && currentToolCall.toolUseId === tc.toolUseId) { - // 同一个工具调用,累积 input currentToolCall.input += tc.input || ''; } else { - // 不同的工具调用 - // 如果有未完成的工具调用,先保存它 + // 切换到新的工具调用前,先收尾旧调用 if (currentToolCall) { + const prevBlockIndex = toolUseBlockIndexes.get(currentToolCall.toolUseId); + let parsedInput = currentToolCall.input; try { - currentToolCall.input = JSON.parse(currentToolCall.input); + parsedInput = JSON.parse(currentToolCall.input); } catch (e) { // input 不是有效 JSON,保持原样 } - toolCalls.push(currentToolCall); + toolCalls.push({ + toolUseId: currentToolCall.toolUseId, + name: currentToolCall.name, + input: parsedInput + }); + if (prevBlockIndex != null) { + toolEvents.push({ type: "content_block_stop", index: prevBlockIndex }); + toolUseBlockIndexes.delete(currentToolCall.toolUseId); + } } - // 开始新的工具调用 + + const blockIndex = streamState.nextBlockIndex++; + toolUseBlockIndexes.set(tc.toolUseId, blockIndex); + toolEvents.push({ + type: "content_block_start", + index: blockIndex, + content_block: { + type: "tool_use", + id: tc.toolUseId || `tool_${uuidv4()}`, + name: tc.name, + input: {} + } + }); + currentToolCall = { toolUseId: tc.toolUseId, name: tc.name, - input: tc.input || '' + input: '' }; + currentToolCall.input += tc.input || ''; } - // 如果这个事件包含 stop,完成工具调用 - if (tc.stop) { + + // 实时向前端推送工具参数增量 + if (tc.input) { + const blockIndex = toolUseBlockIndexes.get(tc.toolUseId); + if (blockIndex != null) { + toolEvents.push({ + type: "content_block_delta", + index: blockIndex, + delta: { + type: "input_json_delta", + partial_json: tc.input + } + }); + } + } + + // 如果这个事件包含 stop,立即结束当前工具块 + if (tc.stop && currentToolCall) { + let parsedInput = currentToolCall.input; try { - currentToolCall.input = JSON.parse(currentToolCall.input); - } catch (e) {} - toolCalls.push(currentToolCall); + parsedInput = JSON.parse(currentToolCall.input); + } catch (e) { + // input 不是有效 JSON,保持原样 + } + toolCalls.push({ + toolUseId: currentToolCall.toolUseId, + name: currentToolCall.name, + input: parsedInput + }); + + const blockIndex = toolUseBlockIndexes.get(currentToolCall.toolUseId); + if (blockIndex != null) { + toolEvents.push({ type: "content_block_stop", index: blockIndex }); + toolUseBlockIndexes.delete(currentToolCall.toolUseId); + } currentToolCall = null; } } + + if (toolEvents.length > 0) { + yield* pushEvents(toolEvents); + } } else if (event.type === 'toolUseInput') { // 工具调用的 input 续传事件 // 统计 input 内容到 totalContent(用于 token 计算) @@ -2432,16 +2490,38 @@ async saveCredentialsToFile(filePath, newData) { } if (currentToolCall) { currentToolCall.input += event.input || ''; + const blockIndex = toolUseBlockIndexes.get(currentToolCall.toolUseId); + if (blockIndex != null && event.input) { + yield* pushEvents([{ + type: "content_block_delta", + index: blockIndex, + delta: { + type: "input_json_delta", + partial_json: event.input + } + }]); + } } } else if (event.type === 'toolUseStop') { // 工具调用结束事件 if (currentToolCall && event.stop) { + let parsedInput = currentToolCall.input; try { - currentToolCall.input = JSON.parse(currentToolCall.input); + parsedInput = JSON.parse(currentToolCall.input); } catch (e) { // input 不是有效 JSON,保持原样 } - toolCalls.push(currentToolCall); + toolCalls.push({ + toolUseId: currentToolCall.toolUseId, + name: currentToolCall.name, + input: parsedInput + }); + + const blockIndex = toolUseBlockIndexes.get(currentToolCall.toolUseId); + if (blockIndex != null) { + yield* pushEvents([{ type: "content_block_stop", index: blockIndex }]); + toolUseBlockIndexes.delete(currentToolCall.toolUseId); + } currentToolCall = null; } } @@ -2449,10 +2529,20 @@ async saveCredentialsToFile(filePath, newData) { // 处理未完成的工具调用(如果流提前结束) if (currentToolCall) { + let parsedInput = currentToolCall.input; try { - currentToolCall.input = JSON.parse(currentToolCall.input); + parsedInput = JSON.parse(currentToolCall.input); } catch (e) {} - toolCalls.push(currentToolCall); + toolCalls.push({ + toolUseId: currentToolCall.toolUseId, + name: currentToolCall.name, + input: parsedInput + }); + const blockIndex = toolUseBlockIndexes.get(currentToolCall.toolUseId); + if (blockIndex != null) { + yield* pushEvents([{ type: "content_block_stop", index: blockIndex }]); + toolUseBlockIndexes.delete(currentToolCall.toolUseId); + } currentToolCall = null; } @@ -2501,36 +2591,7 @@ async saveCredentialsToFile(filePath, newData) { } } - // 3. 处理工具调用(如果有) - if (toolCalls.length > 0) { - const baseIndex = streamState.nextBlockIndex; - for (let i = 0; i < toolCalls.length; i++) { - const tc = toolCalls[i]; - const blockIndex = baseIndex + i; - - yield { - type: "content_block_start", - index: blockIndex, - content_block: { - type: "tool_use", - id: tc.toolUseId || `tool_${uuidv4()}`, - name: tc.name, - input: {} - } - }; - - yield { - type: "content_block_delta", - index: blockIndex, - delta: { - type: "input_json_delta", - partial_json: typeof tc.input === 'string' ? tc.input : JSON.stringify(tc.input || {}) - } - }; - - yield { type: "content_block_stop", index: blockIndex }; - } - } + // 3. 工具调用在流中实时发送,这里不再批量补发 // 计算 output tokens const contentBlocksForCount = thinkingRequested diff --git a/src/providers/openai/codex-core.js b/src/providers/openai/codex-core.js index 1642524..1280e76 100644 --- a/src/providers/openai/codex-core.js +++ b/src/providers/openai/codex-core.js @@ -646,7 +646,7 @@ export class CodexApiService { if (primaryWindow) { // remaining = 1 - (used_percent / 100) const remaining = 1 - (primaryWindow.used_percent || 0) / 100; - const resetTime = primaryWindow.reset_at ? new Date(primaryWindow.reset_at * 1000).toDateString() : null; + const resetTime = primaryWindow.reset_at ? new Date(primaryWindow.reset_at * 1000).toISOString() : null; // 为所有 Codex 模型设置相同的配额信息 const codexModels = ['default']; diff --git a/src/ui-modules/config-scanner.js b/src/ui-modules/config-scanner.js index 46f1734..1e40533 100644 --- a/src/ui-modules/config-scanner.js +++ b/src/ui-modules/config-scanner.js @@ -247,6 +247,8 @@ function getFileUsageInfo(relativePath, fileName, usedPaths, currentConfig) { location: `Gemini OAuth credentials (node ${index + 1})`, providerType: providerType, providerIndex: index, + nodeName: provider.customName, + uuid: provider.uuid, configKey: 'GEMINI_OAUTH_CREDS_FILE_PATH' }); } @@ -259,6 +261,8 @@ function getFileUsageInfo(relativePath, fileName, usedPaths, currentConfig) { location: `Kiro OAuth credentials (node ${index + 1})`, providerType: providerType, providerIndex: index, + nodeName: provider.customName, + uuid: provider.uuid, configKey: 'KIRO_OAUTH_CREDS_FILE_PATH' }); } @@ -271,6 +275,8 @@ function getFileUsageInfo(relativePath, fileName, usedPaths, currentConfig) { location: `Qwen OAuth credentials (node ${index + 1})`, providerType: providerType, providerIndex: index, + nodeName: provider.customName, + uuid: provider.uuid, configKey: 'QWEN_OAUTH_CREDS_FILE_PATH' }); } @@ -283,6 +289,8 @@ function getFileUsageInfo(relativePath, fileName, usedPaths, currentConfig) { location: `Antigravity OAuth credentials (node ${index + 1})`, providerType: providerType, providerIndex: index, + nodeName: provider.customName, + uuid: provider.uuid, configKey: 'ANTIGRAVITY_OAUTH_CREDS_FILE_PATH' }); } @@ -295,6 +303,8 @@ function getFileUsageInfo(relativePath, fileName, usedPaths, currentConfig) { location: `iFlow Token (node ${index + 1})`, providerType: providerType, providerIndex: index, + nodeName: provider.customName, + uuid: provider.uuid, configKey: 'IFLOW_TOKEN_FILE_PATH' }); } @@ -307,6 +317,8 @@ function getFileUsageInfo(relativePath, fileName, usedPaths, currentConfig) { location: `Codex OAuth credentials (node ${index + 1})`, providerType: providerType, providerIndex: index, + nodeName: provider.customName, + uuid: provider.uuid, configKey: 'CODEX_OAUTH_CREDS_FILE_PATH' }); } diff --git a/static/app/app.js b/static/app/app.js index 967d22f..26546af 100644 --- a/static/app/app.js +++ b/static/app/app.js @@ -82,6 +82,10 @@ import { togglePlugin } from './plugin-manager.js'; +import { + initTutorialManager +} from './tutorial-manager.js'; + /** * 加载初始数据 */ @@ -118,6 +122,7 @@ function initApp() { initUsageManager(); // 初始化用量管理功能 initImageZoom(); // 初始化图片放大功能 initPluginManager(); // 初始化插件管理功能 + initTutorialManager(); // 初始化教程管理功能 initMobileMenu(); // 初始化移动端菜单 loadInitialData(); diff --git a/static/app/config-manager.js b/static/app/config-manager.js index 5f2e941..a4af0e2 100644 --- a/static/app/config-manager.js +++ b/static/app/config-manager.js @@ -5,6 +5,71 @@ import { handleProviderChange, handleGeminiCredsTypeChange, handleKiroCredsTypeC import { loadProviders } from './provider-manager.js'; import { t } from './i18n.js'; +// 提供商配置缓存 +let currentProviderConfigs = null; + +/** + * 更新提供商配置并重新渲染配置页面的提供商选择标签 + * @param {Array} configs - 提供商配置列表 + */ +function updateConfigProviderConfigs(configs) { + currentProviderConfigs = configs; + + // 渲染基础设置中的模型提供商选择 + const modelProviderEl = document.getElementById('modelProvider'); + if (modelProviderEl) { + renderProviderTags(modelProviderEl, configs, true); + } + + // 渲染代理设置中的提供商选择 + const proxyProvidersEl = document.getElementById('proxyProviders'); + if (proxyProvidersEl) { + renderProviderTags(proxyProvidersEl, configs, false); + } + + // 重新加载当前配置以恢复选中状态 + loadConfiguration(); +} + +/** + * 渲染提供商标签按钮 + * @param {HTMLElement} container - 容器元素 + * @param {Array} configs - 提供商配置列表 + * @param {boolean} isRequired - 是否至少需要选择一个(用于点击事件逻辑) + */ +function renderProviderTags(container, configs, isRequired) { + // 过滤掉不可见的提供商 + const visibleConfigs = configs.filter(c => c.visible !== false); + + container.innerHTML = visibleConfigs.map(c => ` + + `).join(''); + + // 为新生成的标签添加点击事件 + const tags = container.querySelectorAll('.provider-tag'); + tags.forEach(tag => { + tag.addEventListener('click', (e) => { + e.preventDefault(); + const isSelected = tag.classList.contains('selected'); + + if (isRequired) { + const selectedCount = container.querySelectorAll('.provider-tag.selected').length; + // 如果当前是选中状态且只剩一个选中的,不允许取消 + if (isSelected && selectedCount === 1) { + showToast(t('common.warning'), t('config.modelProviderRequired'), 'warning'); + return; + } + } + + // 切换选中状态 + tag.classList.toggle('selected'); + }); + }); +} + /** * 加载配置 */ @@ -24,7 +89,7 @@ async function loadConfiguration() { if (portEl) portEl.value = data.SERVER_PORT || 3000; if (modelProviderEl) { - // 处理多选 MODEL_PROVIDER (标签按钮) + // 处理多选 MODEL_PROVIDER const providers = Array.isArray(data.DEFAULT_MODEL_PROVIDERS) ? data.DEFAULT_MODEL_PROVIDERS : (typeof data.MODEL_PROVIDER === 'string' ? data.MODEL_PROVIDER.split(',') : []); @@ -44,28 +109,6 @@ async function loadConfiguration() { if (!anySelected && tags.length > 0) { tags[0].classList.add('selected'); } - - // 为标签按钮添加点击事件监听 - tags.forEach(tag => { - // 移除旧的监听器(通过克隆节点) - const newTag = tag.cloneNode(true); - tag.parentNode.replaceChild(newTag, tag); - - newTag.addEventListener('click', (e) => { - e.preventDefault(); - const isSelected = newTag.classList.contains('selected'); - const selectedCount = modelProviderEl.querySelectorAll('.provider-tag.selected').length; - - // 如果当前是选中状态且只剩一个选中的,不允许取消 - if (isSelected && selectedCount === 1) { - showToast(t('common.warning'), t('config.modelProviderRequired'), 'warning'); - return; - } - - // 切换选中状态 - newTag.classList.toggle('selected'); - }); - }); } if (systemPromptEl) systemPromptEl.value = data.systemPrompt || ''; @@ -143,19 +186,6 @@ async function loadConfiguration() { tag.classList.remove('selected'); } }); - - // 为代理提供商标签按钮添加点击事件监听 - proxyTags.forEach(tag => { - // 移除旧的监听器(通过克隆节点) - const newTag = tag.cloneNode(true); - tag.parentNode.replaceChild(newTag, tag); - - newTag.addEventListener('click', (e) => { - e.preventDefault(); - // 代理提供商可以全部取消选择,所以不需要检查最少选择数量 - newTag.classList.toggle('selected'); - }); - }); } // 加载日志配置 @@ -320,5 +350,6 @@ async function saveConfiguration() { export { loadConfiguration, - saveConfiguration + saveConfiguration, + updateConfigProviderConfigs }; diff --git a/static/app/i18n.js b/static/app/i18n.js index f29542c..3b5924c 100644 --- a/static/app/i18n.js +++ b/static/app/i18n.js @@ -114,7 +114,7 @@ const translations = { 'oauth.modal.step4.qwen': '授权有效期: {min} 分钟', 'oauth.modal.step4.google': '授权完成后,凭据文件会自动保存', 'oauth.modal.urlLabel': '授权链接:', - 'oauth.modal.copyTitle': '复制链接', + 'oauth.modal.copyTitle': '复制', 'oauth.modal.openInBrowser': '在浏览器中打开', 'oauth.manual.title': '自动监听受阻?', 'oauth.manual.desc': '如果授权窗口重定向后显示“无法访问”,请将该窗口地址栏的 完整 URL 粘贴到下方:', @@ -402,6 +402,7 @@ const translations = { 'upload.refresh.success': '刷新成功', 'upload.action.view.failed': '查看失败', 'upload.action.delete.failed': '删除失败', + 'upload.action.quickLink': '一键关联', 'upload.config.notExist': '配置文件不存在', 'upload.link.identifying': '正在识别提供商类型...', 'upload.link.failed.identify': '无法识别配置文件对应的提供商类型', @@ -639,7 +640,7 @@ const translations = { 'guide.providers.badge.experimental': '实验性', 'guide.providers.badge.free': '免费使用', 'guide.providers.badge.official': '官方 API', - 'guide.providers.gemini.desc': '通过 Google OAuth 认证访问 Gemini 模型,支持 gemini-2.0-flash-exp 等模型', + 'guide.providers.gemini.desc': '通过 Google OAuth 认证访问 Gemini 模型,支持 gemini-3.1-pro-preview 等模型', 'guide.providers.antigravity.desc': '通过 Google 内部接口访问 Gemini 3 Pro、Claude Sonnet 4.5 等模型', 'guide.providers.kiro.desc': '通过 Kiro 客户端免费使用 Claude Opus 4.5、Claude Sonnet 4.5 等模型', 'guide.providers.qwen.desc': '通过阿里云 OAuth 认证访问 Qwen3 Coder Plus 等模型', @@ -713,8 +714,8 @@ const translations = { 'tutorial.config.desc': '所有配置文件都存放在 configs/ 目录下。主要配置文件包括:', 'tutorial.config.badge.required': '必需', 'tutorial.config.badge.optional': '可选', - 'tutorial.config.file.config': '主配置文件,包含 API Key、端口、模型提供商等核心设置', - 'tutorial.config.file.pools': '提供商池配置,用于多账号轮询和故障转移', + 'tutorial.config.file.config': '主配置文件,包含 API Key、端口、模型提供商等核心设置 (保存配置管理后自动新建)', + 'tutorial.config.file.pools': '提供商池配置,用于多账号轮询和故障转移 (保存节点后自动新建)', 'tutorial.config.file.plugins': '插件配置,用于启用或禁用系统插件', 'tutorial.config.file.pwd': '后台登录密码文件,默认密码为 admin123', 'tutorial.main.title': '主配置详解 (config.json)', @@ -927,7 +928,7 @@ const translations = { 'oauth.modal.step4.qwen': 'Authorization valid for: {min} minutes', 'oauth.modal.step4.google': 'After authorization, the credentials file will be saved automatically', 'oauth.modal.urlLabel': 'Auth URL:', - 'oauth.modal.copyTitle': 'Copy Link', + 'oauth.modal.copyTitle': 'Copy', 'oauth.modal.openInBrowser': 'Open in Browser', 'oauth.manual.title': 'Auto-listener blocked?', 'oauth.manual.desc': 'If the auth window shows "Cannot access" after redirect, please paste the Full URL from that window\'s address bar below:', @@ -1215,6 +1216,7 @@ const translations = { 'upload.refresh.success': 'Refresh successful', 'upload.action.view.failed': 'View failed', 'upload.action.delete.failed': 'Delete failed', + 'upload.action.quickLink': 'Quick Link', 'upload.config.notExist': 'Configuration file does not exist', 'upload.link.identifying': 'Identifying provider type...', 'upload.link.failed.identify': 'Unable to identify provider type for the config file', @@ -1452,7 +1454,7 @@ const translations = { 'guide.providers.badge.experimental': 'Experimental', 'guide.providers.badge.free': 'Free', 'guide.providers.badge.official': 'Official API', - 'guide.providers.gemini.desc': 'Access Gemini models via Google OAuth, supporting gemini-2.0-flash-exp and more', + 'guide.providers.gemini.desc': 'Access Gemini models via Google OAuth, supporting gemini-3.1-pro-preview and more', 'guide.providers.antigravity.desc': 'Access Gemini 3 Pro, Claude Sonnet 4.5 via Google internal interface', 'guide.providers.kiro.desc': 'Free access to Claude Opus 4.5, Claude Sonnet 4.5 via Kiro client', 'guide.providers.qwen.desc': 'Access Qwen3 Coder Plus via Alibaba Cloud OAuth', @@ -1526,8 +1528,8 @@ const translations = { 'tutorial.config.desc': 'All configuration files are stored in the configs/ directory. Main configuration files include:', 'tutorial.config.badge.required': 'Required', 'tutorial.config.badge.optional': 'Optional', - 'tutorial.config.file.config': 'Main config file with API Key, port, model provider settings', - 'tutorial.config.file.pools': 'Provider pool config for multi-account polling and failover', + 'tutorial.config.file.config': 'Main config file with API Key, port, model provider settings (Automatically created after saving configuration management)', + 'tutorial.config.file.pools': 'Provider pool config for multi-account polling and failover (Automatically created after saving nodes)', 'tutorial.config.file.plugins': 'Plugin config for enabling/disabling system plugins', 'tutorial.config.file.pwd': 'Admin password file, default password is admin123', 'tutorial.main.title': 'Main Config Details (config.json)', diff --git a/static/app/models-manager.js b/static/app/models-manager.js index 15682ff..25a5099 100644 --- a/static/app/models-manager.js +++ b/static/app/models-manager.js @@ -8,6 +8,21 @@ import { t } from './i18n.js'; // 模型数据缓存 let modelsCache = null; +// 提供商配置缓存 +let currentProviderConfigs = null; + +/** + * 更新提供商配置 + * @param {Array} configs - 提供商配置列表 + */ +function updateModelsProviderConfigs(configs) { + currentProviderConfigs = configs; + // 如果已经加载了模型,重新渲染一次以更新显示名称和图标 + if (modelsCache) { + renderModelsList(modelsCache); + } +} + /** * 获取所有提供商的可用模型 * @returns {Promise} 模型数据 @@ -120,6 +135,12 @@ function renderModelsList(models) { const modelList = models[providerType]; if (!modelList || modelList.length === 0) continue; + // 如果配置了不可见,则跳过 + if (currentProviderConfigs) { + const config = currentProviderConfigs.find(c => c.id === providerType); + if (config && config.visible === false) continue; + } + const providerDisplayName = getProviderDisplayName(providerType); const providerIcon = getProviderIcon(providerType); @@ -161,6 +182,14 @@ function renderModelsList(models) { * @returns {string} 显示名称 */ function getProviderDisplayName(providerType) { + // 优先从外部传入的配置中获取名称 + if (currentProviderConfigs) { + const config = currentProviderConfigs.find(c => c.id === providerType); + if (config && config.name) { + return config.name; + } + } + const displayNames = { 'gemini-cli-oauth': 'Gemini CLI (OAuth)', 'gemini-antigravity': 'Gemini Antigravity', @@ -182,6 +211,15 @@ function getProviderDisplayName(providerType) { * @returns {string} 图标类名 */ function getProviderIcon(providerType) { + // 优先从外部传入的配置中获取图标 + if (currentProviderConfigs) { + const config = currentProviderConfigs.find(c => c.id === providerType); + if (config && config.icon) { + // 如果 icon 已经包含 fa- 则直接使用,否则加上 fas + return config.icon.startsWith('fa-') ? `fas ${config.icon}` : config.icon; + } + } + if (providerType.includes('gemini')) { return 'fas fa-gem'; } else if (providerType.includes('claude')) { @@ -285,5 +323,6 @@ window.addEventListener('componentsLoaded', () => { export { initModelsManager, refreshModels, - fetchProviderModels -}; \ No newline at end of file + fetchProviderModels, + updateModelsProviderConfigs +}; diff --git a/static/app/provider-manager.js b/static/app/provider-manager.js index 92a911c..634f7d3 100644 --- a/static/app/provider-manager.js +++ b/static/app/provider-manager.js @@ -1,10 +1,15 @@ // 提供商管理功能模块 import { providerStats, updateProviderStats } from './constants.js'; -import { showToast, formatUptime } from './utils.js'; +import { showToast, formatUptime, getProviderConfigs } from './utils.js'; import { fileUploadHandler } from './file-upload.js'; import { t, getCurrentLanguage } from './i18n.js'; -import { loadConfigList } from './upload-config-manager.js'; +import { renderRoutingExamples } from './routing-examples.js'; +import { updateModelsProviderConfigs } from './models-manager.js'; +import { updateTutorialProviderConfigs } from './tutorial-manager.js'; +import { updateUsageProviderConfigs } from './usage-manager.js'; +import { updateConfigProviderConfigs } from './config-manager.js'; +import { loadConfigList, updateProviderFilterOptions } from './upload-config-manager.js'; import { setServiceMode } from './event-handlers.js'; // 保存初始服务器时间和运行时间 @@ -209,21 +214,7 @@ function renderProviders(providers, supportedProviders = []) { // 始终显示统计卡片 if (statsGrid) statsGrid.style.display = 'grid'; - // 定义所有支持的提供商配置(顺序、显示名称、是否显示) - // visible 现在由 supportedProviders 决定 - const providerConfigs = [ - { id: 'forward-api', name: 'NewAPI', visible: supportedProviders.includes('forward-api') }, - { id: 'gemini-cli-oauth', name: 'Gemini CLI OAuth', visible: supportedProviders.includes('gemini-cli-oauth') }, - { id: 'gemini-antigravity', name: 'Gemini Antigravity', visible: supportedProviders.includes('gemini-antigravity') }, - { id: 'openai-custom', name: 'OpenAI Custom', visible: supportedProviders.includes('openai-custom') }, - { id: 'claude-custom', name: 'Claude Custom', visible: supportedProviders.includes('claude-custom') }, - { id: 'claude-kiro-oauth', name: 'Claude Kiro OAuth', visible: supportedProviders.includes('claude-kiro-oauth') }, - { id: 'openai-qwen-oauth', name: 'OpenAI Qwen OAuth', visible: supportedProviders.includes('openai-qwen-oauth') }, - { id: 'openaiResponses-custom', name: 'OpenAI Responses', visible: supportedProviders.includes('openaiResponses-custom') }, - { id: 'openai-iflow', name: 'OpenAI iFlow', visible: supportedProviders.includes('openai-iflow') }, - { id: 'openai-codex-oauth', name: 'OpenAI Codex OAuth', visible: supportedProviders.includes('openai-codex-oauth') }, - { id: 'grok-custom', name: 'Grok Reverse', visible: supportedProviders.includes('grok-custom') }, - ]; + const providerConfigs = getProviderConfigs(supportedProviders); // 提取显示的 ID 顺序 const providerDisplayOrder = providerConfigs.filter(c => c.visible !== false).map(c => c.id); @@ -267,7 +258,7 @@ function renderProviders(providers, supportedProviders = []) { providerDiv.dataset.providerType = providerType; providerDiv.style.cursor = 'pointer'; - const healthyCount = accounts.filter(acc => acc.isHealthy).length; + const healthyCount = accounts.filter(acc => acc.isHealthy && !acc.isDisabled).length; const totalCount = accounts.length; const usageCount = accounts.reduce((sum, acc) => sum + (acc.usageCount || 0), 0); const errorCount = accounts.reduce((sum, acc) => sum + (acc.errorCount || 0), 0); @@ -357,6 +348,24 @@ function renderProviders(providers, supportedProviders = []) { }); } }); + + // 动态更新凭据文件管理的提供商类型筛选项 + updateProviderFilterOptions(providerConfigs); + + // 动态更新仪表盘页面的路径路由调用示例 + renderRoutingExamples(providerConfigs); + + // 动态更新仪表盘页面的可用模型列表提供商信息 + updateModelsProviderConfigs(providerConfigs); + + // 动态更新配置教程页面的提供商信息 + updateTutorialProviderConfigs(providerConfigs); + + // 动态更新用量查询页面的提供商信息 + updateUsageProviderConfigs(providerConfigs); + + // 动态更新配置管理页面的提供商选择标签 + updateConfigProviderConfigs(providerConfigs); // 更新统计卡片数据 const activeProviders = hasProviders ? Object.keys(providers).length : 0; diff --git a/static/app/routing-examples.js b/static/app/routing-examples.js index 20de420..ed71b3b 100644 --- a/static/app/routing-examples.js +++ b/static/app/routing-examples.js @@ -118,6 +118,17 @@ function initCardInteractions() { */ function getAvailableRoutes() { return [ + { + provider: 'forward-api', + name: 'NewAPI', + paths: { + openai: '/forward-api/v1/chat/completions', + claude: '/forward-api/v1/messages' + }, + description: t('dashboard.routing.official'), + badge: t('dashboard.routing.official'), + badgeClass: 'official' + }, { provider: 'claude-custom', name: 'Claude Custom', @@ -162,6 +173,17 @@ function getAvailableRoutes() { badge: t('dashboard.routing.oauth'), badgeClass: 'oauth' }, + { + provider: 'gemini-antigravity', + name: 'Gemini Antigravity', + paths: { + openai: '/gemini-antigravity/v1/chat/completions', + claude: '/gemini-antigravity/v1/messages' + }, + description: t('dashboard.routing.experimental') || '实验性', + badge: t('dashboard.routing.experimental') || '实验性', + badgeClass: 'oauth' + }, { provider: 'openai-qwen-oauth', name: 'Qwen OAuth', @@ -173,6 +195,28 @@ function getAvailableRoutes() { badge: t('dashboard.routing.oauth'), badgeClass: 'oauth' }, + { + provider: 'openai-iflow', + name: 'iFlow OAuth', + paths: { + openai: '/openai-iflow/v1/chat/completions', + claude: '/openai-iflow/v1/messages' + }, + description: t('dashboard.routing.oauth'), + badge: t('dashboard.routing.oauth'), + badgeClass: 'oauth' + }, + { + provider: 'openai-codex-oauth', + name: 'OpenAI Codex OAuth', + paths: { + openai: '/openai-codex-oauth/v1/chat/completions', + claude: '/openai-codex-oauth/v1/messages' + }, + description: t('dashboard.routing.oauth'), + badge: t('dashboard.routing.oauth'), + badgeClass: 'oauth' + }, { provider: 'openaiResponses-custom', name: 'OpenAI Responses', @@ -294,7 +338,7 @@ async function copyCurlExample(provider, options = {}) { curlCommand = `curl http://localhost:3000${path} \\ -H "Content-Type: application/json" \\ -d '{ - "model": "gemini-2.0-flash-exp", + "model": "gemini-3.1-pro-preview", "messages": [{"role": "user", "content": "${message}"}], "max_tokens": 1000 }'`; @@ -302,7 +346,7 @@ async function copyCurlExample(provider, options = {}) { curlCommand = `curl http://localhost:3000${path} \\ -H "Content-Type: application/json" \\ -d '{ - "model": "gemini-2.0-flash-exp", + "model": "gemini-3.1-pro-preview", "max_tokens": 1000, "messages": [{"role": "user", "content": "${message}"}] }'`; @@ -362,9 +406,139 @@ async function copyCurlExample(provider, options = {}) { } } +/** + * 动态渲染路径路由示例 + * @param {Array} providerConfigs - 提供商配置列表 + */ +function renderRoutingExamples(providerConfigs) { + const container = document.querySelector('.routing-examples-grid'); + if (!container) return; + + container.innerHTML = ''; + + // 获取路由端点基础信息 + const routes = getAvailableRoutes(); + + // 图标映射 + const iconMap = { + 'forward-api': 'fa-share-square', + 'gemini-cli-oauth': 'fa-gem', + 'gemini-antigravity': 'fa-rocket', + 'openai-custom': 'fa-comments', + 'claude-custom': 'fa-brain', + 'claude-kiro-oauth': 'fa-robot', + 'openai-qwen-oauth': 'fa-code', + 'openaiResponses-custom': 'fa-comment-alt', + 'openai-iflow': 'fa-wind', + 'openai-codex-oauth': 'fa-keyboard', + 'grok-custom': 'fa-search' + }; + + // 默认模型映射 (用于 curl 示例) + const modelMap = { + 'gemini-cli-oauth': 'gemini-3-flash-preview', + 'gemini-antigravity': 'gemini-3-flash-preview', + 'claude-custom': 'claude-sonnet-4-6', + 'claude-kiro-oauth': 'claude-sonnet-4-6', + 'openai-custom': 'gpt-4o', + 'openai-qwen-oauth': 'qwen3-coder-plus', + 'openai-iflow': 'qwen3-max', + 'openai-codex-oauth': 'gpt-5', + 'grok-custom': 'grok-3', + 'openaiResponses-custom': 'gpt-4o' + }; + + providerConfigs.forEach(config => { + if (config.visible === false) return; + + let routeInfo = routes.find(r => r.provider === config.id); + + // 如果没找到,则创建一个默认的 + if (!routeInfo) { + routeInfo = { + provider: config.id, + name: config.name, + paths: { + openai: `/${config.id}/v1/chat/completions`, + claude: `/${config.id}/v1/messages` + }, + description: t('dashboard.routing.oauth'), + badge: t('dashboard.routing.oauth'), + badgeClass: 'oauth' + }; + } + + const icon = iconMap[config.id] || 'fa-route'; + const defaultModel = modelMap[config.id] || 'default-model'; + const hostname = window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1' ? + `http://${window.location.host}` : + `${window.location.protocol}//${window.location.host}`; + + const card = document.createElement('div'); + card.className = 'routing-example-card'; + card.dataset.provider = `${config.id}-card`; + + card.innerHTML = ` +
+ +

${routeInfo.name}

+ ${routeInfo.badge} +
+
+
+ + +
+ +
+
+ + ${routeInfo.paths.openai} +
+
+ +
curl ${hostname}${routeInfo.paths.openai} \\
+  -H "Content-Type: application/json" \\
+  -H "Authorization: Bearer YOUR_API_KEY" \\
+  -d '{
+    "model": "${defaultModel}",
+    "messages": [{"role": "user", "content": "Hello!"}],
+    "max_tokens": 1000
+  }'
+
+
+ +
+
+ + ${routeInfo.paths.claude} +
+
+ +
curl ${hostname}${routeInfo.paths.claude} \\
+  -H "Content-Type: application/json" \\
+  -H "X-API-Key: YOUR_API_KEY" \\
+  -d '{
+    "model": "${defaultModel}",
+    "max_tokens": 1000,
+    "messages": [{"role": "user", "content": "Hello!"}]
+  }'
+
+
+
+ `; + + container.appendChild(card); + }); + + // 重新初始化卡片交互 + initCardInteractions(); +} + export { initRoutingExamples, getAvailableRoutes, highlightProviderRoute, - copyCurlExample -}; \ No newline at end of file + copyCurlExample, + renderRoutingExamples +}; diff --git a/static/app/tutorial-manager.js b/static/app/tutorial-manager.js new file mode 100644 index 0000000..f3b01b8 --- /dev/null +++ b/static/app/tutorial-manager.js @@ -0,0 +1,57 @@ +// 教程管理模块 +import { getProviderConfigs } from './utils.js'; + +// 提供商配置缓存 +let currentProviderConfigs = null; + +/** + * 初始化教程功能 + */ +function initTutorialManager() { + renderOauthPaths(); + + // 监听语言切换事件 + window.addEventListener('languageChanged', () => { + renderOauthPaths(currentProviderConfigs); + }); +} + +/** + * 更新提供商配置 + * @param {Array} configs - 提供商配置列表 + */ +function updateTutorialProviderConfigs(configs) { + currentProviderConfigs = configs; + renderOauthPaths(configs); +} + +/** + * 渲染 OAuth 授权路径列表 + * @param {Array} configs - 提供商配置列表(可选) + */ +function renderOauthPaths(configs = null) { + const oauthPathList = document.getElementById('oauthPathList'); + if (!oauthPathList) return; + + // 获取所有提供商配置 + const providers = configs || getProviderConfigs([]); + + // 过滤出有默认路径配置的提供商(即 OAuth 类提供商)且可见的 + const oauthProviders = providers.filter(p => p.defaultPath && p.visible !== false); + + oauthPathList.innerHTML = oauthProviders.map(p => ` +
+
+ + ${p.name} +
+ ${p.defaultPath} +
+ `).join(''); +} + +export { + initTutorialManager, + renderOauthPaths, + updateTutorialProviderConfigs +}; diff --git a/static/app/upload-config-manager.js b/static/app/upload-config-manager.js index c9ba2c0..736a613 100644 --- a/static/app/upload-config-manager.js +++ b/static/app/upload-config-manager.js @@ -82,43 +82,102 @@ function createConfigItemElement(config, index) { item.className = `config-item-manager ${configStatus}`; item.dataset.index = index; - const statusIcon = config.isUsed ? 'fa-check-circle' : 'fa-circle'; + const statusIcon = config.isUsed ? 'fa-check-circle' : 'fa-circle-question'; const statusText = config.isUsed ? t('upload.statusFilter.used') : t('upload.statusFilter.unused'); const typeIcon = config.type === 'oauth' ? 'fa-key' : config.type === 'api-key' ? 'fa-lock' : config.type === 'provider-pool' ? 'fa-network-wired' : - config.type === 'system-prompt' ? 'fa-file-text' : 'fa-cog'; + config.type === 'system-prompt' ? 'fa-file-text' : 'fa-file-code'; + + // 检测提供商信息 + const providerInfo = detectProviderFromPath(config.path); + const providerBadge = providerInfo ? + ` + ${providerInfo.displayName} + ` : ''; // 生成关联详情HTML const usageInfoHtml = generateUsageInfoHtml(config); + // 获取关联的节点简要信息 + let linkedNodesInfo = ''; + if (config.isUsed && config.usageInfo && config.usageInfo.usageDetails) { + const details = config.usageInfo.usageDetails; + const infoParts = details.map(d => { + if (d.type === 'Provider Pool' || d.type === '提供商池') { + // 严格按照优先级:自定义名称 > UUID (简短) > 默认位置描述 + if (d.nodeName) return d.nodeName; + if (d.uuid) return d.uuid.substring(0, 8); + return d.location; + } else if (d.type === 'Main Config' || d.type === '主要配置') { + return t('upload.usage.mainConfig'); + } + return null; + }).filter(Boolean); + + if (infoParts.length > 0) { + const uniqueParts = [...new Set(infoParts)]; + linkedNodesInfo = `
+ ${uniqueParts.map(name => ` ${name}`).join('')} +
`; + } + } + // 判断是否可以一键关联(未关联且路径包含支持的提供商目录) - const providerInfo = detectProviderFromPath(config.path); const canQuickLink = !config.isUsed && providerInfo !== null; const quickLinkBtnHtml = canQuickLink ? - `` : ''; item.innerHTML = ` -
-
${config.name}
-
${config.path}
-
-
-
${formatFileSize(config.size)}
-
${formatDate(config.modified)}
-
- - ${statusText} - ${quickLinkBtnHtml} +
+
+
+ +
+
+
+ ${config.name} + ${providerBadge} +
+
+ ${config.path} +
+
+
+ +
+
+ + ${formatFileSize(config.size)} + + + ${formatDate(config.modified)} + +
+
+ +
+
+
+ + ${statusText} +
+ ${linkedNodesInfo} + ${quickLinkBtnHtml} +
+
+ +
+
-
-
文件路径
+
+
文件完整路径
${config.path}
@@ -126,12 +185,12 @@ function createConfigItemElement(config, index) {
${formatFileSize(config.size)}
-
最后修改
+
最后修改时间
${formatDate(config.modified)}
-
关联状态
-
${statusText}
+
当前关联状态
+
${statusText}
${usageInfoHtml} @@ -165,7 +224,7 @@ function createConfigItemElement(config, index) { } // 一键关联按钮事件 - const quickLinkBtn = item.querySelector('.btn-quick-link'); + const quickLinkBtn = item.querySelector('.btn-quick-link-main'); if (quickLinkBtn) { quickLinkBtn.addEventListener('click', (e) => { e.stopPropagation(); @@ -175,14 +234,69 @@ function createConfigItemElement(config, index) { // 添加点击事件展开/折叠详情 item.addEventListener('click', (e) => { - if (!e.target.closest('.config-item-actions')) { + if (!e.target.closest('.config-item-actions') && !e.target.closest('.config-detail-value')) { item.classList.toggle('expanded'); } }); + // 点击路径复制 + const pathValueEl = item.querySelector('.path-item .config-detail-value'); + if (pathValueEl) { + pathValueEl.addEventListener('click', async (e) => { + e.stopPropagation(); + const textToCopy = config.path; + + // 优先使用 Clipboard API + if (navigator.clipboard && navigator.clipboard.writeText) { + try { + await navigator.clipboard.writeText(textToCopy); + showToast(t('common.success'), t('common.copy.success'), 'success'); + } catch (err) { + console.error('Clipboard API failed:', err); + fallbackCopyTextToClipboard(textToCopy); + } + } else { + fallbackCopyTextToClipboard(textToCopy); + } + }); + pathValueEl.title = t('models.clickToCopy') || '点击复制'; + } + return item; } +/** + * 降级复制方案 + * @param {string} text - 要复制的文本 + */ +function fallbackCopyTextToClipboard(text) { + const textArea = document.createElement("textarea"); + textArea.value = text; + + // 确保不可见且不影响布局 + textArea.style.position = "fixed"; + textArea.style.left = "-9999px"; + textArea.style.top = "0"; + document.body.appendChild(textArea); + + textArea.focus(); + textArea.select(); + + try { + const successful = document.execCommand('copy'); + if (successful) { + showToast(t('common.success'), t('common.copy.success'), 'success'); + } else { + showToast(t('common.error'), t('common.copy.failed'), 'error'); + } + } catch (err) { + console.error('Fallback copy failed:', err); + showToast(t('common.error'), t('common.copy.failed'), 'error'); + } + + document.body.removeChild(textArea); +} + /** * 生成关联详情HTML * @param {Object} config - 配置数据 @@ -212,11 +326,32 @@ function generateUsageInfoHtml(config) { const isMain = detail.type === '主要配置' || detail.type === 'Main Config'; const icon = isMain ? 'fa-cog' : 'fa-network-wired'; const usageTypeKey = isMain ? 'main_config' : 'provider_pool'; + + // 严格遵循显示优先级:自定义名称 > UUID > 默认位置描述 + let displayTitle = ''; + let subtitle = ''; + + if (detail.nodeName) { + displayTitle = detail.nodeName; + subtitle = detail.providerType ? `${detail.providerType} - ${detail.location}` : detail.location; + } else if (detail.uuid) { + displayTitle = detail.uuid; + subtitle = detail.providerType ? `${detail.providerType} - ${detail.location}` : detail.location; + } else { + displayTitle = detail.location; + subtitle = detail.providerType || ''; + } + detailsHtml += `
- ${detail.type} - ${detail.location} +
+
+ ${detail.type} + ${displayTitle} +
+ ${subtitle ? `
${subtitle}
` : ''} +
`; }); @@ -234,6 +369,28 @@ function generateUsageInfoHtml(config) { `; } +/** + * 对配置列表进行排序 + * 规则:未关联的排在前面,然后按修改时间倒序排列 + * @param {Array} configs - 配置列表 + * @returns {Array} 排序后的列表 + */ +function sortConfigs(configs) { + if (!configs || !configs.length) return []; + + return configs.sort((a, b) => { + // 1. 未关联优先 (isUsed 为 false 的排在前面) + if (a.isUsed !== b.isUsed) { + return a.isUsed ? 1 : -1; + } + + // 2. 时间倒序 (最新的排在前面) + const dateA = new Date(a.modified); + const dateB = new Date(b.modified); + return dateB - dateA; + }); +} + /** * 格式化文件大小 * @param {number} bytes - 字节数 @@ -291,8 +448,11 @@ function updateStats() { /** * 加载配置文件列表 + * @param {string} searchTerm - 搜索关键词 + * @param {string} statusFilter - 状态过滤 + * @param {string} providerFilter - 提供商过滤 */ -async function loadConfigList() { +async function loadConfigList(searchTerm = '', statusFilter = '', providerFilter = '') { // 防止重复加载 if (isLoadingConfigs) { console.log('正在加载配置列表,跳过重复调用'); @@ -304,18 +464,24 @@ async function loadConfigList() { try { const result = await window.apiClient.get('/upload-configs'); - allConfigs = result; - filteredConfigs = [...allConfigs]; - renderConfigList(); - updateStats(); + allConfigs = sortConfigs(result); + + // 如果提供了过滤参数,则执行搜索过滤,否则显示全部 + if (searchTerm || statusFilter || providerFilter) { + searchConfigs(searchTerm, statusFilter, providerFilter); + } else { + filteredConfigs = [...allConfigs]; + renderConfigList(); + updateStats(); + } + console.log('配置列表加载成功,共', allConfigs.length, '个项目'); - // showToast(t('common.success'), t('upload.refresh') + '成功', 'success'); } catch (error) { console.error('加载配置列表失败:', error); showToast(t('common.error'), t('common.error') + ': ' + error.message, 'error'); // 使用模拟数据作为示例 - allConfigs = generateMockConfigData(); + allConfigs = sortConfigs(generateMockConfigData()); filteredConfigs = [...allConfigs]; renderConfigList(); updateStats(); @@ -497,37 +663,23 @@ function closeConfigModal() { async function copyConfigContent(path) { try { const fileData = await window.apiClient.get(`/upload-configs/view/${encodeURIComponent(path)}`); - - // 尝试使用现代 Clipboard API + const textToCopy = fileData.content; + + // 优先使用 Clipboard API if (navigator.clipboard && navigator.clipboard.writeText) { - await navigator.clipboard.writeText(fileData.content); - showToast(t('common.success'), t('oauth.success.msg'), 'success'); - } else { - // 降级方案:使用传统的 document.execCommand - const textarea = document.createElement('textarea'); - textarea.value = fileData.content; - textarea.style.position = 'fixed'; - textarea.style.opacity = '0'; - document.body.appendChild(textarea); - textarea.select(); - try { - const successful = document.execCommand('copy'); - if (successful) { - showToast(t('common.copy.success'), 'success'); - } else { - showToast(t('common.copy.failed'), 'error'); - } + await navigator.clipboard.writeText(textToCopy); + showToast(t('common.success'), t('common.copy.success'), 'success'); } catch (err) { - console.error('复制失败:', err); - showToast(t('common.copy.failed'), 'error'); - } finally { - document.body.removeChild(textarea); + console.error('Clipboard API failed:', err); + fallbackCopyTextToClipboard(textToCopy); } + } else { + fallbackCopyTextToClipboard(textToCopy); } } catch (error) { console.error('复制失败:', error); - showToast(t('common.copy.failed') + ': ' + error.message, 'error'); + showToast(t('common.error'), t('common.copy.failed') + ': ' + error.message, 'error'); } } @@ -736,7 +888,8 @@ function initUploadConfigManager() { const searchTerm = searchInput?.value.trim() || ''; const currentStatusFilter = statusFilter?.value || ''; const currentProviderFilter = providerFilter?.value || ''; - searchConfigs(searchTerm, currentStatusFilter, currentProviderFilter); + // 点击搜索按钮时,调接口刷新数据 + loadConfigList(searchTerm, currentStatusFilter, currentProviderFilter); }); } @@ -1091,6 +1244,52 @@ async function downloadAllConfigs() { } } +/** + * 动态更新提供商筛选下拉框选项 + * @param {Array} providerConfigs - 提供商配置列表 + */ +function updateProviderFilterOptions(providerConfigs) { + const filterSelect = document.getElementById('configProviderFilter'); + if (!filterSelect) return; + + // 保存当前选中的值 + const currentValue = filterSelect.value; + + // 清空现有选项(保留第一个"全部提供商") + const firstOption = filterSelect.options[0]; + filterSelect.innerHTML = ''; + if (firstOption) { + filterSelect.appendChild(firstOption); + } else { + const option = document.createElement('option'); + option.value = ''; + option.setAttribute('data-i18n', 'upload.providerFilter.all'); + option.textContent = t('upload.providerFilter.all'); + filterSelect.appendChild(option); + } + + // 添加动态选项 + providerConfigs.forEach(config => { + // 根据是否有 defaultPath 来过滤,这意味着该提供商支持 OAuth 凭据文件管理 + if (config.visible !== false && config.defaultPath) { + const option = document.createElement('option'); + option.value = config.id; + option.textContent = config.name; + filterSelect.appendChild(option); + } + }); + + // 添加"其他"选项 + const otherOption = document.createElement('option'); + otherOption.value = 'other'; + otherOption.setAttribute('data-i18n', 'upload.providerFilter.other'); + otherOption.textContent = t('upload.providerFilter.other'); + filterSelect.appendChild(otherOption); + + // 恢复选中的值(如果还存在) + filterSelect.value = currentValue; +} + // 导出函数 export { initUploadConfigManager, @@ -1101,5 +1300,6 @@ export { closeConfigModal, copyConfigContent, reloadConfig, - deleteUnboundConfigs -}; \ No newline at end of file + deleteUnboundConfigs, + updateProviderFilterOptions +}; diff --git a/static/app/usage-manager.js b/static/app/usage-manager.js index dd908c6..086b122 100644 --- a/static/app/usage-manager.js +++ b/static/app/usage-manager.js @@ -12,6 +12,20 @@ const PROVIDERS_WITHOUT_USAGE_DISPLAY = [ 'gemini-antigravity' ]; +// 提供商配置缓存 +let currentProviderConfigs = null; + +/** + * 更新提供商配置 + * @param {Array} configs - 提供商配置列表 + */ +export function updateUsageProviderConfigs(configs) { + currentProviderConfigs = configs; + // 重新触发列表加载,以应用最新的可见性过滤、名称和图标 + loadSupportedProviders(); + loadUsage(); +} + /** * 检查提供商是否支持显示用量 * @param {string} providerType - 提供商类型 @@ -55,16 +69,31 @@ async function loadSupportedProviders() { const providers = await response.json(); listEl.innerHTML = ''; - providers.forEach(provider => { + + // 按照 currentProviderConfigs 的顺序渲染,确保顺序一致性 + const displayOrder = currentProviderConfigs + ? currentProviderConfigs.map(c => c.id) + : providers; + + displayOrder.forEach(providerId => { + // 必须是后端支持且前端配置可见的提供商 + const isSupported = providers.includes(providerId); + if (!isSupported) return; + + if (currentProviderConfigs) { + const config = currentProviderConfigs.find(c => c.id === providerId); + if (config && config.visible === false) return; + } + const tag = document.createElement('span'); tag.className = 'provider-tag'; - tag.textContent = getProviderDisplayName(provider); + tag.textContent = getProviderDisplayName(providerId); tag.title = t('usage.doubleClickToRefresh') || '双击刷新该提供商用量'; tag.setAttribute('data-i18n-title', 'usage.doubleClickToRefresh'); // 添加双击事件 tag.addEventListener('dblclick', () => { - refreshProviderUsage(provider); + refreshProviderUsage(providerId); }); listEl.appendChild(tag); @@ -240,6 +269,12 @@ function renderUsageData(data, container) { const groupedInstances = {}; for (const [providerType, providerData] of Object.entries(data.providers)) { + // 如果配置了不可见,则跳过 + if (currentProviderConfigs) { + const config = currentProviderConfigs.find(c => c.id === providerType); + if (config && config.visible === false) continue; + } + if (providerData.instances && providerData.instances.length > 0) { const validInstances = []; for (const instance of providerData.instances) { @@ -269,11 +304,18 @@ function renderUsageData(data, container) { return; } - // 按提供商分组渲染 - for (const [providerType, instances] of Object.entries(groupedInstances)) { - const groupContainer = createProviderGroup(providerType, instances); - container.appendChild(groupContainer); - } + // 按提供商分组渲染,使用统一的显示顺序 + const displayOrder = currentProviderConfigs + ? currentProviderConfigs.map(c => c.id) + : Object.keys(groupedInstances); + + displayOrder.forEach(providerType => { + const instances = groupedInstances[providerType]; + if (instances && instances.length > 0) { + const groupContainer = createProviderGroup(providerType, instances); + container.appendChild(groupContainer); + } + }); } /** @@ -822,6 +864,14 @@ function calculateTotalUsage(usageBreakdown) { * @returns {string} 显示名称 */ function getProviderDisplayName(providerType) { + // 优先从外部传入的配置中获取名称 + if (currentProviderConfigs) { + const config = currentProviderConfigs.find(c => c.id === providerType); + if (config && config.name) { + return config.name; + } + } + const names = { 'claude-kiro-oauth': 'Claude Kiro OAuth', 'gemini-cli-oauth': 'Gemini CLI OAuth', @@ -839,6 +889,15 @@ function getProviderDisplayName(providerType) { * @returns {string} 图标类名 */ function getProviderIcon(providerType) { + // 优先从外部传入的配置中获取图标 + if (currentProviderConfigs) { + const config = currentProviderConfigs.find(c => c.id === providerType); + if (config && config.icon) { + // 如果 icon 已经包含 fa- 则直接使用,否则加上 fas + return config.icon.startsWith('fa-') ? `fas ${config.icon}` : config.icon; + } + } + const icons = { 'claude-kiro-oauth': 'fas fa-robot', 'gemini-cli-oauth': 'fas fa-gem', diff --git a/static/app/utils.js b/static/app/utils.js index 49a06ad..7f1567a 100644 --- a/static/app/utils.js +++ b/static/app/utils.js @@ -2,6 +2,88 @@ import { t, getCurrentLanguage } from './i18n.js'; import { apiClient } from './auth.js'; +/** + * 获取所有支持的提供商配置列表 + * @param {string[]} supportedProviders - 已注册的提供商类型列表 + * @returns {Object[]} 提供商配置对象数组 + */ +function getProviderConfigs(supportedProviders = []) { + return [ + { + id: 'forward-api', + name: 'NewAPI', + icon: 'fa-share-square', + visible: supportedProviders.includes('forward-api') + }, + { + id: 'gemini-cli-oauth', + name: t('dashboard.routing.nodeName.gemini'), + icon: 'fa-robot', + defaultPath: 'configs/gemini/', + visible: supportedProviders.includes('gemini-cli-oauth') + }, + { + id: 'gemini-antigravity', + name: t('dashboard.routing.nodeName.antigravity'), + icon: 'fa-rocket', + defaultPath: 'configs/antigravity/', + visible: supportedProviders.includes('gemini-antigravity') + }, + { + id: 'claude-kiro-oauth', + name: t('dashboard.routing.nodeName.kiro'), + icon: 'fa-key', + defaultPath: 'configs/kiro/', + visible: supportedProviders.includes('claude-kiro-oauth') + }, + { + id: 'openai-codex-oauth', + name: t('dashboard.routing.nodeName.codex'), + icon: 'fa-code', + defaultPath: 'configs/codex/', + visible: supportedProviders.includes('openai-codex-oauth') + }, + { + id: 'openai-qwen-oauth', + name: t('dashboard.routing.nodeName.qwen'), + icon: 'fa-cloud', + defaultPath: 'configs/qwen/', + visible: supportedProviders.includes('openai-qwen-oauth') + }, + { + id: 'openai-iflow', + name: t('dashboard.routing.nodeName.iflow'), + icon: 'fa-stream', + defaultPath: 'configs/iflow/', + visible: supportedProviders.includes('openai-iflow') + }, + { + id: 'grok-custom', + name: t('dashboard.routing.nodeName.grok'), + icon: 'fa-user-secret', + visible: supportedProviders.includes('grok-custom') + }, + { + id: 'openai-custom', + name: t('dashboard.routing.nodeName.openai'), + icon: 'fa-microchip', + visible: supportedProviders.includes('openai-custom') + }, + { + id: 'claude-custom', + name: t('dashboard.routing.nodeName.claude'), + icon: 'fa-brain', + visible: supportedProviders.includes('claude-custom') + }, + { + id: 'openaiResponses-custom', + name: 'OpenAI Responses', + icon: 'fa-reply-all', + visible: supportedProviders.includes('openaiResponses-custom') + }, + ]; +} + /** * 格式化运行时间 * @param {number} seconds - 秒数 @@ -310,32 +392,6 @@ function getProviderTypeFields(providerType) { placeholder: 'https://grok.com' } ], - 'grok-custom': [ - { - id: 'GROK_COOKIE_TOKEN', - label: t('modal.provider.field.ssoToken'), - type: 'password', - placeholder: 'sso cookie token' - }, - { - id: 'GROK_CF_CLEARANCE', - label: t('modal.provider.field.cfClearance'), - type: 'text', - placeholder: 'cf_clearance cookie value' - }, - { - id: 'GROK_USER_AGENT', - label: t('modal.provider.field.userAgent'), - type: 'text', - placeholder: 'Mozilla/5.0 ...' - }, - { - id: 'GROK_BASE_URL', - label: `Grok Base URL ${t('config.optional')}`, - type: 'text', - placeholder: 'https://grok.com' - } - ], 'forward-api': [ { id: 'FORWARD_API_KEY', @@ -404,6 +460,7 @@ export { showToast, getFieldLabel, getProviderTypeFields, + getProviderConfigs, getProviderStats, apiRequest }; \ No newline at end of file diff --git a/static/components/section-dashboard.html b/static/components/section-dashboard.html index f86380b..83a1b93 100644 --- a/static/components/section-dashboard.html +++ b/static/components/section-dashboard.html @@ -126,481 +126,10 @@

通过不同路径路由访问不同的AI模型提供商,支持灵活的模型切换

-
-
- -

Gemini CLI OAuth

- 突破限制 -
-
- -
- - -
- - -
-
- - /gemini-cli-oauth/v1/chat/completions -
-
- -
curl http://localhost:3000/gemini-cli-oauth/v1/chat/completions \
-  -H "Content-Type: application/json" \
-  -H "Authorization: Bearer YOUR_API_KEY" \
-  -d '{
-    "model": "gemini-2.0-flash-exp",
-    "messages": [{"role": "user", "content": "Hello!"}],
-    "max_tokens": 1000
-  }'
-
-
- - -
-
- - /gemini-cli-oauth/v1/messages -
-
- -
curl http://localhost:3000/gemini-cli-oauth/v1/messages \
-  -H "Content-Type: application/json" \
-  -H "X-API-Key: YOUR_API_KEY" \
-  -d '{
-    "model": "gemini-2.0-flash-exp",
-    "max_tokens": 1000,
-    "messages": [{"role": "user", "content": "Hello!"}]
-  }'
-
-
-
-
- -
-
- -

Gemini Antigravity

- 突破限制/实验性 -
-
- -
- - -
- - -
-
- - /gemini-antigravity/v1/chat/completions -
-
- -
curl http://localhost:3000/gemini-antigravity/v1/chat/completions \
-  -H "Content-Type: application/json" \
-  -H "Authorization: Bearer YOUR_API_KEY" \
-  -d '{
-    "model": "gemini-3-pro-preview",
-    "messages": [{"role": "user", "content": "Hello!"}],
-    "max_tokens": 1000
-  }'
-
-
- - -
-
- - /gemini-antigravity/v1/messages -
-
- -
curl http://localhost:3000/gemini-antigravity/v1/messages \
-  -H "Content-Type: application/json" \
-  -H "X-API-Key: YOUR_API_KEY" \
-  -d '{
-    "model": "gemini-3-pro-preview",
-    "max_tokens": 1000,
-    "messages": [{"role": "user", "content": "Hello!"}]
-  }'
-
-
-
-
- -
-
- -

Claude Custom

- 官方API/三方 -
-
- -
- - -
- - -
-
- - /claude-custom/v1/chat/completions -
-
- -
curl http://localhost:3000/claude-custom/v1/chat/completions \
-  -H "Content-Type: application/json" \
-  -H "Authorization: Bearer YOUR_API_KEY" \
-  -d '{
-    "model": "claude-3-sonnet-20240229",
-    "messages": [{"role": "user", "content": "Hello!"}],
-    "max_tokens": 1000
-  }'
-
-
- - -
-
- - /claude-custom/v1/messages -
-
- -
curl http://localhost:3000/claude-custom/v1/messages \
-  -H "Content-Type: application/json" \
-  -H "X-API-Key: YOUR_API_KEY" \
-  -d '{
-    "model": "claude-3-sonnet-20240229",
-    "max_tokens": 1000,
-    "messages": [{"role": "user", "content": "Hello!"}]
-  }'
-
-
-
-
- -
-
- -

Claude Kiro OAuth

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

OpenAI Custom

- 官方API/三方 -
-
- -
- - -
- - -
-
- - /openai-custom/v1/chat/completions -
-
- -
curl http://localhost:3000/openai-custom/v1/chat/completions \
-  -H "Content-Type: application/json" \
-  -H "Authorization: Bearer YOUR_API_KEY" \
-  -d '{
-    "model": "gpt-4",
-    "messages": [{"role": "user", "content": "Hello!"}],
-    "max_tokens": 1000
-  }'
-
-
- - -
-
- - /openai-custom/v1/messages -
-
- -
curl http://localhost:3000/openai-custom/v1/messages \
-  -H "Content-Type: application/json" \
-  -H "X-API-Key: YOUR_API_KEY" \
-  -d '{
-    "model": "gpt-4",
-    "max_tokens": 1000,
-    "messages": [{"role": "user", "content": "Hello!"}]
-  }'
-
-
-
-
- -
-
- -

Qwen OAuth

- 突破限制 -
-
- -
- - -
- - -
-
- - /openai-qwen-oauth/v1/chat/completions -
-
- -
curl http://localhost:3000/openai-qwen-oauth/v1/chat/completions \
-  -H "Content-Type: application/json" \
-  -H "Authorization: Bearer YOUR_API_KEY" \
-  -d '{
-    "model": "qwen-turbo",
-    "messages": [{"role": "user", "content": "Hello!"}],
-    "max_tokens": 1000
-  }'
-
-
- - -
-
- - /openai-qwen-oauth/v1/messages -
-
- -
curl http://localhost:3000/openai-qwen-oauth/v1/messages \
-  -H "Content-Type: application/json" \
-  -H "X-API-Key: YOUR_API_KEY" \
-  -d '{
-    "model": "qwen-turbo",
-    "max_tokens": 1000,
-    "messages": [{"role": "user", "content": "Hello!"}]
-  }'
-
-
-
-
- -
-
- -

iFlow OAuth

- 突破限制 -
-
- -
- - -
- - -
-
- - /openai-iflow/v1/chat/completions -
-
- -
curl http://localhost:3000/openai-iflow/v1/chat/completions \
-  -H "Content-Type: application/json" \
-  -H "Authorization: Bearer YOUR_API_KEY" \
-  -d '{
-    "model": "qwen3-max",
-    "messages": [{"role": "user", "content": "Hello!"}],
-    "max_tokens": 1000
-  }'
-
-
- - -
-
- - /openai-iflow/v1/messages -
-
- -
curl http://localhost:3000/openai-iflow/v1/messages \
-  -H "Content-Type: application/json" \
-  -H "X-API-Key: YOUR_API_KEY" \
-  -d '{
-    "model": "qwen3-max",
-    "max_tokens": 1000,
-    "messages": [{"role": "user", "content": "Hello!"}]
-  }'
-
-
-
-
- -
-
- -

OpenAI Codex OAuth

- 突破限制 -
-
- -
- - -
- - -
-
- - /openai-codex-oauth/v1/chat/completions -
-
- -
curl http://localhost:3000/openai-codex-oauth/v1/chat/completions \
-  -H "Content-Type: application/json" \
-  -H "Authorization: Bearer YOUR_API_KEY" \
-  -d '{
-    "model": "gpt-4o",
-    "messages": [{"role": "user", "content": "写一个Python快速排序"}],
-    "stream": true
-  }'
-
-
- - -
-
- - /openai-codex-oauth/v1/messages -
-
- -
curl http://localhost:3000/openai-codex-oauth/v1/messages \
-  -H "Content-Type: application/json" \
-  -H "X-API-Key: YOUR_API_KEY" \
-  -d '{
-    "model": "gpt-5",
-    "max_tokens": 4096,
-    "messages": [{"role": "user", "content": "解释PKCE认证流程"}]
-  }'
-
-
-
-
- -
-
- -

Grok Reverse

- 突破限制/免费使用 -
-
- -
- - -
- - -
-
- - /grok-custom/v1/chat/completions -
-
- -
curl http://localhost:3000/grok-custom/v1/chat/completions \
-  -H "Content-Type: application/json" \
-  -H "Authorization: Bearer YOUR_API_KEY" \
-  -d '{
-    "model": "grok-3",
-    "messages": [{"role": "user", "content": "你好"}],
-    "stream": true
-  }'
-
-
- - -
-
- - /grok-custom/v1/messages -
-
- -
curl http://localhost:3000/grok-custom/v1/messages \
-  -H "Content-Type: application/json" \
-  -H "X-API-Key: YOUR_API_KEY" \
-  -d '{
-    "model": "grok-3",
-    "max_tokens": 4096,
-    "messages": [{"role": "user", "content": "你好"}]
-  }'
-
-
-
+ +
+ + 加载中...
diff --git a/static/components/section-tutorial.html b/static/components/section-tutorial.html index 3cc77f5..e616e93 100644 --- a/static/components/section-tutorial.html +++ b/static/components/section-tutorial.html @@ -16,7 +16,7 @@ config.json 必需
-

主配置文件,包含 API Key、端口、模型提供商等核心设置

+

主配置文件,包含 API Key、端口、模型提供商等核心设置 (保存配置管理后自动新建)

@@ -24,7 +24,7 @@ provider_pools.json 必需
-

提供商池配置,用于多账号轮询和故障转移

+

提供商池配置,用于多账号轮询和故障转移 (保存节点后自动新建)

@@ -295,7 +295,7 @@ "name": "Gemini 账号 1", "oauthCredsFilePath": "configs/gemini/oauth_creds_1.json", "checkHealth": true, - "checkModel": "gemini-2.0-flash-exp" + "checkModel": "gemini-3-flash-preview" } ], "claude-kiro-oauth": [ @@ -351,49 +351,8 @@

各提供商的 OAuth 凭据文件默认存储位置(建议保持在 configs/ 目录下以便统一管理):

-
-
-
- - Gemini CLI OAuth -
- configs/gemini/ -
-
-
- - Gemini Antigravity -
- configs/antigravity/ -
-
-
- - Claude Kiro OAuth -
- configs/kiro/ -
-
-
- - Qwen OAuth -
- configs/qwen/ -
-
-
- - iFlow OAuth -
- configs/iflow/ -
-
-
- - OpenAI Codex OAuth -
- configs/codex/ -
+
+
diff --git a/static/components/section-upload-config.css b/static/components/section-upload-config.css index ee0c877..0584546 100644 --- a/static/components/section-upload-config.css +++ b/static/components/section-upload-config.css @@ -128,85 +128,228 @@ } .config-item-manager { - padding: 1.5rem; + padding: 0; border-bottom: 1px solid var(--border-color); transition: var(--transition); cursor: pointer; + background: var(--bg-primary); } .config-item-manager:hover { background: var(--bg-secondary); } -.config-item-manager:last-child { - border-bottom: none; -} - -.config-item-header { +.config-item-main-row { display: flex; - justify-content: space-between; + padding: 1.25rem 1.5rem; align-items: center; - margin-bottom: 0.75rem; + gap: 1.5rem; } -.config-item-name { - font-size: 1rem; +.config-item-left { + display: flex; + align-items: center; + gap: 1rem; + flex: 1.5; + min-width: 0; +} + +.config-item-icon-wrapper { + width: 40px; + height: 40px; + border-radius: 10px; + display: flex; + align-items: center; + justify-content: center; + font-size: 1.25rem; + flex-shrink: 0; +} + +.config-item-icon-wrapper.oauth { background: rgba(59, 130, 246, 0.1); color: #3b82f6; } +.config-item-icon-wrapper.api-key { background: rgba(16, 185, 129, 0.1); color: #10b981; } +.config-item-icon-wrapper.provider-pool { background: rgba(139, 92, 246, 0.1); color: #8b5cf6; } +.config-item-icon-wrapper.system-prompt { background: rgba(245, 158, 11, 0.1); color: #f59e0b; } +.config-item-icon-wrapper.other { background: rgba(107, 114, 128, 0.1); color: #6b7280; } + +.config-item-title-area { + display: flex; + flex-direction: column; + gap: 0.25rem; + min-width: 0; +} + +.config-item-name-line { + display: flex; + align-items: center; + gap: 0.5rem; + flex-wrap: wrap; +} + +.config-item-display-name { + font-size: 1.05rem; font-weight: 600; color: var(--text-primary); - flex: 1; -} - -.config-item-path { - font-size: 0.75rem; - color: var(--text-secondary); - font-family: 'Courier New', monospace; - background: var(--bg-tertiary); - padding: 0.25rem 0.5rem; - border-radius: 0.25rem; - margin: 0 0.5rem; - max-width: 300px; + white-space: nowrap; overflow: hidden; text-overflow: ellipsis; +} + +.provider-type-tag { + font-size: 0.7rem; + font-weight: 600; + padding: 0.15rem 0.5rem; + border-radius: 20px; + display: inline-flex; + align-items: center; + gap: 0.25rem; + text-transform: uppercase; +} + +.tag-kiro-oauth { background: rgba(236, 72, 153, 0.1); color: #ec4899; border: 1px solid rgba(236, 72, 153, 0.2); } +.tag-gemini-oauth { background: rgba(59, 130, 246, 0.1); color: #3b82f6; border: 1px solid rgba(59, 130, 246, 0.2); } +.tag-qwen-oauth { background: rgba(16, 185, 129, 0.1); color: #10b981; border: 1px solid rgba(16, 185, 129, 0.2); } +.tag-antigravity { background: rgba(139, 92, 246, 0.1); color: #8b5cf6; border: 1px solid rgba(139, 92, 246, 0.2); } +.tag-codex-oauth { background: rgba(245, 158, 11, 0.1); color: #f59e0b; border: 1px solid rgba(245, 158, 11, 0.2); } +.tag-iflow-oauth { background: rgba(20, 184, 166, 0.1); color: #14b8a6; border: 1px solid rgba(20, 184, 166, 0.2); } + +.config-item-path-line { + font-size: 0.75rem; + color: var(--text-tertiary); white-space: nowrap; -} - -.config-item-meta { + overflow: hidden; + text-overflow: ellipsis; display: flex; - gap: 1rem; align-items: center; - font-size: 0.875rem; + gap: 0.35rem; +} + +.config-item-middle { + flex: 1.2; + display: flex; + align-items: center; +} + +.config-meta-info { + display: flex; + flex-direction: column; + gap: 0.35rem; +} + +.meta-item { + font-size: 0.8rem; color: var(--text-secondary); + display: flex; + align-items: center; + gap: 0.5rem; } -.config-item-type { +.meta-item i { + width: 14px; + color: var(--text-tertiary); +} + +.config-item-right { + display: flex; + align-items: center; + gap: 1.5rem; + flex: 1.3; + justify-content: flex-end; +} + +.config-status-col { + display: flex; + flex-direction: column; + align-items: flex-end; + gap: 0.5rem; + width: 100%; +} + +.config-status-indicator { + display: inline-flex; + align-items: center; + gap: 0.35rem; + padding: 0.35rem 0.75rem; + border-radius: 30px; + font-size: 0.75rem; + font-weight: 600; +} + +.config-status-indicator.used { + background: rgba(16, 185, 129, 0.1); + color: #10b981; +} + +.config-status-indicator.unused { + background: rgba(245, 158, 11, 0.1); + color: #f59e0b; +} + +.linked-nodes-tags { + display: flex; + flex-wrap: wrap; + gap: 0.35rem; + justify-content: flex-end; + margin-top: 0.25rem; + width: 100%; +} + +.node-tag { + font-size: 0.65rem; + color: #3b82f6; + background: rgba(59, 130, 246, 0.08); + padding: 0.15rem 0.45rem; + border-radius: 4px; + border: 1px solid rgba(59, 130, 246, 0.15); + white-space: nowrap; + max-width: 250px; + overflow: hidden; + text-overflow: ellipsis; display: inline-flex; align-items: center; gap: 0.25rem; - padding: 0.25rem 0.5rem; - background: var(--bg-tertiary); - border-radius: 0.25rem; - font-size: 0.75rem; font-weight: 500; } -.config-item-status { - display: inline-flex; +.node-tag i { + font-size: 0.6rem; + color: #3b82f6; +} + +.btn-quick-link-main { + background: var(--primary-color); + color: white; + border: none; + padding: 0.35rem 0.75rem; + border-radius: 6px; + font-size: 0.75rem; + font-weight: 500; + cursor: pointer; + transition: all 0.2s ease; + display: flex; align-items: center; - gap: 0.25rem; - padding: 0.25rem 0.5rem; - border-radius: 0.25rem; - font-size: 0.75rem; - font-weight: 500; + gap: 0.35rem; } -.config-item-manager.used .config-item-status { background: var(--success-bg); color: var(--success-text); } -.config-item-manager.unused .config-item-status { background: var(--warning-bg); color: var(--warning-text); } -.config-item-manager.invalid .config-item-status { background: var(--danger-bg); color: var(--danger-text); } +.btn-quick-link-main:hover { + background: var(--btn-primary-hover); + transform: translateY(-1px); + box-shadow: 0 4px 12px rgba(59, 130, 246, 0.2); +} + +.config-item-chevron { + color: var(--text-tertiary); + transition: transform 0.3s ease; +} + +.config-item-manager.expanded .config-item-chevron { + transform: rotate(90deg); +} .config-item-details { - margin-top: 0.75rem; - padding-top: 0.75rem; + margin: 0; + padding: 1.5rem; border-top: 1px solid var(--border-color); + background: var(--bg-tertiary); display: none; } @@ -214,6 +357,41 @@ display: block; } +.config-item-actions { + display: flex; + gap: 0.75rem; + margin-top: 1.25rem; + padding-top: 1.25rem; + border-top: 1px dashed var(--border-color); +} + +@media (max-width: 992px) { + .config-item-main-row { + flex-direction: column; + align-items: flex-start; + gap: 1rem; + } + + .config-item-middle { + width: 100%; + } + + .config-meta-info { + flex-direction: row; + gap: 1.5rem; + } + + .config-item-right { + width: 100%; + justify-content: space-between; + } + + .config-status-col { + flex-direction: row; + align-items: center; + } +} + .config-details-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); @@ -235,13 +413,33 @@ letter-spacing: 0.05em; } +.config-detail-item.path-item { + grid-column: 1 / -1; +} + +.config-detail-item.path-item .config-detail-value { + cursor: pointer; + transition: var(--transition); + border: 1px solid transparent; + padding: 2px 4px; + margin-left: -4px; + border-radius: 4px; +} + +.config-detail-item.path-item .config-detail-value:hover { + color: var(--primary-color); +} + .config-detail-value { font-size: 0.875rem; color: var(--text-primary); - font-family: 'Courier New', monospace; word-break: break-all; + overflow-wrap: break-word; } +.config-detail-value.status-text-used { color: #10b981; font-weight: 600; } +.config-detail-value.status-text-unused { color: #f59e0b; font-weight: 600; } + .config-item-actions { display: flex; gap: 0.5rem; @@ -333,19 +531,68 @@ } .usage-detail-item { - display: flex; align-items: center; gap: 0.75rem; - padding: 0.75rem; background: var(--bg-primary); - border: 1px solid var(--border-color); border-radius: 0.375rem; + display: flex; + align-items: flex-start; + gap: 1rem; + padding: 1rem; + background: var(--bg-primary); + border: 1px solid var(--border-color); + border-radius: 0.5rem; + transition: var(--transition); +} + +.usage-detail-item:hover { + border-color: var(--primary-color); + box-shadow: var(--shadow-sm); +} + +.usage-detail-item i { + margin-top: 0.25rem; + color: var(--primary-color); + font-size: 1.1rem; +} + +.usage-detail-content { + flex: 1; + display: flex; + flex-direction: column; + gap: 0.5rem; +} + +.usage-detail-top { + display: flex; + align-items: center; + gap: 1rem; + flex-wrap: wrap; +} + +.usage-detail-subtitle { + font-size: 0.75rem; + color: var(--text-tertiary); + font-family: 'Courier New', monospace; + background: var(--bg-secondary); + padding: 0.25rem 0.5rem; + border-radius: 4px; + display: inline-block; + word-break: break-all; } .usage-detail-type { - font-size: 0.75rem; font-weight: 600; background: var(--primary-color); - color: var(--white); padding: 0.125rem 0.5rem; border-radius: 0.25rem; + font-size: 0.7rem; + font-weight: 700; + background: var(--primary-color); + color: white; + padding: 0.15rem 0.5rem; + border-radius: 4px; + text-transform: uppercase; + letter-spacing: 0.02em; } .usage-detail-location { - font-size: 0.875rem; color: var(--text-secondary); - font-family: 'Courier New', monospace; word-break: break-all; flex: 1; + font-size: 0.9rem; + color: var(--text-primary); + font-weight: 500; + word-break: break-all; } /* 删除确认模态框样式 */ diff --git a/static/components/section-upload-config.html b/static/components/section-upload-config.html index a18c1e0..95a34b2 100644 --- a/static/components/section-upload-config.html +++ b/static/components/section-upload-config.html @@ -19,13 +19,7 @@