feat: 添加 TLS Sidecar 支持并重构前端提供商配置系统
- 新增 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 模型版本信息
This commit is contained in:
parent
3989add60b
commit
05df61df74
20 changed files with 1360 additions and 796 deletions
43
README-JA.md
43
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 ユーザー**: イメージには既にプリビルド済みのバイナリが含まれています。設定で有効にするだけでよく、手動でのビルドは不要です。
|
||||
|
||||
</details>
|
||||
|
||||
---
|
||||
|
|
@ -664,6 +706,7 @@ kill -9 <PID>
|
|||
**問題の説明**:APIリクエストが403 Forbiddenエラーを返します。
|
||||
|
||||
**解決策**:
|
||||
- **TLS Sidecar の有効化**:Grok などのサービスにおいて、403 エラーは TLS 指紋のブロックが原因であることが多いです。[高度な設定 - TLS Sidecar](#5-tls-sidecar-bypass-403cloudflare) を参照して Sidecar を有効にし、ビルドしてください。
|
||||
- **ノード状態を確認**:Web UIの「プロバイダープール」ページでノード状態が正常(ヘルスチェック合格)であれば、このエラーは無視できます。システムが自動的に処理します
|
||||
- **アカウント権限を確認**:使用しているアカウントがリクエストされたモデルまたはサービスにアクセスする権限があることを確認
|
||||
- **API Key権限を確認**:一部のプロバイダーのAPI Keyにはアクセス範囲の制限がある場合があります。Keyに十分な権限があることを確認
|
||||
|
|
|
|||
43
README-ZH.md
43
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 用户**:镜像已内置编译好的二进制,只需在配置中开启即可,无需手动编译。
|
||||
|
||||
</details>
|
||||
|
||||
---
|
||||
|
|
@ -663,6 +705,7 @@ kill -9 <PID>
|
|||
**问题描述**:API 请求返回 403 Forbidden 错误。
|
||||
|
||||
**解决方案**:
|
||||
- **开启 TLS Sidecar**:针对 Grok 等服务,403 通常是因为 TLS 指纹被屏蔽。请参考 [高级配置 - TLS Sidecar](#5-tls-sidecar-bypass-403cloudflare) 开启并编译 Sidecar。
|
||||
- **检查节点状态**:如果在 Web UI 的"提供商池"页面中看到节点状态正常(健康检查通过),则可以忽略此报错,系统会自动处理
|
||||
- **检查账号权限**:确认使用的账号有权限访问请求的模型或服务
|
||||
- **检查 API Key 权限**:某些提供商的 API Key 可能有访问范围限制,确保 Key 有足够权限
|
||||
|
|
|
|||
43
README.md
43
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.
|
||||
|
||||
</details>
|
||||
|
||||
---
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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'];
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
||||
|
|
|
|||
|
|
@ -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 => `
|
||||
<button type="button" class="provider-tag" data-value="${c.id}">
|
||||
<i class="fas ${c.icon || 'fa-server'}"></i>
|
||||
<span>${c.name}</span>
|
||||
</button>
|
||||
`).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
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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': '如果授权窗口重定向后显示“无法访问”,请将该窗口地址栏的 <strong>完整 URL</strong> 粘贴到下方:',
|
||||
|
|
@ -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 <strong>Full URL</strong> 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)',
|
||||
|
|
|
|||
|
|
@ -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<Object>} 模型数据
|
||||
|
|
@ -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
|
||||
};
|
||||
fetchProviderModels,
|
||||
updateModelsProviderConfigs
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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 = `
|
||||
<div class="routing-card-header">
|
||||
<i class="fas ${icon}"></i>
|
||||
<h4>${routeInfo.name}</h4>
|
||||
<span class="provider-badge ${routeInfo.badgeClass}">${routeInfo.badge}</span>
|
||||
</div>
|
||||
<div class="routing-card-content">
|
||||
<div class="protocol-tabs">
|
||||
<button class="protocol-tab ${config.id === 'openai-codex-oauth' ? '' : 'active'}" data-protocol="openai" data-i18n="dashboard.routing.openai">${t('dashboard.routing.openai')}</button>
|
||||
<button class="protocol-tab ${config.id === 'openai-codex-oauth' ? 'active' : ''}" data-protocol="claude" data-i18n="dashboard.routing.claude">${t('dashboard.routing.claude')}</button>
|
||||
</div>
|
||||
|
||||
<div class="protocol-content ${config.id === 'openai-codex-oauth' ? '' : 'active'}" data-protocol="openai">
|
||||
<div class="endpoint-info">
|
||||
<label data-i18n="dashboard.routing.endpoint">${t('dashboard.routing.endpoint')}</label>
|
||||
<code class="endpoint-path">${routeInfo.paths.openai}</code>
|
||||
</div>
|
||||
<div class="usage-example">
|
||||
<label data-i18n="dashboard.routing.exampleOpenAI">${t('dashboard.routing.exampleOpenAI')}</label>
|
||||
<pre><code>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
|
||||
}'</code></pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="protocol-content ${config.id === 'openai-codex-oauth' ? 'active' : ''}" data-protocol="claude">
|
||||
<div class="endpoint-info">
|
||||
<label data-i18n="dashboard.routing.endpoint">${t('dashboard.routing.endpoint')}</label>
|
||||
<code class="endpoint-path">${routeInfo.paths.claude}</code>
|
||||
</div>
|
||||
<div class="usage-example">
|
||||
<label data-i18n="dashboard.routing.exampleClaude">${t('dashboard.routing.exampleClaude')}</label>
|
||||
<pre><code>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!"}]
|
||||
}'</code></pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
container.appendChild(card);
|
||||
});
|
||||
|
||||
// 重新初始化卡片交互
|
||||
initCardInteractions();
|
||||
}
|
||||
|
||||
export {
|
||||
initRoutingExamples,
|
||||
getAvailableRoutes,
|
||||
highlightProviderRoute,
|
||||
copyCurlExample
|
||||
};
|
||||
copyCurlExample,
|
||||
renderRoutingExamples
|
||||
};
|
||||
|
|
|
|||
57
static/app/tutorial-manager.js
Normal file
57
static/app/tutorial-manager.js
Normal file
|
|
@ -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 => `
|
||||
<div class="oauth-path-item">
|
||||
<div class="path-header">
|
||||
<i class="fas ${p.icon || 'fa-key'}"></i>
|
||||
<span class="path-provider">${p.name}</span>
|
||||
</div>
|
||||
<code class="path-value">${p.defaultPath}</code>
|
||||
</div>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
export {
|
||||
initTutorialManager,
|
||||
renderOauthPaths,
|
||||
updateTutorialProviderConfigs
|
||||
};
|
||||
|
|
@ -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 ?
|
||||
`<span class="provider-type-tag tag-${providerInfo.shortName}">
|
||||
<i class="fas fa-robot"></i> ${providerInfo.displayName}
|
||||
</span>` : '';
|
||||
|
||||
// 生成关联详情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 = `<div class="linked-nodes-tags">
|
||||
${uniqueParts.map(name => `<span class="node-tag" title="${name}"><i class="fas fa-link"></i> ${name}</span>`).join('')}
|
||||
</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
// 判断是否可以一键关联(未关联且路径包含支持的提供商目录)
|
||||
const providerInfo = detectProviderFromPath(config.path);
|
||||
const canQuickLink = !config.isUsed && providerInfo !== null;
|
||||
const quickLinkBtnHtml = canQuickLink ?
|
||||
`<button class="btn-quick-link" data-path="${config.path}" title="一键关联到 ${providerInfo.displayName}">
|
||||
<i class="fas fa-link"></i> ${providerInfo.shortName}
|
||||
`<button class="btn-quick-link-main" data-path="${config.path}" title="一键关联到 ${providerInfo.displayName}">
|
||||
<i class="fas fa-link"></i> ${t('upload.action.quickLink')}
|
||||
</button>` : '';
|
||||
|
||||
item.innerHTML = `
|
||||
<div class="config-item-header">
|
||||
<div class="config-item-name">${config.name}</div>
|
||||
<div class="config-item-path" title="${config.path}">${config.path}</div>
|
||||
</div>
|
||||
<div class="config-item-meta">
|
||||
<div class="config-item-size">${formatFileSize(config.size)}</div>
|
||||
<div class="config-item-modified">${formatDate(config.modified)}</div>
|
||||
<div class="config-item-status">
|
||||
<i class="fas ${statusIcon}"></i>
|
||||
<span data-i18n="${config.isUsed ? 'upload.statusFilter.used' : 'upload.statusFilter.unused'}">${statusText}</span>
|
||||
${quickLinkBtnHtml}
|
||||
<div class="config-item-main-row">
|
||||
<div class="config-item-left">
|
||||
<div class="config-item-icon-wrapper ${config.type || 'other'}">
|
||||
<i class="fas ${typeIcon}"></i>
|
||||
</div>
|
||||
<div class="config-item-title-area">
|
||||
<div class="config-item-name-line">
|
||||
<span class="config-item-display-name">${config.name}</span>
|
||||
${providerBadge}
|
||||
</div>
|
||||
<div class="config-item-path-line" title="${config.path}">
|
||||
<i class="fas fa-folder-open"></i> ${config.path}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="config-item-middle">
|
||||
<div class="config-meta-info">
|
||||
<span class="meta-item" title="文件大小">
|
||||
<i class="fas fa-weight-hanging"></i> ${formatFileSize(config.size)}
|
||||
</span>
|
||||
<span class="meta-item" title="最后修改时间">
|
||||
<i class="fas fa-calendar-alt"></i> ${formatDate(config.modified)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="config-item-right">
|
||||
<div class="config-status-col">
|
||||
<div class="config-status-indicator ${configStatus}">
|
||||
<i class="fas ${statusIcon}"></i>
|
||||
<span data-i18n="${config.isUsed ? 'upload.statusFilter.used' : 'upload.statusFilter.unused'}">${statusText}</span>
|
||||
</div>
|
||||
${linkedNodesInfo}
|
||||
${quickLinkBtnHtml}
|
||||
</div>
|
||||
<div class="config-item-chevron">
|
||||
<i class="fas fa-chevron-right"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="config-item-details">
|
||||
<div class="config-details-grid">
|
||||
<div class="config-detail-item">
|
||||
<div class="config-detail-label" data-i18n="upload.detail.path">文件路径</div>
|
||||
<div class="config-detail-item path-item">
|
||||
<div class="config-detail-label" data-i18n="upload.detail.path">文件完整路径</div>
|
||||
<div class="config-detail-value">${config.path}</div>
|
||||
</div>
|
||||
<div class="config-detail-item">
|
||||
|
|
@ -126,12 +185,12 @@ function createConfigItemElement(config, index) {
|
|||
<div class="config-detail-value">${formatFileSize(config.size)}</div>
|
||||
</div>
|
||||
<div class="config-detail-item">
|
||||
<div class="config-detail-label" data-i18n="upload.detail.modified">最后修改</div>
|
||||
<div class="config-detail-label" data-i18n="upload.detail.modified">最后修改时间</div>
|
||||
<div class="config-detail-value">${formatDate(config.modified)}</div>
|
||||
</div>
|
||||
<div class="config-detail-item">
|
||||
<div class="config-detail-label" data-i18n="upload.detail.status">关联状态</div>
|
||||
<div class="config-detail-value" data-i18n="${config.isUsed ? 'upload.statusFilter.used' : 'upload.statusFilter.unused'}">${statusText}</div>
|
||||
<div class="config-detail-label" data-i18n="upload.detail.status">当前关联状态</div>
|
||||
<div class="config-detail-value status-text-${configStatus}" data-i18n="${config.isUsed ? 'upload.statusFilter.used' : 'upload.statusFilter.unused'}">${statusText}</div>
|
||||
</div>
|
||||
</div>
|
||||
${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 += `
|
||||
<div class="usage-detail-item" data-usage-type="${usageTypeKey}">
|
||||
<i class="fas ${icon}"></i>
|
||||
<span class="usage-detail-type">${detail.type}</span>
|
||||
<span class="usage-detail-location">${detail.location}</span>
|
||||
<div class="usage-detail-content">
|
||||
<div class="usage-detail-top">
|
||||
<span class="usage-detail-type">${detail.type}</span>
|
||||
<span class="usage-detail-location">${displayTitle}</span>
|
||||
</div>
|
||||
${subtitle ? `<div class="usage-detail-subtitle">${subtitle}</div>` : ''}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
|
|
@ -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
|
||||
};
|
||||
deleteUnboundConfigs,
|
||||
updateProviderFilterOptions
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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 <span class="optional-tag">${t('config.optional')}</span>`,
|
||||
type: 'text',
|
||||
placeholder: 'https://grok.com'
|
||||
}
|
||||
],
|
||||
'forward-api': [
|
||||
{
|
||||
id: 'FORWARD_API_KEY',
|
||||
|
|
@ -404,6 +460,7 @@ export {
|
|||
showToast,
|
||||
getFieldLabel,
|
||||
getProviderTypeFields,
|
||||
getProviderConfigs,
|
||||
getProviderStats,
|
||||
apiRequest
|
||||
};
|
||||
|
|
@ -126,481 +126,10 @@
|
|||
<p class="routing-description" data-i18n="dashboard.routing.description">通过不同路径路由访问不同的AI模型提供商,支持灵活的模型切换</p>
|
||||
|
||||
<div class="routing-examples-grid">
|
||||
<div class="routing-example-card" data-provider="gemini-cli-oauth-card">
|
||||
<div class="routing-card-header">
|
||||
<i class="fas fa-gem"></i>
|
||||
<h4 data-i18n="dashboard.routing.nodeName.gemini">Gemini CLI OAuth</h4>
|
||||
<span class="provider-badge oauth" data-i18n="dashboard.routing.oauth">突破限制</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">/gemini-cli-oauth/v1/chat/completions</code>
|
||||
</div>
|
||||
<div class="usage-example">
|
||||
<label data-i18n="dashboard.routing.exampleOpenAI">使用示例 (OpenAI格式):</label>
|
||||
<pre><code>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
|
||||
}'</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">/gemini-cli-oauth/v1/messages</code>
|
||||
</div>
|
||||
<div class="usage-example">
|
||||
<label data-i18n="dashboard.routing.exampleClaude">使用示例 (Claude格式):</label>
|
||||
<pre><code>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!"}]
|
||||
}'</code></pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="routing-example-card" data-provider="gemini-antigravity-card">
|
||||
<div class="routing-card-header">
|
||||
<i class="fas fa-rocket"></i>
|
||||
<h4 data-i18n="dashboard.routing.nodeName.antigravity">Gemini Antigravity</h4>
|
||||
<span class="provider-badge oauth" data-i18n="dashboard.routing.experimental">突破限制/实验性</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">/gemini-antigravity/v1/chat/completions</code>
|
||||
</div>
|
||||
<div class="usage-example">
|
||||
<label data-i18n="dashboard.routing.exampleOpenAI">使用示例 (OpenAI格式):</label>
|
||||
<pre><code>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
|
||||
}'</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">/gemini-antigravity/v1/messages</code>
|
||||
</div>
|
||||
<div class="usage-example">
|
||||
<label data-i18n="dashboard.routing.exampleClaude">使用示例 (Claude格式):</label>
|
||||
<pre><code>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!"}]
|
||||
}'</code></pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="routing-example-card" data-provider="claude-custom-card">
|
||||
<div class="routing-card-header">
|
||||
<i class="fas fa-brain"></i>
|
||||
<h4 data-i18n="dashboard.routing.nodeName.claude">Claude Custom</h4>
|
||||
<span class="provider-badge official" data-i18n="dashboard.routing.official">官方API/三方</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-custom/v1/chat/completions</code>
|
||||
</div>
|
||||
<div class="usage-example">
|
||||
<label data-i18n="dashboard.routing.exampleOpenAI">使用示例 (OpenAI格式):</label>
|
||||
<pre><code>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
|
||||
}'</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-custom/v1/messages</code>
|
||||
</div>
|
||||
<div class="usage-example">
|
||||
<label data-i18n="dashboard.routing.exampleClaude">使用示例 (Claude格式):</label>
|
||||
<pre><code>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!"}]
|
||||
}'</code></pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="routing-example-card" data-provider="claude-kiro-oauth-card">
|
||||
<div class="routing-card-header">
|
||||
<i class="fas fa-robot"></i>
|
||||
<h4 data-i18n="dashboard.routing.nodeName.kiro">Claude Kiro 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-kiro-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-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
|
||||
}'</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-kiro-oauth/v1/messages</code>
|
||||
</div>
|
||||
<div class="usage-example">
|
||||
<label data-i18n="dashboard.routing.exampleClaude">使用示例 (Claude格式):</label>
|
||||
<pre><code>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!"}]
|
||||
}'</code></pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="routing-example-card" data-provider="openai-custom-card">
|
||||
<div class="routing-card-header">
|
||||
<i class="fas fa-comments"></i>
|
||||
<h4 data-i18n="dashboard.routing.nodeName.openai">OpenAI Custom</h4>
|
||||
<span class="provider-badge official" data-i18n="dashboard.routing.official">官方API/三方</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">/openai-custom/v1/chat/completions</code>
|
||||
</div>
|
||||
<div class="usage-example">
|
||||
<label data-i18n="dashboard.routing.exampleOpenAI">使用示例 (OpenAI格式):</label>
|
||||
<pre><code>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
|
||||
}'</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">/openai-custom/v1/messages</code>
|
||||
</div>
|
||||
<div class="usage-example">
|
||||
<label data-i18n="dashboard.routing.exampleClaude">使用示例 (Claude格式):</label>
|
||||
<pre><code>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!"}]
|
||||
}'</code></pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="routing-example-card" data-provider="openai-qwen-oauth-card">
|
||||
<div class="routing-card-header">
|
||||
<i class="fas fa-code"></i>
|
||||
<h4 data-i18n="dashboard.routing.nodeName.qwen">Qwen OAuth</h4>
|
||||
<span class="provider-badge oauth" data-i18n="dashboard.routing.oauth">突破限制</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">/openai-qwen-oauth/v1/chat/completions</code>
|
||||
</div>
|
||||
<div class="usage-example">
|
||||
<label data-i18n="dashboard.routing.exampleOpenAI">使用示例 (OpenAI格式):</label>
|
||||
<pre><code>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
|
||||
}'</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">/openai-qwen-oauth/v1/messages</code>
|
||||
</div>
|
||||
<div class="usage-example">
|
||||
<label data-i18n="dashboard.routing.exampleClaude">使用示例 (Claude格式):</label>
|
||||
<pre><code>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!"}]
|
||||
}'</code></pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="routing-example-card" data-provider="openai-iflow-card">
|
||||
<div class="routing-card-header">
|
||||
<i class="fas fa-wind"></i>
|
||||
<h4 data-i18n="dashboard.routing.nodeName.iflow">iFlow OAuth</h4>
|
||||
<span class="provider-badge oauth" data-i18n="dashboard.routing.oauth">突破限制</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">/openai-iflow/v1/chat/completions</code>
|
||||
</div>
|
||||
<div class="usage-example">
|
||||
<label data-i18n="dashboard.routing.exampleOpenAI">使用示例 (OpenAI格式):</label>
|
||||
<pre><code>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
|
||||
}'</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">/openai-iflow/v1/messages</code>
|
||||
</div>
|
||||
<div class="usage-example">
|
||||
<label data-i18n="dashboard.routing.exampleClaude">使用示例 (Claude格式):</label>
|
||||
<pre><code>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!"}]
|
||||
}'</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>
|
||||
<h4 data-i18n="dashboard.routing.nodeName.codex">OpenAI Codex OAuth</h4>
|
||||
<span class="provider-badge oauth" data-i18n="dashboard.routing.oauth">突破限制</span>
|
||||
</div>
|
||||
<div class="routing-card-content">
|
||||
<!-- 协议标签切换 -->
|
||||
<div class="protocol-tabs">
|
||||
<button class="protocol-tab active" data-protocol="openai" data-i18n="dashboard.routing.openai">OpenAI协议</button>
|
||||
<button class="protocol-tab" data-protocol="claude" data-i18n="dashboard.routing.claude">Claude协议</button>
|
||||
</div>
|
||||
|
||||
<!-- OpenAI协议示例 -->
|
||||
<div class="protocol-content active" data-protocol="openai">
|
||||
<div class="endpoint-info">
|
||||
<label data-i18n="dashboard.routing.endpoint">端点路径:</label>
|
||||
<code class="endpoint-path">/openai-codex-oauth/v1/chat/completions</code>
|
||||
</div>
|
||||
<div class="usage-example">
|
||||
<label data-i18n="dashboard.routing.exampleOpenAI">使用示例 (OpenAI格式):</label>
|
||||
<pre><code>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
|
||||
}'</code></pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Claude协议示例 -->
|
||||
<div class="protocol-content" data-protocol="claude">
|
||||
<div class="endpoint-info">
|
||||
<label data-i18n="dashboard.routing.endpoint">端点路径:</label>
|
||||
<code class="endpoint-path">/openai-codex-oauth/v1/messages</code>
|
||||
</div>
|
||||
<div class="usage-example">
|
||||
<label data-i18n="dashboard.routing.exampleClaude">使用示例 (Claude格式):</label>
|
||||
<pre><code>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认证流程"}]
|
||||
}'</code></pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="routing-example-card" data-provider="grok-custom-card">
|
||||
<div class="routing-card-header">
|
||||
<i class="fas fa-search"></i>
|
||||
<h4 data-i18n="dashboard.routing.nodeName.grok">Grok Reverse</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 active" data-protocol="openai" data-i18n="dashboard.routing.openai">OpenAI协议</button>
|
||||
<button class="protocol-tab" data-protocol="claude" data-i18n="dashboard.routing.claude">Claude协议</button>
|
||||
</div>
|
||||
|
||||
<!-- OpenAI协议示例 -->
|
||||
<div class="protocol-content active" data-protocol="openai">
|
||||
<div class="endpoint-info">
|
||||
<label data-i18n="dashboard.routing.endpoint">端点路径:</label>
|
||||
<code class="endpoint-path">/grok-custom/v1/chat/completions</code>
|
||||
</div>
|
||||
<div class="usage-example">
|
||||
<label data-i18n="dashboard.routing.exampleOpenAI">使用示例 (OpenAI格式):</label>
|
||||
<pre><code>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
|
||||
}'</code></pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Claude协议示例 -->
|
||||
<div class="protocol-content" data-protocol="claude">
|
||||
<div class="endpoint-info">
|
||||
<label data-i18n="dashboard.routing.endpoint">端点路径:</label>
|
||||
<code class="endpoint-path">/grok-custom/v1/messages</code>
|
||||
</div>
|
||||
<div class="usage-example">
|
||||
<label data-i18n="dashboard.routing.exampleClaude">使用示例 (Claude格式):</label>
|
||||
<pre><code>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": "你好"}]
|
||||
}'</code></pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 路径路由示例将由 routing-examples.js 动态渲染 -->
|
||||
<div class="routing-examples-loading">
|
||||
<i class="fas fa-spinner fa-spin"></i>
|
||||
<span data-i18n="common.loading">加载中...</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@
|
|||
<span class="file-name">config.json</span>
|
||||
<span class="file-badge required" data-i18n="tutorial.config.badge.required">必需</span>
|
||||
</div>
|
||||
<p class="file-desc" data-i18n="tutorial.config.file.config">主配置文件,包含 API Key、端口、模型提供商等核心设置</p>
|
||||
<p class="file-desc" data-i18n="tutorial.config.file.config">主配置文件,包含 API Key、端口、模型提供商等核心设置 (保存配置管理后自动新建)</p>
|
||||
</div>
|
||||
<div class="config-file-item">
|
||||
<div class="file-header">
|
||||
|
|
@ -24,7 +24,7 @@
|
|||
<span class="file-name">provider_pools.json</span>
|
||||
<span class="file-badge required" data-i18n="tutorial.config.badge.required">必需</span>
|
||||
</div>
|
||||
<p class="file-desc" data-i18n="tutorial.config.file.pools">提供商池配置,用于多账号轮询和故障转移</p>
|
||||
<p class="file-desc" data-i18n="tutorial.config.file.pools">提供商池配置,用于多账号轮询和故障转移 (保存节点后自动新建)</p>
|
||||
</div>
|
||||
<div class="config-file-item">
|
||||
<div class="file-header">
|
||||
|
|
@ -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 @@
|
|||
<div class="tutorial-content">
|
||||
<p data-i18n="tutorial.oauth.desc">各提供商的 OAuth 凭据文件默认存储位置(建议保持在 <code>configs/</code> 目录下以便统一管理):</p>
|
||||
|
||||
<div class="oauth-path-list">
|
||||
<div class="oauth-path-item">
|
||||
<div class="path-header">
|
||||
<i class="fas fa-robot"></i>
|
||||
<span class="path-provider">Gemini CLI OAuth</span>
|
||||
</div>
|
||||
<code class="path-value">configs/gemini/</code>
|
||||
</div>
|
||||
<div class="oauth-path-item">
|
||||
<div class="path-header">
|
||||
<i class="fas fa-rocket"></i>
|
||||
<span class="path-provider">Gemini Antigravity</span>
|
||||
</div>
|
||||
<code class="path-value">configs/antigravity/</code>
|
||||
</div>
|
||||
<div class="oauth-path-item">
|
||||
<div class="path-header">
|
||||
<i class="fas fa-key"></i>
|
||||
<span class="path-provider">Claude Kiro OAuth</span>
|
||||
</div>
|
||||
<code class="path-value">configs/kiro/</code>
|
||||
</div>
|
||||
<div class="oauth-path-item">
|
||||
<div class="path-header">
|
||||
<i class="fas fa-cloud"></i>
|
||||
<span class="path-provider">Qwen OAuth</span>
|
||||
</div>
|
||||
<code class="path-value">configs/qwen/</code>
|
||||
</div>
|
||||
<div class="oauth-path-item">
|
||||
<div class="path-header">
|
||||
<i class="fas fa-stream"></i>
|
||||
<span class="path-provider">iFlow OAuth</span>
|
||||
</div>
|
||||
<code class="path-value">configs/iflow/</code>
|
||||
</div>
|
||||
<div class="oauth-path-item">
|
||||
<div class="path-header">
|
||||
<i class="fas fa-code"></i>
|
||||
<span class="path-provider">OpenAI Codex OAuth</span>
|
||||
</div>
|
||||
<code class="path-value">configs/codex/</code>
|
||||
</div>
|
||||
<div id="oauthPathList" class="oauth-path-list">
|
||||
<!-- 动态生成 -->
|
||||
</div>
|
||||
|
||||
<div class="tutorial-note">
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
/* 删除确认模态框样式 */
|
||||
|
|
|
|||
|
|
@ -19,13 +19,7 @@
|
|||
<label for="configProviderFilter" data-i18n="upload.providerFilter">提供商类型</label>
|
||||
<select id="configProviderFilter" class="form-control">
|
||||
<option value="" data-i18n="upload.providerFilter.all">全部提供商</option>
|
||||
<option value="gemini-cli-oauth" data-i18n="upload.providerFilter.gemini">Gemini CLI OAuth</option>
|
||||
<option value="gemini-antigravity" data-i18n="upload.providerFilter.antigravity">Gemini Antigravity</option>
|
||||
<option value="claude-kiro-oauth" data-i18n="upload.providerFilter.kiro">Claude Kiro OAuth</option>
|
||||
<option value="openai-qwen-oauth" data-i18n="upload.providerFilter.qwen">OpenAI Qwen OAuth</option>
|
||||
<option value="openai-iflow" data-i18n="upload.providerFilter.iflow">OpenAI iFlow</option>
|
||||
<option value="openai-codex-oauth" data-i18n="upload.providerFilter.codex">OpenAI Codex OAuth</option>
|
||||
<option value="other" data-i18n="upload.providerFilter.other">其他/未识别</option>
|
||||
<!-- 选项将由 provider-manager.js 动态填充 -->
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
|
|
|
|||
Loading…
Reference in a new issue