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:
hex2077 2026-03-03 20:05:51 +08:00
parent 3989add60b
commit 05df61df74
20 changed files with 1360 additions and 796 deletions

View file

@ -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に十分な権限があることを確認

View file

@ -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 有足够权限

View file

@ -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

View file

@ -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

View file

@ -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'];

View file

@ -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'
});
}

View file

@ -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();

View file

@ -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
};

View file

@ -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)',

View file

@ -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
};

View file

@ -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;

View file

@ -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
};

View 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
};

View file

@ -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
};

View file

@ -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',

View file

@ -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
};

View file

@ -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>

View file

@ -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">

View file

@ -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;
}
/* 删除确认模态框样式 */

View file

@ -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">