AIClient-2-API/static/index.html
ZqinKing 4ec39f4df6 feat(fallback): 新增跨协议模型 Fallback 映射功能
## 功能概述
实现跨协议模型级别的 Fallback 映射机制,允许在主 Provider 不可用时,
根据模型名称自动映射到其他协议的 Provider 和模型。

## 设计原理
- 原有 providerFallbackChain 只支持同协议内回退
- 新增 modelFallbackMapping 支持跨协议映射(如 Gemini → Claude)
- 优先级:同协议回退 > 跨协议模型映射

## 代码修改
- configs/config.json.example: 新增 modelFallbackMapping 配置示例
- src/provider-pool-manager.js: 实现跨协议映射选择逻辑
- src/service-manager.js: 传递 actualModel 参数
- src/common.js: 处理模型 fallback 后的模型名更新
- src/ui-manager.js: UI配置读写支持
- static/app/config-manager.js: 前端配置管理
- static/app/i18n.js: 中英文国际化文案
- static/index.html: 高级配置区新增配置项

## 配置示例
modelFallbackMapping: {
  "gemini-claude-opus-4-5-thinking": {
    "targetProviderType": "claude-kiro-oauth",
    "targetModel": "claude-opus-4-5"
  }
}
2026-01-04 08:40:40 +08:00

986 lines
65 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html lang="zh-CN" id="html-root">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=5.0, user-scalable=yes">
<meta name="theme-color" content="#059669">
<meta name="description" content="AIClient2API Management Console - Unified management of AI service providers" data-i18n="header.description">
<title data-i18n="header.title">AIClient2API - 管理控制台</title>
<link rel="stylesheet" href="app/styles.css">
<link rel="stylesheet" href="app/mobile.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
</head>
<body>
<div class="container">
<!-- Header -->
<header class="header">
<div class="header-content">
<h1><i class="fas fa-robot"></i> <span class="header-title" data-i18n="header.title">AIClient2API 管理控制台</span></h1>
<div class="header-controls">
<span class="status-badge" id="serverStatus">
<i class="fas fa-circle"></i> <span class="status-text" data-i18n="header.status.connecting">连接中...</span>
</span>
<button id="themeToggleBtn" class="theme-toggle" aria-label="Toggle Theme" data-i18n-aria-label="header.themeToggle" title="切换主题" data-i18n-title="header.themeToggle">
<i class="fas fa-moon"></i>
<i class="fas fa-sun"></i>
</button>
<button id="logoutBtn" class="logout-btn" data-i18n="header.logout" title="Logout" data-i18n-title="header.logout">
<i class="fas fa-sign-out-alt"></i> <span data-i18n="header.logout">登出</span>
</button>
<button id="restartBtn" class="logout-btn" aria-label="Restart Service" data-i18n-aria-label="header.restart">
<i id="restartBtnIcon" class="fas fa-redo"></i> <span id="restartBtnText" class="btn-text" data-i18n="header.restart">重启</span>
</button>
</div>
</div>
</header>
<!-- Main Content -->
<div class="main-content">
<!-- Sidebar -->
<aside class="sidebar" role="navigation" aria-label="Main Navigation" data-i18n-aria-label="nav.main">
<nav class="sidebar-nav">
<a href="#dashboard" class="nav-item active" data-section="dashboard" aria-label="Dashboard" data-i18n-aria-label="nav.dashboard">
<i class="fas fa-tachometer-alt" aria-hidden="true"></i> <span data-i18n="nav.dashboard">仪表盘</span>
</a>
<a href="#config" class="nav-item" data-section="config" aria-label="Config Management" data-i18n-aria-label="nav.config">
<i class="fas fa-cog" aria-hidden="true"></i> <span data-i18n="nav.config">配置管理</span>
</a>
<a href="#providers" class="nav-item" data-section="providers" aria-label="Provider Pool Management" data-i18n-aria-label="nav.providers">
<i class="fas fa-network-wired" aria-hidden="true"></i> <span data-i18n="nav.providers">提供商池管理</span>
</a>
<a href="#upload-config" class="nav-item" data-section="upload-config" aria-label="Upload Config Management" data-i18n-aria-label="nav.upload">
<i class="fas fa-upload" aria-hidden="true"></i> <span data-i18n="nav.upload">配置管理</span>
</a>
<a href="#usage" class="nav-item" data-section="usage" aria-label="Usage Query" data-i18n-aria-label="nav.usage">
<i class="fas fa-chart-bar" aria-hidden="true"></i> <span data-i18n="nav.usage">用量查询</span>
</a>
<a href="#logs" class="nav-item" data-section="logs" aria-label="Real-time Logs" data-i18n-aria-label="nav.logs">
<i class="fas fa-file-alt" aria-hidden="true"></i> <span data-i18n="nav.logs">实时日志</span>
</a>
</nav>
</aside>
<!-- Content Area -->
<main class="content" role="main">
<!-- Dashboard Section -->
<section id="dashboard" class="section active" aria-labelledby="dashboard-title">
<h2 id="dashboard-title" data-i18n="dashboard.title">系统概览</h2>
<div class="dashboard-top-row">
<div class="stats-grid">
<div class="stat-card">
<div class="stat-icon">
<i class="fas fa-clock"></i>
</div>
<div class="stat-info">
<h3 id="uptime">--</h3>
<p data-i18n="dashboard.uptime">运行时间</p>
</div>
</div>
</div>
<!-- Contact and Sponsor Section -->
<div class="contact-section dashboard-contact">
<div class="contact-grid">
<div class="contact-card">
<h3><i id="wechat-icon" class="fab fa-weixin"></i> <span id="wechat-title" data-i18n="dashboard.contact.wechat">扫码进群,注明来意</span></h3>
<div class="qr-container">
<img src="static/wechat.png" id="wechat-img" alt="微信二维码" class="qr-code clickable-qr">
</div>
<p class="qr-description" id="wechat-desc" data-i18n="dashboard.contact.wechatDesc">添加微信获取更多技术支持和交流</p>
</div>
<div class="contact-card" id="sponsor-card">
<h3><i class="fas fa-heart"></i> <span id="sponsor-title" data-i18n="dashboard.contact.sponsor">扫码赞助</span></h3>
<div class="qr-container">
<img src="static/sponsor.png" id="sponsor-img" alt="赞助二维码" class="qr-code clickable-qr">
</div>
<p class="qr-description" id="sponsor-desc" data-i18n="dashboard.contact.sponsorDesc">您的赞助是项目持续发展的动力</p>
</div>
</div>
</div>
</div>
<!-- System Information Panel -->
<div class="system-info-panel">
<div class="system-info-header">
<h3 data-i18n="dashboard.systemInfo">系统信息</h3>
<div class="update-controls">
<button id="checkUpdateBtn" class="btn btn-outline btn-sm" data-i18n-title="dashboard.update.checkTitle" title="检查更新">
<i class="fas fa-sync-alt"></i> <span data-i18n="dashboard.update.check">检查更新</span>
</button>
<button id="performUpdateBtn" class="btn btn-primary btn-sm" style="display: none;" data-i18n-title="dashboard.update.performTitle" title="执行更新">
<i class="fas fa-download"></i> <span data-i18n="dashboard.update.perform">立即更新</span>
</button>
</div>
</div>
<div class="info-grid">
<div class="info-item">
<span class="info-label">
<i class="fas fa-tag"></i> <span data-i18n="dashboard.version">版本号</span>
</span>
<div class="version-display-wrapper">
<span class="info-value" id="appVersion">--</span>
<span class="update-badge" id="updateBadge" style="display: none;">
<i class="fas fa-arrow-up"></i> <span id="latestVersionText">--</span>
</span>
</div>
</div>
<div class="info-item">
<span class="info-label">
<i class="fas fa-code"></i> <span data-i18n="dashboard.nodeVersion">Node.js版本</span>
</span>
<div class="version-display-wrapper">
<span class="info-value" id="nodeVersion">--</span>
</div>
</div>
<div class="info-item">
<span class="info-label">
<i class="fas fa-clock"></i> <span data-i18n="dashboard.serverTime">服务器时间</span>
</span>
<div class="version-display-wrapper">
<span class="info-value" id="serverTime">--</span>
</div>
</div>
<div class="info-item">
<span class="info-label">
<i class="fas fa-desktop"></i> <span data-i18n="dashboard.platform">操作系统</span>
</span>
<div class="version-display-wrapper">
<span class="info-value" id="platformInfo">--</span>
</div>
</div>
<div class="info-item">
<span class="info-label">
<i class="fas fa-memory"></i> <span data-i18n="dashboard.memoryUsage">内存使用</span>
</span>
<div class="version-display-wrapper">
<span class="info-value" id="memoryUsage">--</span>
</div>
</div>
<div class="info-item">
<span class="info-label">
<i class="fas fa-microchip"></i> <span data-i18n="dashboard.cpuUsage">CPU 使用</span>
</span>
<div class="version-display-wrapper">
<span class="info-value" id="cpuUsage">--</span>
</div>
</div>
<div class="info-item">
<span class="info-label">
<i class="fas fa-cogs"></i> <span data-i18n="dashboard.serviceMode">运行模式</span>
</span>
<div class="version-display-wrapper">
<span class="info-value" id="serviceMode">--</span>
</div>
</div>
<div class="info-item">
<span class="info-label">
<i class="fas fa-microchip"></i> <span data-i18n="dashboard.processPid">进程 PID</span>
</span>
<div class="version-display-wrapper">
<span class="info-value" id="processPid">--</span>
</div>
</div>
</div>
</div>
<!-- Path Routing Examples Panel -->
<div class="routing-examples-panel">
<h3><i class="fas fa-route"></i> <span data-i18n="dashboard.routing.title">路径路由调用示例</span></h3>
<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>
<div class="routing-tips">
<h4><i class="fas fa-lightbulb"></i> <span data-i18n="dashboard.routing.tips">使用提示</span></h4>
<ul>
<li data-i18n="dashboard.routing.tip1"><strong>即时切换:</strong> 通过修改URL路径即可切换不同的AI模型提供商</li>
<li data-i18n="dashboard.routing.tip2"><strong>客户端配置:</strong> 在Cherry-Studio、NextChat、Cline等客户端中设置API端点为对应路径</li>
<li data-i18n="dashboard.routing.tip3"><strong>跨协议调用:</strong> 支持OpenAI协议调用Claude模型或Claude协议调用OpenAI模型</li>
</ul>
</div>
</div>
</section>
<!-- Configuration Section -->
<section id="config" class="section" aria-labelledby="config-title">
<h2 id="config-title" data-i18n="config.title">配置管理</h2>
<div class="config-panel">
<div class="config-form">
<div class="form-group password-input-group">
<label for="apiKey" data-i18n="config.apiKey">API密钥</label>
<div class="password-input-wrapper">
<input type="password" id="apiKey" class="form-control" data-i18n="config.apiKeyPlaceholder" placeholder="请输入API密钥" autocomplete="off">
<button type="button" class="password-toggle" data-target="apiKey" aria-label="显示/隐藏密码">
<i class="fas fa-eye" aria-hidden="true"></i>
</button>
</div>
</div>
<div class="form-row">
<div class="form-group">
<label for="host" data-i18n="config.host">监听地址</label>
<input type="text" id="host" class="form-control" value="127.0.0.1">
</div>
<div class="form-group">
<label for="port" data-i18n="config.port">端口</label>
<input type="number" id="port" class="form-control" value="3000">
</div>
</div>
<div class="form-group">
<label data-i18n="config.modelProvider">模型提供商 (可多选)</label>
<div id="modelProvider" class="provider-checklist">
<label class="checkbox-item">
<input type="checkbox" value="gemini-cli-oauth">
<span>Gemini CLI OAuth</span>
</label>
<label class="checkbox-item">
<input type="checkbox" value="gemini-antigravity">
<span>Gemini Antigravity</span>
</label>
<label class="checkbox-item">
<input type="checkbox" value="openai-custom">
<span>OpenAI Custom</span>
</label>
<label class="checkbox-item">
<input type="checkbox" value="claude-custom">
<span>Claude Custom</span>
</label>
<label class="checkbox-item">
<input type="checkbox" value="claude-kiro-oauth">
<span>Claude Kiro OAuth</span>
</label>
<label class="checkbox-item">
<input type="checkbox" value="openai-qwen-oauth">
<span>Qwen OAuth</span>
</label>
<label class="checkbox-item">
<input type="checkbox" value="openaiResponses-custom">
<span>OpenAI Responses</span>
</label>
</div>
<small class="form-text text-muted" data-i18n="config.modelProviderHelp">勾选启动时初始化的模型提供商 (必须至少勾选一个)</small>
</div>
<!-- 高级配置区域 -->
<div class="advanced-config-section">
<h3 data-i18n="config.advanced.title"><i class="fas fa-cogs"></i> 高级配置</h3>
<!-- 代理配置 -->
<div class="proxy-config-section">
<h4 data-i18n="config.proxy.title"><i class="fas fa-globe"></i> 代理设置</h4>
<div class="form-group">
<label for="proxyUrl" data-i18n="config.proxy.url">代理地址</label>
<input type="text" id="proxyUrl" class="form-control" data-i18n-placeholder="config.proxy.urlPlaceholder" placeholder="例如: http://127.0.0.1:7890 或 socks5://127.0.0.1:1080">
<small class="form-text" data-i18n="config.proxy.urlNote">支持 HTTP、HTTPS 和 SOCKS5 代理,留空则不使用代理</small>
</div>
<div class="form-group">
<label data-i18n="config.proxy.enabledProviders">启用代理的提供商</label>
<div id="proxyProviders" class="provider-checklist">
<label class="checkbox-item">
<input type="checkbox" name="proxyProvider" value="gemini-cli-oauth">
<span>Gemini CLI OAuth</span>
</label>
<label class="checkbox-item">
<input type="checkbox" name="proxyProvider" value="gemini-antigravity">
<span>Gemini Antigravity</span>
</label>
<label class="checkbox-item">
<input type="checkbox" name="proxyProvider" value="claude-kiro-oauth">
<span>Claude Kiro OAuth</span>
</label>
<label class="checkbox-item">
<input type="checkbox" name="proxyProvider" value="openai-qwen-oauth">
<span>Qwen OAuth</span>
</label>
<label class="checkbox-item">
<input type="checkbox" name="proxyProvider" value="openai-custom">
<span>OpenAI Custom</span>
</label>
<label class="checkbox-item">
<input type="checkbox" name="proxyProvider" value="claude-custom">
<span>Claude Custom</span>
</label>
</div>
<small class="form-text" data-i18n="config.proxy.enabledProvidersNote">选择需要通过代理访问的提供商,未选中的提供商将直接连接</small>
</div>
</div>
<div class="config-row">
<div class="form-group">
<label for="systemPromptFilePath" data-i18n="config.advanced.systemPromptFile">系统提示文件路径</label>
<input type="text" id="systemPromptFilePath" class="form-control" value="configs/input_system_prompt.txt" data-i18n-placeholder="config.advanced.systemPromptFilePlaceholder" placeholder="例如: configs/input_system_prompt.txt">
</div>
<div class="form-group">
<label for="systemPromptMode" data-i18n="config.advanced.systemPromptMode">系统提示模式</label>
<select id="systemPromptMode" class="form-control">
<option value="append" selected data-i18n="config.advanced.systemPromptMode.append">追加 (append)</option>
<option value="overwrite" data-i18n="config.advanced.systemPromptMode.overwrite">覆盖 (overwrite)</option>
</select>
</div>
</div>
<div class="config-row">
<div class="form-group">
<label for="promptLogBaseName" data-i18n="config.advanced.promptLogBaseName">提示日志基础名称</label>
<input type="text" id="promptLogBaseName" class="form-control" data-i18n-placeholder="config.advanced.promptLogBaseNamePlaceholder" placeholder="例如: prompt_log">
</div>
<div class="form-group">
<label for="promptLogMode" data-i18n="config.advanced.promptLogMode">提示日志模式</label>
<select id="promptLogMode" class="form-control">
<option value="none" data-i18n="config.advanced.promptLogMode.none">无 (none)</option>
<option value="console" data-i18n="config.advanced.promptLogMode.console">控制台 (console)</option>
<option value="file" data-i18n="config.advanced.promptLogMode.file">文件 (file)</option>
</select>
</div>
</div>
<div class="config-row">
<div class="form-group">
<label for="requestMaxRetries" data-i18n="config.advanced.maxRetries">最大重试次数</label>
<input type="number" id="requestMaxRetries" class="form-control" min="0" max="10" value="3">
</div>
<div class="form-group">
<label for="requestBaseDelay" data-i18n="config.advanced.baseDelay">重试基础延迟(毫秒)</label>
<input type="number" id="requestBaseDelay" class="form-control" min="0" step="100" value="1000">
</div>
</div>
<div class="config-row">
<div class="form-group">
<label for="cronNearMinutes" data-i18n="config.advanced.cronInterval">OAuth令牌刷新间隔(分钟)</label>
<input type="number" id="cronNearMinutes" class="form-control" min="1" max="60" value="1">
</div>
<div class="form-group">
<label for="cronNearMinutes" data-i18n="config.advanced.cronEnabled">启用OAuth令牌自动刷新(需重启服务)</label>
<label class="toggle-switch">
<input type="checkbox" id="cronRefreshToken">
<span class="toggle-slider"></span>
</label>
</div>
</div>
<div class="form-group pool-section">
<label for="providerPoolsFilePath" data-i18n="config.advanced.poolFilePath">提供商池配置文件路径(不能为空)</label>
<input type="text" id="providerPoolsFilePath" class="form-control" value="" data-i18n-placeholder="config.advanced.poolFilePathPlaceholder" placeholder="默认: configs/provider_pools.json">
<small class="form-text" data-i18n="config.advanced.poolNote">使用默认路径配置需添加一个空节点</small>
</div>
<div class="form-group pool-section">
<label for="maxErrorCount" data-i18n="config.advanced.maxErrorCount">提供商最大错误次数</label>
<input type="number" id="maxErrorCount" class="form-control" value="3" min="1" max="10" data-i18n-placeholder="config.advanced.maxErrorCountPlaceholder" placeholder="默认: 3">
<small class="form-text" data-i18n="config.advanced.maxErrorCountNote">提供商连续错误达到此次数后将被标记为不健康,默认为 3 次</small>
</div>
<div class="form-group pool-section">
<label for="providerFallbackChain" data-i18n="config.advanced.fallbackChain">跨类型 Fallback 链配置</label>
<textarea id="providerFallbackChain" class="form-control" rows="6" data-i18n-placeholder="config.advanced.fallbackChainPlaceholder" placeholder='例如:
{
"gemini-cli-oauth": ["gemini-antigravity"],
"gemini-antigravity": ["gemini-cli-oauth"],
"claude-kiro-oauth": ["claude-custom"]
}'></textarea>
<small class="form-text" data-i18n="config.advanced.fallbackChainNote">当某一 Provider Type 所有账号都不健康时,自动切换到配置的 Fallback 类型。JSON 格式,键为主类型,值为 Fallback 类型数组(按优先级排序)</small>
</div>
<div class="form-group pool-section">
<label for="modelFallbackMapping" data-i18n="config.advanced.modelFallbackMapping">跨协议模型 Fallback 映射</label>
<textarea id="modelFallbackMapping" class="form-control" rows="6" data-i18n-placeholder="config.advanced.modelFallbackMappingPlaceholder" placeholder='例如:
{
"gemini-claude-opus-4-5-thinking": {
"targetProviderType": "claude-kiro-oauth",
"targetModel": "claude-opus-4-5"
}
}'></textarea>
<small class="form-text" data-i18n="config.advanced.modelFallbackMappingNote">当主 Provider 不可用时,根据模型名映射到其他协议的 Provider 和模型。JSON 格式。</small>
</div>
<!-- 系统提示配置移到最下面 -->
<div class="form-group system-prompt-section">
<label for="systemPrompt" data-i18n="config.advanced.systemPrompt">系统提示</label>
<textarea id="systemPrompt" class="form-control" rows="4" data-i18n-placeholder="config.advanced.systemPromptPlaceholder" placeholder="输入系统提示..."></textarea>
</div>
<!-- 后台登录密码配置 -->
<div class="form-group pool-section">
<label for="adminPassword" data-i18n="config.advanced.adminPassword">后台登录密码</label>
<div class="password-input-wrapper">
<input type="password" id="adminPassword" class="form-control" data-i18n-placeholder="config.advanced.adminPasswordPlaceholder" placeholder="设置后台登录密码(留空则不修改)" autocomplete="new-password">
<button type="button" class="password-toggle" data-target="adminPassword" aria-label="显示/隐藏密码" data-i18n-aria-label="common.togglePassword">
<i class="fas fa-eye" aria-hidden="true"></i>
</button>
</div>
<small class="form-text" data-i18n="config.advanced.adminPasswordNote">用于保护管理控制台的访问,修改后需要重新登录</small>
</div>
</div>
<div class="form-actions">
<button class="btn btn-success" id="saveConfig">
<i class="fas fa-save"></i> <span data-i18n="config.save">保存配置</span>
</button>
<button class="btn btn-secondary" id="resetConfig">
<i class="fas fa-undo"></i> <span data-i18n="config.reset">重置</span>
</button>
</div>
</div>
</div>
</section>
<!-- Upload Configuration Section -->
<section id="upload-config" class="section" aria-labelledby="upload-config-title">
<h2 id="upload-config-title" data-i18n="upload.title">配置管理</h2>
<div class="upload-config-panel">
<!-- 搜索和过滤区域 -->
<div class="config-search-panel">
<div class="search-controls">
<div class="form-group">
<label for="configSearch" data-i18n="upload.search">搜索配置</label>
<div class="search-input-group">
<input type="text" id="configSearch" class="form-control" data-i18n-placeholder="upload.searchPlaceholder" placeholder="输入文件名">
<button type="button" class="btn btn-outline" id="searchConfigBtn" data-i18n-title="common.search" aria-label="搜索配置" data-i18n-aria-label="common.search">
<i class="fas fa-search"></i>
</button>
</div>
</div>
<div class="form-group">
<label for="configStatusFilter" data-i18n="upload.statusFilter">关联状态</label>
<select id="configStatusFilter" class="form-control">
<option value="" data-i18n="upload.statusFilter.all">全部状态</option>
<option value="used" data-i18n="upload.statusFilter.used">已关联</option>
<option value="unused" data-i18n="upload.statusFilter.unused">未关联</option>
</select>
</div>
<div class="form-group">
<label>&nbsp;</label>
<div class="config-actions">
<button class="btn btn-outline" id="refreshConfigList" aria-label="Refresh Config List" data-i18n-aria-label="upload.refresh">
<i class="fas fa-sync-alt"></i> <span data-i18n="upload.refresh">刷新</span>
</button>
<button class="btn btn-outline" id="downloadAllConfigs" aria-label="Download All Configs" data-i18n-aria-label="upload.downloadAll">
<i class="fas fa-file-archive"></i> <span data-i18n="upload.downloadAll">打包下载</span>
</button>
</div>
</div>
</div>
</div>
<!-- 配置列表 -->
<div class="config-list-container">
<div class="config-list-header">
<h3 data-i18n="upload.listTitle">配置文件列表</h3>
<div class="config-stats">
<span id="configCount" data-i18n="upload.count" data-i18n-params='{"count":"0"}'>共 0 个配置文件</span>
<span id="usedConfigCount" class="status-used" data-i18n="upload.usedCount" data-i18n-params='{"count":"0"}'>已关联: 0</span>
<span id="unusedConfigCount" class="status-unused" data-i18n="upload.unusedCount" data-i18n-params='{"count":"0"}'>未关联: 0</span>
<button id="batchLinkKiroBtn" class="btn-batch-link" data-i18n-title="upload.batchLink" title="批量关联 configs/ 下的未关联配置">
<i class="fas fa-link"></i> <span data-i18n="upload.batchLink">自动关联oauth</span>
</button>
</div>
</div>
<div id="configList" class="config-list">
<!-- 配置文件列表将在这里动态生成 -->
</div>
</div>
</div>
</section>
<!-- Provider Pools Section -->
<section id="providers" class="section" aria-labelledby="providers-title">
<h2 id="providers-title" data-i18n="providers.title">提供商池管理</h2>
<div class="pool-description">
<div class="highlight-note">
<i class="fas fa-info-circle"></i>
<span data-i18n="providers.note">使用默认路径配置需添加一个空节点</span>
</div>
</div>
<!-- Provider Pool Stats -->
<div class="stats-grid">
<div class="stat-card">
<div class="stat-icon">
<i class="fas fa-server"></i>
</div>
<div class="stat-info">
<h3 id="activeConnections">0</h3>
<p data-i18n="providers.activeConnections">活动连接</p>
</div>
</div>
<div class="stat-card">
<div class="stat-icon">
<i class="fas fa-network-wired"></i>
</div>
<div class="stat-info">
<h3 id="activeProviders">0</h3>
<p data-i18n="providers.activeProviders">活跃提供商</p>
</div>
</div>
<div class="stat-card">
<div class="stat-icon">
<i class="fas fa-check-circle"></i>
</div>
<div class="stat-info">
<h3 id="healthyProviders">0</h3>
<p data-i18n="providers.healthyProviders">健康提供商</p>
</div>
</div>
</div>
<div class="providers-container">
<div id="providersList" class="providers-list">
<!-- Providers will be loaded here -->
</div>
</div>
</section>
<!-- Usage Section -->
<section id="usage" class="section" aria-labelledby="usage-title">
<h2 id="usage-title" data-i18n="usage.title">用量查询</h2>
<div class="usage-panel">
<div class="usage-controls">
<button class="btn btn-primary" id="refreshUsageBtn" aria-label="Refresh Usage" data-i18n-aria-label="usage.refresh">
<i class="fas fa-sync-alt"></i> <span data-i18n="usage.refresh">刷新用量</span>
</button>
<span class="usage-last-update" id="usageLastUpdate" data-i18n="usage.lastUpdate" data-i18n-params='{"time":"--"}'>上次更新: --</span>
</div>
<div class="usage-loading" id="usageLoading" style="display: none;">
<i class="fas fa-spinner fa-spin"></i> <span data-i18n="usage.loading">正在加载用量数据...</span>
</div>
<div class="usage-error" id="usageError" style="display: none;">
<i class="fas fa-exclamation-triangle"></i>
<span id="usageErrorMessage"></span>
</div>
<div class="usage-content" id="usageContent">
<!-- 用量数据将在这里动态生成 -->
<div class="usage-empty" id="usageEmpty">
<i class="fas fa-chart-bar"></i>
<p data-i18n="usage.empty">点击"刷新用量"按钮获取授权文件用量信息</p>
</div>
</div>
</div>
</section>
<!-- Logs Section -->
<section id="logs" class="section" aria-labelledby="logs-title">
<h2 id="logs-title" data-i18n="logs.title">实时日志</h2>
<div class="logs-controls">
<button class="btn btn-danger" id="clearLogs" aria-label="Clear All Logs" data-i18n-aria-label="logs.clear">
<i class="fas fa-trash" aria-hidden="true"></i> <span data-i18n="logs.clear">清空日志</span>
</button>
<button class="btn btn-primary" id="toggleAutoScroll" data-enabled="true" aria-label="Toggle Auto-scroll" data-i18n-aria-label="logs.autoScroll">
<i class="fas fa-arrow-down" aria-hidden="true"></i> <span data-i18n="logs.autoScroll.on">自动滚动: 开</span>
</button>
</div>
<div class="logs-container" id="logsContainer" role="log" aria-live="polite" aria-atomic="false">
<!-- Logs will appear here -->
</div>
</section>
</main>
</div>
</div>
<!-- Toast Notifications -->
<div id="toastContainer" class="toast-container"></div>
<!-- Scripts -->
<script type="module" src="app/i18n.js"></script>
<script type="module" src="app/language-switcher.js"></script>
<script type="module" src="app/theme-switcher.js"></script>
<script type="module" src="app/auth.js"></script>
<script type="module">
// 导入多语言、主题切换和认证函数
import { initI18n, t } from './app/i18n.js';
import { initLanguageSwitcher } from './app/language-switcher.js';
import { initThemeSwitcher } from './app/theme-switcher.js';
import { initAuth, logout } from './app/auth.js';
// 初始化多语言
initI18n();
// 初始化主题切换器(尽早初始化以避免闪烁)
initThemeSwitcher();
// 页面加载时检查登录状态
(async function() {
const isAuthenticated = await initAuth();
if (!isAuthenticated) {
// 如果未认证initAuth会自动重定向到登录页面
return;
}
// 认证成功,继续加载页面
console.log('用户已认证');
// 初始化语言切换器
initLanguageSwitcher();
// 显示登出按钮(如果配置了密码保护)
const logoutBtn = document.getElementById('logoutBtn');
if (logoutBtn && localStorage.getItem('authToken')) {
logoutBtn.style.display = 'inline-block';
logoutBtn.addEventListener('click', async () => {
if (confirm(t('common.confirm') + ' ' + t('header.logout') + '?')) {
await logout();
}
});
}
// 更新路径路由示例中的URL前缀
updateRoutingExamplesURLs();
})();
// 更新路径路由示例中的URL前缀
function updateRoutingExamplesURLs() {
// 获取当前页面的基础URL去掉index.html
const currentURL = window.location.href;
const baseURL = currentURL.replace(/\/index\.html$/, '');
// 更新所有端点路径
const endpointPaths = document.querySelectorAll('.endpoint-path');
endpointPaths.forEach(element => {
const originalPath = element.textContent;
if (!originalPath.startsWith(baseURL)) {
// 确保baseURL不以斜杠结尾然后正确拼接路径
const cleanBaseURL = baseURL.replace(/\/$/, '');
const cleanPath = originalPath.startsWith('/') ? originalPath : '/' + originalPath;
element.textContent = cleanBaseURL + cleanPath;
}
});
// 更新curl命令中的URL
const curlCodes = document.querySelectorAll('.usage-example pre code');
curlCodes.forEach(element => {
const curlCommand = element.textContent;
// 替换curl命令中的http://localhost:3000部分
// 确保baseURL不以斜杠结尾然后正确拼接路径
const cleanBaseURL = baseURL.replace(/\/$/, '');
const updatedCommand = curlCommand.replace(/curl http:\/\/localhost:3000/g, `curl ${cleanBaseURL}`);
element.textContent = updatedCommand;
});
}
// 导出到 window 供其他脚本使用
window.initAuth = initAuth;
window.logout = logout;
</script>
<script type="module" src="app/app.js"></script>
</body>
</html>