diff --git a/package-lock.json b/package-lock.json
index 4c13297..80ddcb2 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -6,6 +6,7 @@
"": {
"dependencies": {
"@anthropic-ai/tokenizer": "^0.0.4",
+ "adm-zip": "^0.5.16",
"axios": "^1.10.0",
"deepmerge": "^4.3.1",
"dotenv": "^16.4.5",
@@ -2364,6 +2365,15 @@
"dev": true,
"license": "ISC"
},
+ "node_modules/adm-zip": {
+ "version": "0.5.16",
+ "resolved": "https://registry.npmmirror.com/adm-zip/-/adm-zip-0.5.16.tgz",
+ "integrity": "sha512-TGw5yVi4saajsSEgz25grObGHEUaDrniwvA2qwSC060KfqGPdglhvPMA2lPIoxs3PQIItj2iag35fONcQqgUaQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=12.0"
+ }
+ },
"node_modules/agent-base": {
"version": "7.1.4",
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz",
diff --git a/package.json b/package.json
index 57c009b..4fe12f9 100644
--- a/package.json
+++ b/package.json
@@ -2,6 +2,7 @@
"type": "module",
"dependencies": {
"@anthropic-ai/tokenizer": "^0.0.4",
+ "adm-zip": "^0.5.16",
"axios": "^1.10.0",
"deepmerge": "^4.3.1",
"dotenv": "^16.4.5",
diff --git a/src/ui-manager.js b/src/ui-manager.js
index 57336ca..87a98cd 100644
--- a/src/ui-manager.js
+++ b/src/ui-manager.js
@@ -3,6 +3,7 @@ import { promises as fs } from 'fs';
import path from 'path';
import multer from 'multer';
import crypto from 'crypto';
+import AdmZip from 'adm-zip';
import { getRequestBody } from './common.js';
import { getAllProviderModels, getProviderModels } from './provider-models.js';
import { CONFIG } from './config-manager.js';
@@ -1567,6 +1568,60 @@ export async function handleUIApiRequests(method, pathParam, req, res, currentCo
}
}
+ // Download all configs as zip
+ if (method === 'GET' && pathParam === '/api/upload-configs/download-all') {
+ try {
+ const configsPath = path.join(process.cwd(), 'configs');
+ if (!existsSync(configsPath)) {
+ res.writeHead(404, { 'Content-Type': 'application/json' });
+ res.end(JSON.stringify({ error: { message: 'configs目录不存在' } }));
+ return true;
+ }
+
+ const zip = new AdmZip();
+
+ // 递归添加目录函数
+ const addDirectoryToZip = async (dirPath, zipPath = '') => {
+ const items = await fs.readdir(dirPath, { withFileTypes: true });
+ for (const item of items) {
+ const fullPath = path.join(dirPath, item.name);
+ const itemZipPath = zipPath ? path.join(zipPath, item.name) : item.name;
+
+ if (item.isFile()) {
+ const content = await fs.readFile(fullPath);
+ zip.addFile(itemZipPath.replace(/\\/g, '/'), content);
+ } else if (item.isDirectory()) {
+ await addDirectoryToZip(fullPath, itemZipPath);
+ }
+ }
+ };
+
+ await addDirectoryToZip(configsPath);
+
+ const zipBuffer = zip.toBuffer();
+ const filename = `configs_backup_${new Date().toISOString().replace(/[:.]/g, '-')}.zip`;
+
+ res.writeHead(200, {
+ 'Content-Type': 'application/zip',
+ 'Content-Disposition': `attachment; filename="${filename}"`,
+ 'Content-Length': zipBuffer.length
+ });
+ res.end(zipBuffer);
+
+ console.log(`[UI API] All configs downloaded as zip: ${filename}`);
+ return true;
+ } catch (error) {
+ console.error('[UI API] Failed to download all configs:', error);
+ res.writeHead(500, { 'Content-Type': 'application/json' });
+ res.end(JSON.stringify({
+ error: {
+ message: '打包下载失败: ' + error.message
+ }
+ }));
+ return true;
+ }
+ }
+
// Quick link config to corresponding provider based on directory
if (method === 'POST' && pathParam === '/api/quick-link-provider') {
try {
diff --git a/static/app/I18N_GUIDE.md b/static/app/I18N_GUIDE.md
new file mode 100644
index 0000000..6595649
--- /dev/null
+++ b/static/app/I18N_GUIDE.md
@@ -0,0 +1,157 @@
+# 多语言支持使用指南
+
+## 概述
+
+本项目已实现中英文双语支持,可以通过页面右上角的语言切换按钮在简体中文和英文之间切换。
+
+## 文件结构
+
+```
+static/app/
+├── i18n.js # 多语言配置文件(包含所有翻译)
+├── language-switcher.js # 语言切换组件
+└── I18N_GUIDE.md # 本指南
+```
+
+## 如何使用
+
+### 1. 在 HTML 中添加多语言支持
+
+使用 `data-i18n` 属性标记需要翻译的元素:
+
+```html
+
+
AIClient2API 管理控制台
+
+
+
+
+
+
+
+
+共 10 个配置文件
+```
+
+### 2. 在 JavaScript 中使用翻译
+
+```javascript
+import { t } from './i18n.js';
+
+// 简单翻译
+const title = t('header.title');
+
+// 带参数的翻译
+const message = t('upload.count', { count: 10 });
+
+// 在 showToast 中使用
+showToast(t('common.success'), t('config.saved'), 'success');
+```
+
+### 3. 添加新的翻译
+
+在 `i18n.js` 文件中的 `translations` 对象中添加:
+
+```javascript
+const translations = {
+ 'zh-CN': {
+ 'your.key': '你的中文翻译',
+ // ...
+ },
+ 'en-US': {
+ 'your.key': 'Your English translation',
+ // ...
+ }
+};
+```
+
+### 4. 动态内容的翻译
+
+对于动态生成的内容,在创建 DOM 元素时添加 `data-i18n` 属性:
+
+```javascript
+const element = document.createElement('div');
+element.setAttribute('data-i18n', 'your.translation.key');
+element.textContent = t('your.translation.key');
+```
+
+## 翻译键命名规范
+
+使用点号分隔的层级结构:
+
+- `header.*` - 页头相关
+- `nav.*` - 导航相关
+- `dashboard.*` - 仪表盘相关
+- `config.*` - 配置相关
+- `providers.*` - 提供商相关
+- `upload.*` - 上传配置相关
+- `usage.*` - 用量查询相关
+- `logs.*` - 日志相关
+- `common.*` - 通用文本
+
+## 已实现的功能
+
+✅ 自动检测并保存用户语言偏好
+✅ 页面刷新后保持语言选择
+✅ 动态添加的元素自动翻译
+✅ 支持带参数的翻译
+✅ 语言切换时实时更新所有文本
+
+## 待完善的部分
+
+由于页面内容较多,以下部分需要继续添加 `data-i18n` 属性:
+
+1. 配置管理页面的表单标签和提示
+2. 提供商池管理的详细信息
+3. 上传配置管理的列表项
+4. 用量查询的统计信息
+5. 实时日志的控制按钮
+
+## 示例:完整的多语言表单
+
+```html
+
+
+
+
+```
+
+对应的翻译配置:
+
+```javascript
+'zh-CN': {
+ 'config.apiKey': 'API密钥',
+ 'config.apiKeyPlaceholder': '请输入API密钥'
+},
+'en-US': {
+ 'config.apiKey': 'API Key',
+ 'config.apiKeyPlaceholder': 'Please enter API key'
+}
+```
+
+## 注意事项
+
+1. 所有翻译键必须在两种语言中都存在
+2. 参数化翻译使用 `{paramName}` 格式
+3. HTML 内容使用 `data-i18n-html` 属性
+4. 语言切换会触发 `languageChanged` 事件
+5. 新添加的 DOM 元素会自动被翻译系统检测
+
+## 调试
+
+在浏览器控制台中:
+
+```javascript
+// 获取当前语言
+import { getCurrentLanguage } from './app/i18n.js';
+console.log(getCurrentLanguage());
+
+// 手动切换语言
+import { setLanguage } from './app/i18n.js';
+setLanguage('en-US');
\ No newline at end of file
diff --git a/static/app/app.js b/static/app/app.js
index 567b35a..68df82c 100644
--- a/static/app/app.js
+++ b/static/app/app.js
@@ -11,6 +11,8 @@ import {
getProviderStats
} from './utils.js';
+import { t } from './i18n.js';
+
import {
initFileUpload,
fileUploadHandler
@@ -106,7 +108,7 @@ function initApp() {
loadInitialData();
// 显示欢迎消息
- showToast('欢迎使用AIClent2API管理控制台!', 'success');
+ showToast(t('common.success'), t('common.welcome'), 'success');
// 每5秒更新服务器时间和运行时间显示
setInterval(() => {
diff --git a/static/app/config-manager.js b/static/app/config-manager.js
index 0e13cc9..55c4f69 100644
--- a/static/app/config-manager.js
+++ b/static/app/config-manager.js
@@ -3,6 +3,7 @@
import { showToast, formatUptime } from './utils.js';
import { handleProviderChange, handleGeminiCredsTypeChange, handleKiroCredsTypeChange } from './event-handlers.js';
import { loadProviders } from './provider-manager.js';
+import { t } from './i18n.js';
/**
* 加载配置
@@ -233,26 +234,26 @@ async function saveConfiguration() {
// 清空密码输入框
const adminPasswordEl = document.getElementById('adminPassword');
if (adminPasswordEl) adminPasswordEl.value = '';
- showToast('后台密码已更新,下次登录生效', 'success');
+ showToast(t('common.success'), t('common.passwordUpdated'), 'success');
} catch (pwdError) {
console.error('Failed to save admin password:', pwdError);
- showToast('保存后台密码失败: ' + pwdError.message, 'error');
+ showToast(t('common.error'), t('common.error') + ': ' + pwdError.message, 'error');
}
}
await window.apiClient.post('/reload-config');
- showToast('配置已保存', 'success');
+ showToast(t('common.success'), t('common.configSaved'), 'success');
// 检查当前是否在提供商池管理页面,如果是则刷新数据
const providersSection = document.getElementById('providers');
if (providersSection && providersSection.classList.contains('active')) {
// 当前在提供商池页面,刷新数据
await loadProviders();
- showToast('提供商池数据已刷新', 'success');
+ showToast(t('common.success'), t('common.providerPoolRefreshed'), 'success');
}
} catch (error) {
console.error('Failed to save configuration:', error);
- showToast('保存配置失败: ' + error.message, 'error');
+ showToast(t('common.error'), t('common.error') + ': ' + error.message, 'error');
}
}
diff --git a/static/app/event-handlers.js b/static/app/event-handlers.js
index b608e88..c49490d 100644
--- a/static/app/event-handlers.js
+++ b/static/app/event-handlers.js
@@ -3,6 +3,7 @@
import { elements, autoScroll, setAutoScroll, clearLogs } from './constants.js';
import { showToast } from './utils.js';
import { fileUploadHandler } from './file-upload.js';
+import { t } from './i18n.js';
/**
* 初始化所有事件监听器
@@ -20,7 +21,7 @@ function initEventListeners() {
if (elements.logsContainer) {
elements.logsContainer.innerHTML = '';
}
- showToast('日志已清空', 'success');
+ showToast(t('common.success'), t('common.refresh.success'), 'success');
});
}
@@ -30,9 +31,10 @@ function initEventListeners() {
const newAutoScroll = !autoScroll;
setAutoScroll(newAutoScroll);
elements.toggleAutoScrollBtn.dataset.enabled = newAutoScroll;
+ const statusText = newAutoScroll ? t('logs.autoScroll.on') : t('logs.autoScroll.off');
elements.toggleAutoScrollBtn.innerHTML = `
- 自动滚动: ${newAutoScroll ? '开' : '关'}
+ ${statusText}
`;
});
}
@@ -89,7 +91,7 @@ function initEventListeners() {
elements.toggleAutoScrollBtn.dataset.enabled = false;
elements.toggleAutoScrollBtn.innerHTML = `
- 自动滚动: 关
+ ${t('logs.autoScroll.off')}
`;
}
}
@@ -189,7 +191,7 @@ async function handleGenerateCreds(event) {
const targetInputId = button.getAttribute('data-target');
try {
- showToast('正在初始化凭据生成...', 'info');
+ showToast(t('common.info'), t('modal.provider.auth.initializing'), 'info');
// 使用 fileUploadHandler 中的 getProviderKey 获取目录名称
const providerDir = fileUploadHandler.getProviderKey(providerType);
@@ -211,7 +213,7 @@ async function handleGenerateCreds(event) {
if (input) {
input.value = data.relativePath;
input.dispatchEvent(new Event('input', { bubbles: true }));
- showToast('凭据已生成并自动填充路径', 'success');
+ showToast(t('common.success'), t('modal.provider.auth.success'), 'success');
}
window.removeEventListener('oauth_success_event', handleSuccess);
}
@@ -225,14 +227,14 @@ async function handleGenerateCreds(event) {
} else {
// 降级处理:如果在 app.js 中没导出,尝试直接打开
window.open(response.authUrl, '_blank');
- showToast('请在打开的窗口中完成授权', 'info');
+ showToast(t('common.info'), t('modal.provider.auth.window'), 'info');
}
} else {
- showToast('初始化凭据生成失败', 'error');
+ showToast(t('common.error'), t('modal.provider.auth.failed'), 'error');
}
} catch (error) {
console.error('生成凭据失败:', error);
- showToast(`生成凭据失败: ${error.message}`, 'error');
+ showToast(t('common.error'), t('modal.provider.auth.failed') + `: ${error.message}`, 'error');
}
}
@@ -305,7 +307,7 @@ async function handleRefresh() {
}
} catch (error) {
console.error('刷新失败:', error);
- showToast('刷新失败: ' + error.message, 'error');
+ showToast(t('common.error'), t('common.refresh.failed') + ': ' + error.message, 'error');
}
}
diff --git a/static/app/event-stream.js b/static/app/event-stream.js
index 11ea0f3..78c0bec 100644
--- a/static/app/event-stream.js
+++ b/static/app/event-stream.js
@@ -1,6 +1,7 @@
// Server-Sent Events处理模块
import { eventSource, setEventSource, elements, addLog, autoScroll } from './constants.js';
+import { t } from './i18n.js';
/**
* Server-Sent Events初始化
@@ -35,7 +36,7 @@ function initEventStream() {
newEventSource.addEventListener('oauth_success', (event) => {
const data = JSON.parse(event.data);
- showToast(`授权成功 (${data.provider})`, 'success');
+ showToast(t('common.success'), `${t('common.success')} (${data.provider})`, 'success');
// 发送自定义事件,以便其他模块(如生成凭据逻辑)可以接收到详细信息
window.dispatchEvent(new CustomEvent('oauth_success_event', { detail: data }));
@@ -102,11 +103,11 @@ function updateServerStatus(connected) {
if (connected) {
statusBadge.classList.remove('error');
icon.style.color = 'var(--success-color)';
- statusBadge.innerHTML = ' 已连接';
+ statusBadge.innerHTML = ` ${t('header.status.connected')}`;
} else {
statusBadge.classList.add('error');
icon.style.color = 'var(--danger-color)';
- statusBadge.innerHTML = ' 连接断开';
+ statusBadge.innerHTML = ` ${t('header.status.disconnected')}`;
}
}
diff --git a/static/app/file-upload.js b/static/app/file-upload.js
index 75d38e7..d440869 100644
--- a/static/app/file-upload.js
+++ b/static/app/file-upload.js
@@ -1,6 +1,7 @@
// 文件上传功能模块
import { showToast } from './utils.js';
+import { t } from './i18n.js';
/**
* 文件上传处理器类
@@ -113,14 +114,14 @@ class FileUploadHandler {
try {
// 验证文件类型
if (!this.validateFileType(file)) {
- showToast('不支持的文件类型,请选择 JSON、TXT、KEY、PEM、P12 或 PFX 文件', 'error');
+ showToast(t('common.error'), t('common.fileType'), 'error');
this.setButtonLoading(button, false);
return;
}
// 验证文件大小 (5MB 限制)
if (file.size > 5 * 1024 * 1024) {
- showToast('文件大小不能超过 5MB', 'error');
+ showToast(t('common.error'), t('common.fileSize'), 'error');
this.setButtonLoading(button, false);
return;
}
@@ -139,11 +140,11 @@ class FileUploadHandler {
// 成功上传,设置文件路径到输入框
this.setFilePathToInput(targetInputId, result.filePath);
- showToast('文件上传成功', 'success');
+ showToast(t('common.success'), t('common.uploadSuccess'), 'success');
} catch (error) {
console.error('文件上传错误:', error);
- showToast('文件上传失败: ' + error.message, 'error');
+ showToast(t('common.error'), t('common.uploadFailed') + ': ' + error.message, 'error');
} finally {
this.setButtonLoading(button, false);
}
diff --git a/static/app/i18n.js b/static/app/i18n.js
new file mode 100644
index 0000000..3b20c80
--- /dev/null
+++ b/static/app/i18n.js
@@ -0,0 +1,838 @@
+// 多语言配置
+const translations = {
+ 'zh-CN': {
+ // Header
+ 'header.title': 'AIClient2API 管理控制台',
+ 'header.status.connecting': '连接中...',
+ 'header.status.connected': '已连接',
+ 'header.status.disconnected': '连接断开',
+ 'header.logout': '登出',
+ 'header.refresh': '重载',
+
+ // Navigation
+ 'nav.main': '主导航',
+ 'nav.dashboard': '仪表盘',
+ 'nav.config': '配置管理',
+ 'nav.providers': '提供商池管理',
+ 'nav.upload': '上传配置管理',
+ 'nav.usage': '用量查询',
+ 'nav.logs': '实时日志',
+
+ // Dashboard
+ 'dashboard.title': '系统概览',
+ 'dashboard.uptime': '运行时间',
+ 'dashboard.systemInfo': '系统信息',
+ 'dashboard.nodeVersion': 'Node.js版本',
+ 'dashboard.serverTime': '服务器时间',
+ 'dashboard.memoryUsage': '内存使用',
+ 'dashboard.routing.title': '路径路由调用示例',
+ 'dashboard.routing.description': '通过不同路径路由访问不同的AI模型提供商,支持灵活的模型切换',
+ 'dashboard.routing.oauth': '突破限制',
+ 'dashboard.routing.official': '官方API/三方',
+ 'dashboard.routing.experimental': '突破限制/实验性',
+ 'dashboard.routing.free': '突破限制/免费使用',
+ 'dashboard.routing.endpoint': '端点路径:',
+ 'dashboard.routing.example': '使用示例',
+ 'dashboard.routing.exampleOpenAI': '使用示例 (OpenAI格式):',
+ 'dashboard.routing.exampleClaude': '使用示例 (Claude格式):',
+ 'dashboard.routing.openai': 'OpenAI协议',
+ 'dashboard.routing.claude': 'Claude协议',
+ 'dashboard.routing.tips': '使用提示',
+ 'dashboard.routing.tip1': '即时切换: 通过修改URL路径即可切换不同的AI模型提供商',
+ 'dashboard.routing.tip2': '客户端配置: 在Cherry-Studio、NextChat、Cline等客户端中设置API端点为对应路径',
+ 'dashboard.routing.tip3': '跨协议调用: 支持OpenAI协议调用Claude模型,或Claude协议调用OpenAI模型',
+ 'dashboard.routing.nodeName.gemini': 'Gemini CLI OAuth',
+ 'dashboard.routing.nodeName.antigravity': 'Gemini Antigravity',
+ 'dashboard.routing.nodeName.claude': 'Claude Custom',
+ 'dashboard.routing.nodeName.kiro': 'Claude Kiro OAuth',
+ 'dashboard.routing.nodeName.openai': 'OpenAI Custom',
+ 'dashboard.routing.nodeName.qwen': 'Qwen OAuth',
+ 'dashboard.contact.title': '联系与赞助',
+ 'dashboard.contact.wechat': '扫码进群,注明来意',
+ 'dashboard.contact.wechatDesc': '添加微信获取更多技术支持和交流',
+ 'dashboard.contact.sponsor': '扫码赞助',
+ 'dashboard.contact.sponsorDesc': '您的赞助是项目持续发展的动力',
+
+ // OAuth
+ 'oauth.modal.title': 'OAuth 授权',
+ 'oauth.modal.provider': '提供商:',
+ 'oauth.modal.steps': '授权步骤:',
+ 'oauth.modal.step1': '点击下方按钮在浏览器中打开授权页面',
+ 'oauth.modal.step2.qwen': '完成授权后,系统会自动获取凭据文件',
+ 'oauth.modal.step2.google': '使用您的Google账号登录并授权',
+ 'oauth.modal.step3': '凭据文件可在上传配置管理中查看和管理',
+ 'oauth.modal.step4.qwen': '授权有效期: {min} 分钟',
+ 'oauth.modal.step4.google': '授权完成后,凭据文件会自动保存',
+ 'oauth.modal.urlLabel': '授权链接:',
+ 'oauth.modal.copyTitle': '复制链接',
+ 'oauth.modal.openInBrowser': '在浏览器中打开',
+ 'oauth.manual.title': '自动监听受阻?',
+ 'oauth.manual.desc': '如果授权窗口重定向后显示“无法访问”,请将该窗口地址栏的 完整 URL 粘贴到下方:',
+ 'oauth.manual.placeholder': '粘贴回调 URL (包含 code=...)',
+ 'oauth.manual.submit': '提交',
+ 'oauth.success.msg': '授权链接已复制到剪贴板',
+ 'oauth.window.blocked': '授权窗口被浏览器拦截,请允许弹出窗口',
+ 'oauth.window.opened': '已打开授权窗口,请在窗口中完成操作',
+ 'oauth.processing': '正在完成授权...',
+ 'oauth.invalid.url': '该 URL 似乎不包含有效的授权代码',
+ 'oauth.error.format': '无效的 URL 格式',
+
+ // Config
+ 'config.title': '配置管理',
+ 'config.apiKey': 'API密钥',
+ 'config.apiKeyPlaceholder': '请输入API密钥',
+ 'config.host': '监听地址',
+ 'config.port': '端口',
+ 'config.modelProvider': '模型提供商',
+ 'config.optional': '(选填)',
+ 'config.gemini.baseUrl': 'Gemini Base URL',
+ 'config.gemini.baseUrlPlaceholder': 'https://cloudcode-pa.googleapis.com',
+ 'config.gemini.projectId': '项目ID',
+ 'config.gemini.projectIdPlaceholder': 'Google Cloud项目ID',
+ 'config.gemini.oauthCreds': 'OAuth凭据',
+ 'config.gemini.credsType.file': '文件路径',
+ 'config.gemini.credsType.base64': 'Base64编码',
+ 'config.gemini.credsBase64': 'OAuth凭据 (Base64)',
+ 'config.gemini.credsBase64Placeholder': '请输入Base64编码的OAuth凭据',
+ 'config.gemini.credsFilePath': 'OAuth凭据文件路径',
+ 'config.gemini.credsFilePathPlaceholder': '例如: ~/.gemini/oauth_creds.json',
+ 'config.antigravity.dailyUrl': 'Daily Base URL',
+ 'config.antigravity.dailyUrlPlaceholder': 'https://daily-cloudcode-pa.sandbox.googleapis.com',
+ 'config.antigravity.autopushUrl': 'Autopush Base URL',
+ 'config.antigravity.autopushUrlPlaceholder': 'https://autopush-cloudcode-pa.sandbox.googleapis.com',
+ 'config.antigravity.credsFilePath': 'OAuth凭据文件路径',
+ 'config.antigravity.credsFilePathPlaceholder': '例如: ~/.antigravity/oauth_creds.json',
+ 'config.antigravity.note': 'Antigravity 使用 Google OAuth 认证,需要提供凭据文件路径',
+ 'config.openai.apiKey': 'OpenAI API Key',
+ 'config.openai.apiKeyPlaceholder': 'sk-...',
+ 'config.openai.baseUrl': 'OpenAI Base URL',
+ 'config.openai.baseUrlPlaceholder': '例如: https://api.openai.com/v1',
+ 'config.claude.apiKey': 'Claude API Key',
+ 'config.claude.apiKeyPlaceholder': 'sk-ant-...',
+ 'config.claude.baseUrl': 'Claude Base URL',
+ 'config.claude.baseUrlPlaceholder': '例如: https://api.anthropic.com',
+ 'config.kiro.baseUrl': 'Base URL',
+ 'config.kiro.baseUrlPlaceholder': 'https://codewhisperer.{{region}}.amazonaws.com/generateAssistantResponse',
+ 'config.kiro.refreshUrl': 'Refresh URL',
+ 'config.kiro.refreshUrlPlaceholder': 'https://prod.{{region}}.auth.desktop.kiro.dev/refreshToken',
+ 'config.kiro.refreshIdcUrl': 'Refresh IDC URL',
+ 'config.kiro.refreshIdcUrlPlaceholder': 'https://oidc.{{region}}.amazonaws.com/token',
+ 'config.kiro.credsFilePath': 'OAuth凭据文件路径',
+ 'config.kiro.credsFilePathPlaceholder': '例如: ~/.aws/sso/cache/kiro-auth-token.json',
+ 'config.kiro.note': '使用 AWS 登录方式时,请确保授权文件中包含 clientId 和 clientSecret 字段',
+ 'config.qwen.baseUrl': 'Qwen Base URL',
+ 'config.qwen.baseUrlPlaceholder': 'https://portal.qwen.ai/v1',
+ 'config.qwen.oauthBaseUrl': 'OAuth Base URL',
+ 'config.qwen.oauthBaseUrlPlaceholder': 'https://chat.qwen.ai',
+ 'config.qwen.credsFilePath': 'OAuth凭据文件路径',
+ 'config.qwen.credsFilePathPlaceholder': '例如: ~/.qwen/oauth_creds.json',
+ 'config.advanced.title': '高级配置',
+ 'config.advanced.systemPromptFile': '系统提示文件路径',
+ 'config.advanced.systemPromptFilePlaceholder': '例如: input_system_prompt.txt',
+ 'config.advanced.systemPromptMode': '系统提示模式',
+ 'config.advanced.systemPromptMode.append': '追加 (append)',
+ 'config.advanced.systemPromptMode.overwrite': '覆盖 (overwrite)',
+ 'config.advanced.promptLogBaseName': '提示日志基础名称',
+ 'config.advanced.promptLogBaseNamePlaceholder': '例如: prompt_log',
+ 'config.advanced.promptLogMode': '提示日志模式',
+ 'config.advanced.promptLogMode.none': '无 (none)',
+ 'config.advanced.promptLogMode.console': '控制台 (console)',
+ 'config.advanced.promptLogMode.file': '文件 (file)',
+ 'config.advanced.maxRetries': '最大重试次数',
+ 'config.advanced.baseDelay': '重试基础延迟(毫秒)',
+ 'config.advanced.cronInterval': 'OAuth令牌刷新间隔(分钟)',
+ 'config.advanced.cronEnabled': '启用OAuth令牌自动刷新(需重启服务)',
+ 'config.advanced.poolFilePath': '提供商池配置文件路径(不能为空)',
+ 'config.advanced.poolFilePathPlaceholder': '默认: provider_pools.json',
+ 'config.advanced.poolNote': '配置了提供商池后,默认使用提供商池的配置,提供商池配置失效降级到默认配置',
+ 'config.advanced.maxErrorCount': '提供商最大错误次数',
+ 'config.advanced.maxErrorCountPlaceholder': '默认: 3',
+ 'config.advanced.maxErrorCountNote': '提供商连续错误达到此次数后将被标记为不健康,默认为 3 次',
+ 'config.advanced.systemPrompt': '系统提示',
+ 'config.advanced.systemPromptPlaceholder': '输入系统提示...',
+ 'config.advanced.adminPassword': '后台登录密码',
+ 'config.advanced.adminPasswordPlaceholder': '设置后台登录密码(留空则不修改)',
+ 'config.advanced.adminPasswordNote': '用于保护管理控制台的访问,修改后需要重新登录',
+ 'config.save': '保存配置',
+ 'config.reset': '重置',
+ 'config.placeholder.nodeName': '例如: 我的节点1',
+ 'config.placeholder.model': '例如: gpt-3.5-turbo',
+
+ // Upload Config
+ 'upload.title': '上传配置管理',
+ 'upload.search': '搜索配置',
+ 'upload.searchPlaceholder': '输入文件名',
+ 'upload.statusFilter': '关联状态',
+ 'upload.statusFilter.all': '全部状态',
+ 'upload.statusFilter.used': '已关联',
+ 'upload.statusFilter.unused': '未关联',
+ 'upload.refresh': '刷新',
+ 'upload.downloadAll': '打包下载',
+ 'upload.listTitle': '配置文件列表',
+ 'upload.count': '共 {count} 个配置文件',
+ 'upload.usedCount': '已关联: {count}',
+ 'upload.unusedCount': '未关联: {count}',
+ 'upload.batchLink': '自动关联oauth',
+ 'upload.noConfigs': '未找到匹配的配置文件',
+ 'upload.detail.path': '文件路径',
+ 'upload.detail.size': '文件大小',
+ 'upload.detail.modified': '最后修改',
+ 'upload.detail.status': '关联状态',
+ 'upload.action.view': '查看',
+ 'upload.action.delete': '删除',
+ 'upload.usage.title': '关联详情 ({type})',
+ 'upload.usage.mainConfig': '主要配置',
+ 'upload.usage.providerPool': '提供商池',
+ 'upload.usage.multiple': '多种用途',
+ 'upload.delete.confirmTitle': '删除配置文件',
+ 'upload.delete.confirmTitleUsed': '删除已关联配置',
+ 'upload.delete.warningUsedTitle': '⚠️ 此配置已被系统使用',
+ 'upload.delete.warningUsedDesc': '删除已关联的配置文件可能会影响系统正常运行。请确保您了解删除的后果。',
+ 'upload.delete.warningUnusedTitle': '🗑️ 确认删除配置文件',
+ 'upload.delete.warningUnusedDesc': '此操作将永久删除配置文件,且无法撤销。',
+ 'upload.delete.fileName': '文件名:',
+ 'upload.delete.usageAlertTitle': '关联详情',
+ 'upload.delete.usageAlertDesc': '此配置文件正在被系统使用,删除后可能会导致:',
+ 'upload.delete.usageAlertItem1': '相关的AI服务无法正常工作',
+ 'upload.delete.usageAlertItem2': '配置管理中的设置失效',
+ 'upload.delete.usageAlertItem3': '提供商池配置丢失',
+ 'upload.delete.usageAlertAdvice': '建议:请先在配置管理中解除文件引用后再删除。',
+ 'upload.delete.forceDelete': '强制删除',
+ 'upload.delete.confirmDelete': '确认删除',
+ 'upload.batchLink.confirm': '确定要批量关联 {count} 个配置吗?\n\n{summary}',
+ 'upload.refresh.success': '刷新成功',
+ 'upload.action.view.failed': '查看失败',
+ 'upload.action.delete.failed': '删除失败',
+ 'upload.config.notExist': '配置文件不存在',
+ 'upload.link.identifying': '正在识别提供商类型...',
+ 'upload.link.failed.identify': '无法识别配置文件对应的提供商类型',
+ 'upload.link.processing': '正在关联配置到 {name}...',
+ 'upload.link.success': '配置关联成功',
+ 'upload.link.failed': '关联失败',
+ 'upload.batchLink.none': '没有需要关联的配置文件',
+ 'upload.batchLink.processing': '正在批量关联 {count} 个配置...',
+ 'upload.batchLink.success': '成功关联 {count} 个配置',
+ 'upload.batchLink.partial': '关联完成: 成功 {success} 个, 失败 {fail} 个',
+
+ // Providers
+ 'providers.title': '提供商池管理',
+ 'providers.note': '配置了提供商池后,默认使用提供商池的配置,提供商池配置失效降级到默认配置',
+ 'providers.activeConnections': '活动连接',
+ 'providers.activeProviders': '活跃提供商',
+ 'providers.healthyProviders': '健康提供商',
+ 'providers.status.healthy': '{healthy}/{total} 健康',
+ 'providers.status.empty': '0/0 节点',
+ 'providers.stat.totalAccounts': '总账户',
+ 'providers.stat.healthyAccounts': '健康账户',
+ 'providers.stat.usageCount': '使用次数',
+ 'providers.stat.errorCount': '错误次数',
+ 'providers.auth.generate': '生成授权',
+
+ // Modal Provider Manager
+ 'modal.provider.manage': '管理 {type} 提供商配置',
+ 'modal.provider.totalAccounts': '总账户数:',
+ 'modal.provider.healthyAccounts': '健康账户:',
+ 'modal.provider.add': '添加新提供商',
+ 'modal.provider.resetHealth': '重置为健康',
+ 'modal.provider.healthCheck': '健康检测',
+ 'modal.provider.resetHealthConfirm': '确定要将 {type} 的所有节点重置为健康状态吗?\n\n这将清除所有节点的错误计数和错误时间。',
+ 'modal.provider.healthCheckConfirm': '确定要对 {type} 的所有节点执行健康检测吗?\n\n这将向每个节点发送测试请求来验证其可用性。',
+ 'modal.provider.deleteConfirm': '确定要删除这个提供商配置吗?此操作不可恢复。',
+ 'modal.provider.disableConfirm': '确定要禁用这个提供商配置吗?禁用后该提供商将不会被选中使用。',
+ 'modal.provider.enableConfirm': '确定要启用这个提供商配置吗?',
+ 'modal.provider.edit': '编辑',
+ 'modal.provider.delete': '删除',
+ 'modal.provider.save': '保存',
+ 'modal.provider.cancel': '取消',
+ 'modal.provider.status.healthy': '正常',
+ 'modal.provider.status.unhealthy': '异常',
+ 'modal.provider.status.disabled': '已禁用',
+ 'modal.provider.status.enabled': '已启用',
+ 'modal.provider.lastError': '最后错误:',
+ 'modal.provider.lastUsed': '最后使用:',
+ 'modal.provider.lastCheck': '最后检测:',
+ 'modal.provider.checkModel': '检测模型:',
+ 'modal.provider.usageCount': '使用次数:',
+ 'modal.provider.errorCount': '失败次数:',
+ 'modal.provider.neverUsed': '从未使用',
+ 'modal.provider.neverChecked': '从未检测',
+ 'modal.provider.noModels': '该提供商类型暂无可用模型列表',
+ 'modal.provider.loadingModels': '加载模型列表...',
+ 'modal.provider.unsupportedModels': '不支持的模型',
+ 'modal.provider.unsupportedModelsHelp': '选择此提供商不支持的模型,系统会自动排除这些模型',
+ 'modal.provider.addTitle': '添加新提供商配置',
+ 'modal.provider.customName': '自定义名称',
+ 'modal.provider.checkModelName': '检查模型名称',
+ 'modal.provider.healthCheckLabel': '健康检查',
+ 'modal.provider.enabled': '启用',
+ 'modal.provider.disabled': '禁用',
+ 'modal.provider.noProviderType': '不支持的提供商类型',
+
+ 'modal.provider.load.failed': '加载提供商详情失败',
+ 'modal.provider.auth.initializing': '正在初始化凭据生成...',
+ 'modal.provider.auth.success': '凭据已生成并自动填充路径',
+ 'modal.provider.auth.window': '请在打开的窗口中完成授权',
+ 'modal.provider.auth.failed': '初始化凭据生成失败',
+ 'modal.provider.save.success': '保存成功',
+ 'modal.provider.save.failed': '保存失败',
+ 'modal.provider.delete.success': '删除成功',
+ 'modal.provider.delete.failed': '删除失败',
+ 'modal.provider.add.success': '添加成功',
+ 'modal.provider.add.failed': '添加失败',
+ 'modal.provider.resetHealth.success': '成功重置 {count} 个节点的健康状态',
+ 'modal.provider.resetHealth.failed': '重置健康状态失败',
+
+ // Pagination
+ 'pagination.showing': '显示 {start}-{end} / 共 {total} 条',
+ 'pagination.jumpTo': '跳转到',
+ 'pagination.page': '页',
+
+ // Usage
+ 'usage.title': '用量查询',
+ 'usage.refresh': '刷新用量',
+ 'usage.lastUpdate': '上次更新: {time}',
+ 'usage.lastUpdateCache': '缓存时间: {time}',
+ 'usage.loading': '正在加载用量数据...',
+ 'usage.empty': '点击"刷新用量"按钮获取授权文件用量信息',
+ 'usage.noData': '暂无用量数据',
+ 'usage.noInstances': '暂无已初始化的服务实例',
+ 'usage.group.instances': '{count} 个实例',
+ 'usage.group.success': '{count}/{total} 成功',
+ 'usage.card.status.disabled': '已禁用',
+ 'usage.card.status.healthy': '健康',
+ 'usage.card.status.unhealthy': '异常',
+ 'usage.card.totalUsage': '总用量',
+ 'usage.card.freeTrial': '免费试用',
+ 'usage.card.bonus': '奖励',
+ 'usage.card.expires': '到期: {time}',
+
+ // Logs
+ 'logs.title': '实时日志',
+ 'logs.clear': '清空日志',
+ 'logs.autoScroll': '自动滚动',
+ 'logs.autoScroll.on': '自动滚动: 开',
+ 'logs.autoScroll.off': '自动滚动: 关',
+
+ // Common
+ 'common.confirm': '确定',
+ 'common.cancel': '取消',
+ 'common.success': '成功',
+ 'common.error': '错误',
+ 'common.warning': '警告',
+ 'common.info': '信息',
+ 'common.loading': '加载中...',
+ 'common.upload': '上传',
+ 'common.generate': '生成',
+ 'common.search': '搜索',
+ 'common.welcome': '欢迎使用AIClient2API管理控制台!',
+ 'common.fileType': '不支持的文件类型,请选择 JSON、TXT、KEY、PEM、P12 或 PFX 文件',
+ 'common.fileSize': '文件大小不能超过 5MB',
+ 'common.uploadSuccess': '文件上传成功',
+ 'common.uploadFailed': '文件上传失败',
+ 'common.passwordUpdated': '后台密码已更新,下次登录生效',
+ 'common.configSaved': '配置已保存',
+ 'common.providerPoolRefreshed': '提供商池数据已刷新',
+ 'common.togglePassword': '显示/隐藏密码',
+ 'common.copy.success': '内容已复制到剪贴板',
+ 'common.copy.failed': '复制失败,请手动复制',
+ 'common.refresh.success': '刷新成功',
+ 'common.refresh.failed': '刷新失败',
+
+ // Login
+ 'login.title': '登录 - AIClient2API',
+ 'login.heading': '请登录以继续',
+ 'login.password': '密码',
+ 'login.passwordPlaceholder': '请输入密码',
+ 'login.error.empty': '请输入密码',
+ 'login.error.incorrect': '密码错误,请重试',
+ 'login.error.failed': '登录失败,请检查网络连接',
+ 'login.button': '登录',
+ 'login.loggingIn': '登录中...',
+ },
+ 'en-US': {
+ // Header
+ 'header.title': 'AIClient2API Management Console',
+ 'header.status.connecting': 'Connecting...',
+ 'header.status.connected': 'Connected',
+ 'header.status.disconnected': 'Disconnected',
+ 'header.logout': 'Logout',
+ 'header.refresh': 'Reload',
+
+ // Navigation
+ 'nav.main': 'Main Navigation',
+ 'nav.dashboard': 'Dashboard',
+ 'nav.config': 'Configuration',
+ 'nav.providers': 'Provider Pools',
+ 'nav.upload': 'Upload Config',
+ 'nav.usage': 'Usage Query',
+ 'nav.logs': 'Real-time Logs',
+
+ // Dashboard
+ 'dashboard.title': 'System Overview',
+ 'dashboard.uptime': 'Uptime',
+ 'dashboard.systemInfo': 'System Information',
+ 'dashboard.nodeVersion': 'Node.js Version',
+ 'dashboard.serverTime': 'Server Time',
+ 'dashboard.memoryUsage': 'Memory Usage',
+ 'dashboard.routing.title': 'Path Routing Examples',
+ 'dashboard.routing.description': 'Access different AI model providers through different path routes, supporting flexible model switching',
+ 'dashboard.routing.oauth': 'Limit Breakthrough',
+ 'dashboard.routing.official': 'Official/Third-party API',
+ 'dashboard.routing.experimental': 'Limit Breakthrough/Experimental',
+ 'dashboard.routing.free': 'Limit Breakthrough/Free',
+ 'dashboard.routing.endpoint': 'Endpoint Path:',
+ 'dashboard.routing.example': 'Usage Example',
+ 'dashboard.routing.exampleOpenAI': 'Usage Example (OpenAI):',
+ 'dashboard.routing.exampleClaude': 'Usage Example (Claude):',
+ 'dashboard.routing.openai': 'OpenAI Protocol',
+ 'dashboard.routing.claude': 'Claude Protocol',
+ 'dashboard.routing.tips': 'Usage Tips',
+ 'dashboard.routing.tip1': 'Instant Switch: Switch between different AI model providers by modifying the URL path',
+ 'dashboard.routing.tip2': 'Client Configuration: Set API endpoint to corresponding path in clients like Cherry-Studio, NextChat, Cline',
+ 'dashboard.routing.tip3': 'Cross-protocol Calls: Support calling Claude models with OpenAI protocol, or OpenAI models with Claude protocol',
+ 'dashboard.routing.nodeName.gemini': 'Gemini CLI OAuth',
+ 'dashboard.routing.nodeName.antigravity': 'Gemini Antigravity',
+ 'dashboard.routing.nodeName.claude': 'Claude Custom',
+ 'dashboard.routing.nodeName.kiro': 'Claude Kiro OAuth',
+ 'dashboard.routing.nodeName.openai': 'OpenAI Custom',
+ 'dashboard.routing.nodeName.qwen': 'Qwen OAuth',
+ 'dashboard.contact.title': 'Contact & Sponsor',
+ 'dashboard.contact.wechat': 'Scan to Join Group',
+ 'dashboard.contact.wechatDesc': 'Add WeChat for more technical support and communication',
+ 'dashboard.contact.sponsor': 'Scan to Sponsor',
+ 'dashboard.contact.sponsorDesc': 'Your sponsorship is the driving force for the project\'s continuous development',
+
+ // OAuth
+ 'oauth.modal.title': 'OAuth Authorization',
+ 'oauth.modal.provider': 'Provider:',
+ 'oauth.modal.steps': 'Authorization Steps:',
+ 'oauth.modal.step1': 'Click the button below to open the authorization page in your browser',
+ 'oauth.modal.step2.qwen': 'After authorization, the system will automatically fetch the credentials file',
+ 'oauth.modal.step2.google': 'Log in with your Google account and authorize',
+ 'oauth.modal.step3': 'Credentials files can be viewed and managed in Upload Config',
+ '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.openInBrowser': 'Open in Browser',
+ 'oauth.manual.title': 'Auto-listener blocked?',
+ 'oauth.manual.desc': 'If the auth window shows "Cannot access" after redirect, please paste the Full URL from that window\'s address bar below:',
+ 'oauth.manual.placeholder': 'Paste callback URL (contains code=...)',
+ 'oauth.manual.submit': 'Submit',
+ 'oauth.success.msg': 'Authorization link copied to clipboard',
+ 'oauth.window.blocked': 'Auth window was blocked by the browser, please allow pop-ups',
+ 'oauth.window.opened': 'Auth window opened, please complete the process there',
+ 'oauth.processing': 'Completing authorization...',
+ 'oauth.invalid.url': 'This URL does not seem to contain a valid auth code',
+ 'oauth.error.format': 'Invalid URL format',
+
+ // Config
+ 'config.title': 'Configuration Management',
+ 'config.apiKey': 'API Key',
+ 'config.apiKeyPlaceholder': 'Please enter API key',
+ 'config.host': 'Listen Address',
+ 'config.port': 'Port',
+ 'config.modelProvider': 'Model Provider',
+ 'config.optional': '(Optional)',
+ 'config.gemini.baseUrl': 'Gemini Base URL',
+ 'config.gemini.baseUrlPlaceholder': 'https://cloudcode-pa.googleapis.com',
+ 'config.gemini.projectId': 'Project ID',
+ 'config.gemini.projectIdPlaceholder': 'Google Cloud Project ID',
+ 'config.gemini.oauthCreds': 'OAuth Credentials',
+ 'config.gemini.credsType.file': 'File Path',
+ 'config.gemini.credsType.base64': 'Base64 Encoded',
+ 'config.gemini.credsBase64': 'OAuth Credentials (Base64)',
+ 'config.gemini.credsBase64Placeholder': 'Please enter Base64 encoded OAuth credentials',
+ 'config.gemini.credsFilePath': 'OAuth Credentials File Path',
+ 'config.gemini.credsFilePathPlaceholder': 'e.g.: ~/.gemini/oauth_creds.json',
+ 'config.antigravity.dailyUrl': 'Daily Base URL',
+ 'config.antigravity.dailyUrlPlaceholder': 'https://daily-cloudcode-pa.sandbox.googleapis.com',
+ 'config.antigravity.autopushUrl': 'Autopush Base URL',
+ 'config.antigravity.autopushUrlPlaceholder': 'https://autopush-cloudcode-pa.sandbox.googleapis.com',
+ 'config.antigravity.credsFilePath': 'OAuth Credentials File Path',
+ 'config.antigravity.credsFilePathPlaceholder': 'e.g.: ~/.antigravity/oauth_creds.json',
+ 'config.antigravity.note': 'Antigravity uses Google OAuth authentication, requires credentials file path',
+ 'config.openai.apiKey': 'OpenAI API Key',
+ 'config.openai.apiKeyPlaceholder': 'sk-...',
+ 'config.openai.baseUrl': 'OpenAI Base URL',
+ 'config.openai.baseUrlPlaceholder': 'e.g.: https://api.openai.com/v1',
+ 'config.claude.apiKey': 'Claude API Key',
+ 'config.claude.apiKeyPlaceholder': 'sk-ant-...',
+ 'config.claude.baseUrl': 'Claude Base URL',
+ 'config.claude.baseUrlPlaceholder': 'e.g.: https://api.anthropic.com',
+ 'config.kiro.baseUrl': 'Base URL',
+ 'config.kiro.baseUrlPlaceholder': 'https://codewhisperer.{{region}}.amazonaws.com/generateAssistantResponse',
+ 'config.kiro.refreshUrl': 'Refresh URL',
+ 'config.kiro.refreshUrlPlaceholder': 'https://prod.{{region}}.auth.desktop.kiro.dev/refreshToken',
+ 'config.kiro.refreshIdcUrl': 'Refresh IDC URL',
+ 'config.kiro.refreshIdcUrlPlaceholder': 'https://oidc.{{region}}.amazonaws.com/token',
+ 'config.kiro.credsFilePath': 'OAuth Credentials File Path',
+ 'config.kiro.credsFilePathPlaceholder': 'e.g.: ~/.aws/sso/cache/kiro-auth-token.json',
+ 'config.kiro.note': 'When using AWS login method, ensure the authorization file contains clientId and clientSecret fields',
+ 'config.qwen.baseUrl': 'Qwen Base URL',
+ 'config.qwen.baseUrlPlaceholder': 'https://portal.qwen.ai/v1',
+ 'config.qwen.oauthBaseUrl': 'OAuth Base URL',
+ 'config.qwen.oauthBaseUrlPlaceholder': 'https://chat.qwen.ai',
+ 'config.qwen.credsFilePath': 'OAuth Credentials File Path',
+ 'config.qwen.credsFilePathPlaceholder': 'e.g.: ~/.qwen/oauth_creds.json',
+ 'config.advanced.title': 'Advanced Configuration',
+ 'config.advanced.systemPromptFile': 'System Prompt File Path',
+ 'config.advanced.systemPromptFilePlaceholder': 'e.g.: input_system_prompt.txt',
+ 'config.advanced.systemPromptMode': 'System Prompt Mode',
+ 'config.advanced.systemPromptMode.append': 'Append',
+ 'config.advanced.systemPromptMode.overwrite': 'Overwrite',
+ 'config.advanced.promptLogBaseName': 'Prompt Log Base Name',
+ 'config.advanced.promptLogBaseNamePlaceholder': 'e.g.: prompt_log',
+ 'config.advanced.promptLogMode': 'Prompt Log Mode',
+ 'config.advanced.promptLogMode.none': 'None',
+ 'config.advanced.promptLogMode.console': 'Console',
+ 'config.advanced.promptLogMode.file': 'File',
+ 'config.advanced.maxRetries': 'Max Retries',
+ 'config.advanced.baseDelay': 'Base Retry Delay (ms)',
+ 'config.advanced.cronInterval': 'OAuth Token Refresh Interval (minutes)',
+ 'config.advanced.cronEnabled': 'Enable OAuth Token Auto Refresh (requires restart)',
+ 'config.advanced.poolFilePath': 'Provider Pool Config File Path (required)',
+ 'config.advanced.poolFilePathPlaceholder': 'Default: provider_pools.json',
+ 'config.advanced.poolNote': 'When provider pool is configured, it will be used by default. Falls back to default config if pool config fails',
+ 'config.advanced.maxErrorCount': 'Provider Max Error Count',
+ 'config.advanced.maxErrorCountPlaceholder': 'Default: 3',
+ 'config.advanced.maxErrorCountNote': 'Provider will be marked as unhealthy after consecutive errors reach this count, default is 3',
+ 'config.advanced.systemPrompt': 'System Prompt',
+ 'config.advanced.systemPromptPlaceholder': 'Enter system prompt...',
+ 'config.advanced.adminPassword': 'Admin Password',
+ 'config.advanced.adminPasswordPlaceholder': 'Set admin password (leave empty to keep unchanged)',
+ 'config.advanced.adminPasswordNote': 'Used to protect management console access, requires re-login after modification',
+ 'config.save': 'Save Configuration',
+ 'config.reset': 'Reset',
+ 'config.placeholder.nodeName': 'e.g.: My Node 1',
+ 'config.placeholder.model': 'e.g.: gpt-3.5-turbo',
+
+ // Upload Config
+ 'upload.title': 'Upload Configuration Management',
+ 'upload.search': 'Search Config',
+ 'upload.searchPlaceholder': 'Enter filename',
+ 'upload.statusFilter': 'Association Status',
+ 'upload.statusFilter.all': 'All Status',
+ 'upload.statusFilter.used': 'Associated',
+ 'upload.statusFilter.unused': 'Not Associated',
+ 'upload.refresh': 'Refresh',
+ 'upload.downloadAll': 'Download All (ZIP)',
+ 'upload.listTitle': 'Configuration File List',
+ 'upload.count': 'Total {count} config files',
+ 'upload.usedCount': 'Associated: {count}',
+ 'upload.unusedCount': 'Not Associated: {count}',
+ 'upload.batchLink': 'Auto Link OAuth',
+ 'upload.noConfigs': 'No matching configuration files found',
+ 'upload.detail.path': 'File Path',
+ 'upload.detail.size': 'File Size',
+ 'upload.detail.modified': 'Last Modified',
+ 'upload.detail.status': 'Status',
+ 'upload.action.view': 'View',
+ 'upload.action.delete': 'Delete',
+ 'upload.usage.title': 'Association Details ({type})',
+ 'upload.usage.mainConfig': 'Main Config',
+ 'upload.usage.providerPool': 'Provider Pool',
+ 'upload.usage.multiple': 'Multiple Purposes',
+ 'upload.delete.confirmTitle': 'Delete Config File',
+ 'upload.delete.confirmTitleUsed': 'Delete Associated Config',
+ 'upload.delete.warningUsedTitle': '⚠️ This config is currently in use',
+ 'upload.delete.warningUsedDesc': 'Deleting an associated config file may affect system stability. Please ensure you understand the consequences.',
+ 'upload.delete.warningUnusedTitle': '🗑️ Confirm deletion',
+ 'upload.delete.warningUnusedDesc': 'This operation will permanently delete the config file and cannot be undone.',
+ 'upload.delete.fileName': 'File Name:',
+ 'upload.delete.usageAlertTitle': 'Association Details',
+ 'upload.delete.usageAlertDesc': 'This file is being used by the system. Deletion may cause:',
+ 'upload.delete.usageAlertItem1': 'Related AI services to stop working',
+ 'upload.delete.usageAlertItem2': 'Settings in Config Management to become invalid',
+ 'upload.delete.usageAlertItem3': 'Provider pool configurations to be lost',
+ 'upload.delete.usageAlertAdvice': 'Advice: Please remove file references in Config Management before deleting.',
+ 'upload.delete.forceDelete': 'Force Delete',
+ 'upload.delete.confirmDelete': 'Confirm Delete',
+ 'upload.batchLink.confirm': 'Are you sure you want to link {count} config files?\n\n{summary}',
+ 'upload.refresh.success': 'Refresh successful',
+ 'upload.action.view.failed': 'View failed',
+ 'upload.action.delete.failed': 'Delete failed',
+ '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',
+ 'upload.link.processing': 'Linking configuration to {name}...',
+ 'upload.link.success': 'Configuration linked successfully',
+ 'upload.link.failed': 'Link failed',
+ 'upload.batchLink.none': 'No configuration files to link',
+ 'upload.batchLink.processing': 'Batch linking {count} configurations...',
+ 'upload.batchLink.success': 'Successfully linked {count} configurations',
+ 'upload.batchLink.partial': 'Linking completed: {success} succeeded, {fail} failed',
+
+ // Providers
+ 'providers.title': 'Provider Pool Management',
+ 'providers.note': 'When provider pool is configured, it will be used by default. Falls back to default config if pool config fails',
+ 'providers.activeConnections': 'Active Connections',
+ 'providers.activeProviders': 'Active Providers',
+ 'providers.healthyProviders': 'Healthy Providers',
+ 'providers.status.healthy': '{healthy}/{total} Healthy',
+ 'providers.status.empty': '0/0 Nodes',
+ 'providers.stat.totalAccounts': 'Total Accounts',
+ 'providers.stat.healthyAccounts': 'Healthy Accounts',
+ 'providers.stat.usageCount': 'Usage Count',
+ 'providers.stat.errorCount': 'Error Count',
+ 'providers.auth.generate': 'Gen Auth',
+
+ // Modal Provider Manager
+ 'modal.provider.manage': 'Manage {type} Provider Config',
+ 'modal.provider.totalAccounts': 'Total Accounts:',
+ 'modal.provider.healthyAccounts': 'Healthy Accounts:',
+ 'modal.provider.add': 'Add Provider',
+ 'modal.provider.resetHealth': 'Reset Health',
+ 'modal.provider.healthCheck': 'Health Check',
+ 'modal.provider.resetHealthConfirm': 'Are you sure you want to reset all {type} nodes to healthy status?\n\nThis will clear error counts and timestamps for all nodes.',
+ 'modal.provider.healthCheckConfirm': 'Are you sure you want to perform a health check on all {type} nodes?\n\nThis will send test requests to each node to verify availability.',
+ 'modal.provider.deleteConfirm': 'Are you sure you want to delete this provider config? This cannot be undone.',
+ 'modal.provider.disableConfirm': 'Are you sure you want to disable this provider? It will no longer be selected for use.',
+ 'modal.provider.enableConfirm': 'Are you sure you want to enable this provider?',
+ 'modal.provider.edit': 'Edit',
+ 'modal.provider.delete': 'Delete',
+ 'modal.provider.save': 'Save',
+ 'modal.provider.cancel': 'Cancel',
+ 'modal.provider.status.healthy': 'Normal',
+ 'modal.provider.status.unhealthy': 'Abnormal',
+ 'modal.provider.status.disabled': 'Disabled',
+ 'modal.provider.status.enabled': 'Enabled',
+ 'modal.provider.lastError': 'Last Error:',
+ 'modal.provider.lastUsed': 'Last Used:',
+ 'modal.provider.lastCheck': 'Last Check:',
+ 'modal.provider.checkModel': 'Check Model:',
+ 'modal.provider.usageCount': 'Usage Count:',
+ 'modal.provider.errorCount': 'Error Count:',
+ 'modal.provider.neverUsed': 'Never Used',
+ 'modal.provider.neverChecked': 'Never Checked',
+ 'modal.provider.noModels': 'No models available for this provider type',
+ 'modal.provider.loadingModels': 'Loading models...',
+ 'modal.provider.unsupportedModels': 'Unsupported Models',
+ 'modal.provider.unsupportedModelsHelp': 'Select models not supported by this provider; they will be excluded automatically',
+ 'modal.provider.addTitle': 'Add New Provider Config',
+ 'modal.provider.customName': 'Custom Name',
+ 'modal.provider.checkModelName': 'Check Model Name',
+ 'modal.provider.healthCheckLabel': 'Health Check',
+ 'modal.provider.enabled': 'Enabled',
+ 'modal.provider.disabled': 'Disabled',
+ 'modal.provider.noProviderType': 'Unsupported provider type',
+
+ 'modal.provider.load.failed': 'Failed to load provider details',
+ 'modal.provider.auth.initializing': 'Initializing credential generation...',
+ 'modal.provider.auth.success': 'Credentials generated and path auto-filled',
+ 'modal.provider.auth.window': 'Please complete authorization in the opened window',
+ 'modal.provider.auth.failed': 'Failed to initialize credential generation',
+ 'modal.provider.save.success': 'Save successful',
+ 'modal.provider.save.failed': 'Save failed',
+ 'modal.provider.delete.success': 'Delete successful',
+ 'modal.provider.delete.failed': 'Delete failed',
+ 'modal.provider.add.success': 'Add successful',
+ 'modal.provider.add.failed': 'Add failed',
+ 'modal.provider.resetHealth.success': 'Successfully reset health status for {count} nodes',
+ 'modal.provider.resetHealth.failed': 'Failed to reset health status',
+
+ // Pagination
+ 'pagination.showing': 'Showing {start}-{end} of {total}',
+ 'pagination.jumpTo': 'Jump to',
+ 'pagination.page': 'Page',
+
+ // Usage
+ 'usage.title': 'Usage Query',
+ 'usage.refresh': 'Refresh Usage',
+ 'usage.lastUpdate': 'Last Update: {time}',
+ 'usage.lastUpdateCache': 'Cache Time: {time}',
+ 'usage.loading': 'Loading usage data...',
+ 'usage.empty': 'Click "Refresh Usage" button to get authorization file usage information',
+ 'usage.noData': 'No usage data available',
+ 'usage.noInstances': 'No initialized service instances',
+ 'usage.group.instances': '{count} instances',
+ 'usage.group.success': '{count}/{total} Success',
+ 'usage.card.status.disabled': 'Disabled',
+ 'usage.card.status.healthy': 'Healthy',
+ 'usage.card.status.unhealthy': 'Abnormal',
+ 'usage.card.totalUsage': 'Total Usage',
+ 'usage.card.freeTrial': 'Free Trial',
+ 'usage.card.bonus': 'Bonus',
+ 'usage.card.expires': 'Expires: {time}',
+
+ // Logs
+ 'logs.title': 'Real-time Logs',
+ 'logs.clear': 'Clear Logs',
+ 'logs.autoScroll': 'Auto Scroll',
+ 'logs.autoScroll.on': 'Auto Scroll: On',
+ 'logs.autoScroll.off': 'Auto Scroll: Off',
+
+ // Common
+ 'common.togglePassword': 'Show/Hide Password',
+ 'common.confirm': 'Confirm',
+ 'common.cancel': 'Cancel',
+ 'common.success': 'Success',
+ 'common.error': 'Error',
+ 'common.warning': 'Warning',
+ 'common.info': 'Info',
+ 'common.loading': 'Loading...',
+ 'common.upload': 'Upload',
+ 'common.generate': 'Generate',
+ 'common.search': 'Search',
+ 'common.welcome': 'Welcome to AIClient2API Management Console!',
+ 'common.fileType': 'Unsupported file type. Please select JSON, TXT, KEY, PEM, P12, or PFX.',
+ 'common.fileSize': 'File size cannot exceed 5MB.',
+ 'common.uploadSuccess': 'File uploaded successfully',
+ 'common.uploadFailed': 'File upload failed',
+ 'common.passwordUpdated': 'Admin password updated, takes effect next login',
+ 'common.configSaved': 'Configuration saved',
+ 'common.providerPoolRefreshed': 'Provider pool data refreshed',
+ 'common.copy.success': 'Content copied to clipboard',
+ 'common.copy.failed': 'Copy failed, please copy manually',
+ 'common.refresh.success': 'Refresh successful',
+ 'common.refresh.failed': 'Refresh failed',
+
+ // Login
+ 'login.title': 'Login - AIClient2API',
+ 'login.heading': 'Please login to continue',
+ 'login.password': 'Password',
+ 'login.passwordPlaceholder': 'Please enter password',
+ 'login.error.empty': 'Please enter password',
+ 'login.error.incorrect': 'Incorrect password, please try again',
+ 'login.error.failed': 'Login failed, please check your network connection',
+ 'login.button': 'Login',
+ 'login.loggingIn': 'Logging in...',
+ }
+};
+
+// 当前语言
+let currentLanguage = localStorage.getItem('language') || 'zh-CN';
+
+// 获取翻译文本
+export function t(key, params = {}) {
+ let text = translations[currentLanguage]?.[key] || translations['zh-CN']?.[key] || key;
+
+ // 替换参数
+ Object.keys(params).forEach(param => {
+ text = text.replace(`{${param}}`, params[param]);
+ });
+
+ return text;
+}
+
+// 切换语言
+export function setLanguage(lang) {
+ if (translations[lang]) {
+ currentLanguage = lang;
+ localStorage.setItem('language', lang);
+ updatePageLanguage();
+ // 触发语言切换事件
+ window.dispatchEvent(new CustomEvent('languageChanged', { detail: { language: lang } }));
+ }
+}
+
+// 获取当前语言
+export function getCurrentLanguage() {
+ return currentLanguage;
+}
+
+// 更新页面语言
+function updatePageLanguage() {
+ // 更新 HTML lang 属性
+ document.documentElement.lang = currentLanguage;
+
+ // 更新所有带 data-i18n 或 data-i18n-xxx 属性的元素
+ document.querySelectorAll('[data-i18n], [data-i18n-placeholder], [data-i18n-title], [data-i18n-aria-label]').forEach(element => {
+ // 1. 处理属性翻译 (placeholder, title, aria-label)
+ const attributes = ['placeholder', 'title', 'aria-label'];
+ attributes.forEach(attr => {
+ const attrKey = element.getAttribute(`data-i18n-${attr}`);
+ if (attrKey) {
+ const params = element.getAttribute(`data-i18n-${attr}-params`);
+ const parsedParams = params ? JSON.parse(params) : {};
+ if (attr === 'aria-label') {
+ element.setAttribute('aria-label', t(attrKey, parsedParams));
+ } else {
+ element[attr] = t(attrKey, parsedParams);
+ }
+ }
+ });
+
+ // 2. 处理主文本翻译 (data-i18n)
+ const key = element.getAttribute('data-i18n');
+ if (key) {
+ const params = element.getAttribute('data-i18n-params');
+ const parsedParams = params ? JSON.parse(params) : {};
+
+ if (element.tagName === 'INPUT' || element.tagName === 'TEXTAREA') {
+ // 如果没有显式的 data-i18n-placeholder,则 data-i18n 作用于 placeholder
+ if (!element.hasAttribute('data-i18n-placeholder')) {
+ element.placeholder = t(key, parsedParams);
+ }
+ } else {
+ element.textContent = t(key, parsedParams);
+ }
+ }
+ });
+
+ // 更新所有带 data-i18n-html 属性的元素(支持 HTML 内容)
+ document.querySelectorAll('[data-i18n-html]').forEach(element => {
+ const key = element.getAttribute('data-i18n-html');
+ const params = element.getAttribute('data-i18n-params');
+ const parsedParams = params ? JSON.parse(params) : {};
+ element.innerHTML = t(key, parsedParams);
+ });
+}
+
+// 初始化多语言
+export function initI18n() {
+ // 设置初始语言
+ updatePageLanguage();
+
+ // 监听 DOM 变化,自动翻译新添加的元素
+ const observer = new MutationObserver((mutations) => {
+ mutations.forEach((mutation) => {
+ mutation.addedNodes.forEach((node) => {
+ if (node.nodeType === 1) { // 元素节点
+ // 翻译新添加的元素
+ if (node.hasAttribute('data-i18n')) {
+ const key = node.getAttribute('data-i18n');
+ const params = node.getAttribute('data-i18n-params');
+ const parsedParams = params ? JSON.parse(params) : {};
+
+ if (node.tagName === 'INPUT' || node.tagName === 'TEXTAREA') {
+ if (node.placeholder !== undefined) {
+ node.placeholder = t(key, parsedParams);
+ }
+ } else {
+ node.textContent = t(key, parsedParams);
+ }
+ }
+
+ // 翻译子元素
+ node.querySelectorAll('[data-i18n]').forEach(element => {
+ const key = element.getAttribute('data-i18n');
+ const params = element.getAttribute('data-i18n-params');
+ const parsedParams = params ? JSON.parse(params) : {};
+
+ if (element.tagName === 'INPUT' || element.tagName === 'TEXTAREA') {
+ if (element.placeholder !== undefined) {
+ element.placeholder = t(key, parsedParams);
+ }
+ } else {
+ element.textContent = t(key, parsedParams);
+ }
+ });
+ }
+ });
+ });
+ });
+
+ observer.observe(document.body, {
+ childList: true,
+ subtree: true
+ });
+}
+
+// 导出所有函数
+export default {
+ t,
+ setLanguage,
+ getCurrentLanguage,
+ initI18n
+};
\ No newline at end of file
diff --git a/static/app/language-switcher.js b/static/app/language-switcher.js
new file mode 100644
index 0000000..baf3744
--- /dev/null
+++ b/static/app/language-switcher.js
@@ -0,0 +1,105 @@
+// 语言切换器组件
+import { setLanguage, getCurrentLanguage, t } from './i18n.js';
+
+// 创建语言切换器 HTML
+export function createLanguageSwitcher() {
+ const currentLang = getCurrentLanguage();
+
+ const switcher = document.createElement('div');
+ switcher.className = 'language-switcher';
+ switcher.innerHTML = `
+
+
+
+
+
+ `;
+
+ return switcher;
+}
+
+// 初始化语言切换器
+export function initLanguageSwitcher() {
+ // 创建并添加语言切换器到 header
+ const headerControls = document.querySelector('.header-controls');
+ if (headerControls) {
+ const switcher = createLanguageSwitcher();
+ // 插入到第一个位置
+ headerControls.insertBefore(switcher, headerControls.firstChild);
+
+ // 绑定事件
+ bindLanguageSwitcherEvents();
+ }
+}
+
+// 绑定语言切换器事件
+function bindLanguageSwitcherEvents() {
+ const languageBtn = document.getElementById('languageBtn');
+ const languageDropdown = document.getElementById('languageDropdown');
+ const languageOptions = document.querySelectorAll('.language-option');
+
+ if (!languageBtn || !languageDropdown) return;
+
+ // 切换下拉菜单显示/隐藏
+ languageBtn.addEventListener('click', (e) => {
+ e.stopPropagation();
+ languageDropdown.classList.toggle('show');
+ });
+
+ // 点击语言选项
+ languageOptions.forEach(option => {
+ option.addEventListener('click', (e) => {
+ e.stopPropagation();
+ const lang = option.getAttribute('data-lang');
+
+ // 切换语言
+ setLanguage(lang);
+
+ // 更新按钮文本
+ const currentLangSpan = languageBtn.querySelector('.current-lang');
+ if (currentLangSpan) {
+ currentLangSpan.textContent = lang === 'zh-CN' ? '中文' : 'EN';
+ }
+
+ // 更新选中状态
+ languageOptions.forEach(opt => opt.classList.remove('active'));
+ option.classList.add('active');
+
+ // 隐藏下拉菜单
+ languageDropdown.classList.remove('show');
+
+ // 显示提示
+ showToast(t('common.success'), lang === 'zh-CN' ? '已切换到简体中文' : 'Switched to English', 'success');
+ });
+ });
+
+ // 点击页面其他地方关闭下拉菜单
+ document.addEventListener('click', () => {
+ languageDropdown.classList.remove('show');
+ });
+}
+
+// 显示提示消息(使用现有的 toast 系统)
+function showToast(title, message, type = 'info') {
+ // 检查是否有 showToast 函数
+ if (typeof window.showToast === 'function') {
+ window.showToast(title, message, type);
+ } else {
+ // 如果没有,使用简单的 alert
+ console.log(`${title}: ${message}`);
+ }
+}
+
+export default {
+ createLanguageSwitcher,
+ initLanguageSwitcher
+};
\ No newline at end of file
diff --git a/static/app/modal.js b/static/app/modal.js
index a3d1d5f..90217b9 100644
--- a/static/app/modal.js
+++ b/static/app/modal.js
@@ -2,6 +2,7 @@
import { showToast, getFieldLabel, getProviderTypeFields } from './utils.js';
import { handleProviderPasswordToggle } from './event-handlers.js';
+import { t } from './i18n.js';
// 分页配置
const PROVIDERS_PER_PAGE = 5;
@@ -42,7 +43,7 @@ function showProviderManagerModal(data) {
modal.innerHTML = `
`;
@@ -633,12 +634,12 @@ function renderProviderConfig(provider) {
html += `
@@ -790,10 +791,10 @@ function editProvider(uuid, event) {
${toggleButtonText}
`;
}, 100);
@@ -857,10 +858,10 @@ function cancelEdit(uuid, event) {
${toggleButtonText}
`;
}
@@ -900,12 +901,12 @@ async function saveProvider(uuid, event) {
try {
await window.apiClient.put(`/providers/${encodeURIComponent(providerType)}/${uuid}`, { providerConfig });
await window.apiClient.post('/reload-config');
- showToast('提供商配置更新成功', 'success');
+ showToast(t('common.success'), t('modal.provider.save.success'), 'success');
// 重新获取该提供商类型的最新配置
await refreshProviderConfig(providerType);
} catch (error) {
console.error('Failed to update provider:', error);
- showToast('更新失败: ' + error.message, 'error');
+ showToast(t('common.error'), t('modal.provider.save.failed') + ': ' + error.message, 'error');
}
}
@@ -917,7 +918,7 @@ async function saveProvider(uuid, event) {
async function deleteProvider(uuid, event) {
event.stopPropagation();
- if (!confirm('确定要删除这个提供商配置吗?此操作不可恢复。')) {
+ if (!confirm(t('modal.provider.deleteConfirm'))) {
return;
}
@@ -927,12 +928,12 @@ async function deleteProvider(uuid, event) {
try {
await window.apiClient.delete(`/providers/${encodeURIComponent(providerType)}/${uuid}`);
await window.apiClient.post('/reload-config');
- showToast('提供商配置删除成功', 'success');
+ showToast(t('common.success'), t('modal.provider.delete.success'), 'success');
// 重新获取最新配置
await refreshProviderConfig(providerType);
} catch (error) {
console.error('Failed to delete provider:', error);
- showToast('删除失败: ' + error.message, 'error');
+ showToast(t('common.error'), t('modal.provider.delete.failed') + ': ' + error.message, 'error');
}
}
@@ -1031,21 +1032,21 @@ function showAddProviderForm(providerType) {
const form = document.createElement('div');
form.className = 'add-provider-form';
form.innerHTML = `
-
添加新提供商配置
+
添加新提供商配置
@@ -1054,10 +1055,10 @@ function showAddProviderForm(providerType) {
`;
@@ -1184,7 +1185,7 @@ function addDynamicConfigFields(form, providerType) {
fields += '';
}
} else {
- fields = '不支持的提供商类型
';
+ fields = `${t('modal.provider.noProviderType')}
`;
}
configFields.innerHTML = fields;
@@ -1245,7 +1246,7 @@ async function addProvider(providerType) {
providerConfig
});
await window.apiClient.post('/reload-config');
- showToast('提供商配置添加成功', 'success');
+ showToast(t('common.success'), t('modal.provider.add.success'), 'success');
// 移除添加表单
const form = document.querySelector('.add-provider-form');
if (form) {
@@ -1255,7 +1256,7 @@ async function addProvider(providerType) {
await refreshProviderConfig(providerType);
} catch (error) {
console.error('Failed to add provider:', error);
- showToast('添加失败: ' + error.message, 'error');
+ showToast(t('common.error'), t('modal.provider.add.failed') + ': ' + error.message, 'error');
}
}
@@ -1275,8 +1276,8 @@ async function toggleProviderStatus(uuid, event) {
const isCurrentlyDisabled = currentProvider.classList.contains('disabled');
const action = isCurrentlyDisabled ? 'enable' : 'disable';
const confirmMessage = isCurrentlyDisabled ?
- `确定要启用这个提供商配置吗?` :
- `确定要禁用这个提供商配置吗?禁用后该提供商将不会被选中使用。`;
+ t('modal.provider.enableConfirm') :
+ t('modal.provider.disableConfirm');
if (!confirm(confirmMessage)) {
return;
@@ -1285,12 +1286,12 @@ async function toggleProviderStatus(uuid, event) {
try {
await window.apiClient.post(`/providers/${encodeURIComponent(providerType)}/${uuid}/${action}`, { action });
await window.apiClient.post('/reload-config');
- showToast(`提供商${isCurrentlyDisabled ? '启用' : '禁用'}成功`, 'success');
+ showToast(t('common.success'), t('common.success'), 'success');
// 重新获取该提供商类型的最新配置
await refreshProviderConfig(providerType);
} catch (error) {
console.error('Failed to toggle provider status:', error);
- showToast(`操作失败: ${error.message}`, 'error');
+ showToast(t('common.error'), t('common.error') + ': ' + error.message, 'error');
}
}
@@ -1299,12 +1300,12 @@ async function toggleProviderStatus(uuid, event) {
* @param {string} providerType - 提供商类型
*/
async function resetAllProvidersHealth(providerType) {
- if (!confirm(`确定要将 ${providerType} 的所有节点重置为健康状态吗?\n\n这将清除所有节点的错误计数和错误时间。`)) {
+ if (!confirm(t('modal.provider.resetHealthConfirm', {type: providerType}))) {
return;
}
try {
- showToast('正在重置健康状态...', 'info');
+ showToast(t('common.info'), t('modal.provider.resetHealth') + '...', 'info');
const response = await window.apiClient.post(
`/providers/${encodeURIComponent(providerType)}/reset-health`,
@@ -1312,7 +1313,7 @@ async function resetAllProvidersHealth(providerType) {
);
if (response.success) {
- showToast(`成功重置 ${response.resetCount} 个节点的健康状态`, 'success');
+ showToast(t('common.success'), t('modal.provider.resetHealth.success', { count: response.resetCount }), 'success');
// 重新加载配置
await window.apiClient.post('/reload-config');
@@ -1320,11 +1321,11 @@ async function resetAllProvidersHealth(providerType) {
// 刷新提供商配置显示
await refreshProviderConfig(providerType);
} else {
- showToast('重置健康状态失败', 'error');
+ showToast(t('common.error'), t('modal.provider.resetHealth.failed'), 'error');
}
} catch (error) {
console.error('重置健康状态失败:', error);
- showToast(`重置健康状态失败: ${error.message}`, 'error');
+ showToast(t('common.error'), t('modal.provider.resetHealth.failed') + ': ' + error.message, 'error');
}
}
@@ -1333,12 +1334,12 @@ async function resetAllProvidersHealth(providerType) {
* @param {string} providerType - 提供商类型
*/
async function performHealthCheck(providerType) {
- if (!confirm(`确定要对 ${providerType} 的所有节点执行健康检测吗?\n\n这将向每个节点发送测试请求来验证其可用性。`)) {
+ if (!confirm(t('modal.provider.healthCheckConfirm', {type: providerType}))) {
return;
}
try {
- showToast('正在执行健康检测,请稍候...', 'info');
+ showToast(t('common.info'), t('modal.provider.healthCheck') + '...', 'info');
const response = await window.apiClient.post(
`/providers/${encodeURIComponent(providerType)}/health-check`,
@@ -1351,11 +1352,11 @@ async function performHealthCheck(providerType) {
// 统计跳过的数量(checkHealth 未启用的)
const skippedCount = results ? results.filter(r => r.success === null).length : 0;
- let message = `健康检测完成: ${successCount} 健康`;
+ let message = `${t('modal.provider.healthCheck')}完成: ${successCount} 健康`;
if (failCount > 0) message += `, ${failCount} 异常`;
if (skippedCount > 0) message += `, ${skippedCount} 跳过(未启用)`;
- showToast(message, failCount > 0 ? 'warning' : 'success');
+ showToast(t('common.info'), message, failCount > 0 ? 'warning' : 'success');
// 重新加载配置
await window.apiClient.post('/reload-config');
@@ -1363,11 +1364,11 @@ async function performHealthCheck(providerType) {
// 刷新提供商配置显示
await refreshProviderConfig(providerType);
} else {
- showToast('健康检测失败', 'error');
+ showToast(t('common.error'), t('modal.provider.healthCheck') + ' ' + t('common.error'), 'error');
}
} catch (error) {
console.error('健康检测失败:', error);
- showToast(`健康检测失败: ${error.message}`, 'error');
+ showToast(t('common.error'), t('modal.provider.healthCheck') + ' ' + t('common.error') + ': ' + error.message, 'error');
}
}
@@ -1382,7 +1383,7 @@ function renderNotSupportedModelsSelector(uuid, models, notSupportedModels = [])
if (!container) return;
if (models.length === 0) {
- container.innerHTML = '该提供商类型暂无可用模型列表
';
+ container.innerHTML = `${t('modal.provider.noModels')}
`;
return;
}
diff --git a/static/app/provider-manager.js b/static/app/provider-manager.js
index 8c0ad37..1ee7618 100644
--- a/static/app/provider-manager.js
+++ b/static/app/provider-manager.js
@@ -3,6 +3,7 @@
import { providerStats, updateProviderStats } from './constants.js';
import { showToast, formatUptime } from './utils.js';
import { fileUploadHandler } from './file-upload.js';
+import { t, getCurrentLanguage } from './i18n.js';
// 保存初始服务器时间和运行时间
let initialServerTime = null;
@@ -57,7 +58,7 @@ function updateTimeDisplay() {
// 更新服务器时间
if (serverTimeEl) {
const currentServerTime = new Date(initialServerTime.getTime() + elapsedSeconds * 1000);
- serverTimeEl.textContent = currentServerTime.toLocaleString('zh-CN', {
+ serverTimeEl.textContent = currentServerTime.toLocaleString(getCurrentLanguage(), {
year: 'numeric',
month: '2-digit',
day: '2-digit',
@@ -170,7 +171,7 @@ function renderProviders(providers) {
const isEmptyState = !hasProviders || totalCount === 0;
const statusClass = isEmptyState ? 'status-empty' : (healthyCount === totalCount ? 'status-healthy' : 'status-unhealthy');
const statusIcon = isEmptyState ? 'fa-info-circle' : (healthyCount === totalCount ? 'fa-check-circle' : 'fa-exclamation-triangle');
- const statusText = isEmptyState ? '0/0 节点' : `${healthyCount}/${totalCount} 健康`;
+ const statusText = isEmptyState ? t('providers.status.empty') : t('providers.status.healthy', { healthy: healthyCount, total: totalCount });
providerDiv.innerHTML = `
- 总账户
+ ${t('providers.stat.totalAccounts')}
${totalCount}
- 健康账户
+ ${t('providers.stat.healthyAccounts')}
${healthyCount}
- 使用次数
+ ${t('providers.stat.usageCount')}
${usageCount}
- 错误次数
+ ${t('providers.stat.errorCount')}
${errorCount}
@@ -310,7 +311,7 @@ async function openProviderManager(providerType) {
showProviderManagerModal(data);
} catch (error) {
console.error('Failed to load provider details:', error);
- showToast('加载提供商详情失败', 'error');
+ showToast(t('common.error'), t('modal.provider.load.failed'), 'error');
}
}
@@ -330,7 +331,7 @@ function generateAuthButton(providerType) {
return `
`;
}
@@ -341,7 +342,7 @@ function generateAuthButton(providerType) {
*/
async function handleGenerateAuthUrl(providerType) {
try {
- showToast('正在生成授权链接...', 'info');
+ showToast(t('common.info'), t('modal.provider.auth.initializing'), 'info');
// 使用 fileUploadHandler 中的 getProviderKey 获取目录名称
const providerDir = fileUploadHandler.getProviderKey(providerType);
@@ -358,11 +359,11 @@ async function handleGenerateAuthUrl(providerType) {
// 显示授权信息模态框
showAuthModal(response.authUrl, response.authInfo);
} else {
- showToast('生成授权链接失败', 'error');
+ showToast(t('common.error'), t('modal.provider.auth.failed'), 'error');
}
} catch (error) {
console.error('生成授权链接失败:', error);
- showToast(`生成授权链接失败: ${error.message}`, 'error');
+ showToast(t('common.error'), t('modal.provider.auth.failed') + `: ${error.message}`, 'error');
}
}
@@ -378,7 +379,7 @@ function getAuthFilePath(provider) {
'openai-qwen-oauth': '~/.qwen/oauth_creds.json',
'claude-kiro-oauth': '~/.aws/sso/cache/kiro-auth-token.json'
};
- return authFilePaths[provider] || '未知路径';
+ return authFilePaths[provider] || (getCurrentLanguage() === 'en-US' ? 'Unknown Path' : '未知路径');
}
/**
@@ -398,24 +399,24 @@ function showAuthModal(authUrl, authInfo) {
if (authInfo.provider === 'openai-qwen-oauth') {
instructionsHtml = `
-
授权步骤:
+
${t('oauth.modal.steps')}
- - 点击下方按钮在浏览器中打开授权页面
- - 完成授权后,系统会自动获取凭据文件
- - 凭据文件可在上传配置管理中查看和管理
- - 授权有效期: ${Math.floor(authInfo.expiresIn / 60)} 分钟
+ - ${t('oauth.modal.step1')}
+ - ${t('oauth.modal.step2.qwen')}
+ - ${t('oauth.modal.step3')}
+ - ${t('oauth.modal.step4.qwen', { min: Math.floor(authInfo.expiresIn / 60) })}
`;
} else {
instructionsHtml = `
-
授权步骤:
+
${t('oauth.modal.steps')}
- - 点击下方按钮在浏览器中打开授权页面
- - 使用您的Google账号登录并授权
- - 授权完成后,凭据文件会自动保存
- - 凭据文件可在上传配置管理中查看和管理
+ - ${t('oauth.modal.step1')}
+ - ${t('oauth.modal.step2.google')}
+ - ${t('oauth.modal.step4.google')}
+ - ${t('oauth.modal.step3')}
`;
@@ -424,18 +425,18 @@ function showAuthModal(authUrl, authInfo) {
modal.innerHTML = `
-
提供商: ${authInfo.provider}
+
${t('oauth.modal.provider')} ${authInfo.provider}
${instructionsHtml}
-
+
-
@@ -443,10 +444,10 @@ function showAuthModal(authUrl, authInfo) {
@@ -469,7 +470,7 @@ function showAuthModal(authUrl, authInfo) {
const input = modal.querySelector('.auth-url-input');
input.select();
document.execCommand('copy');
- showToast('授权链接已复制到剪贴板', 'success');
+ showToast(t('common.success'), t('oauth.success.msg'), 'success');
});
// 在浏览器中打开按钮
@@ -498,19 +499,19 @@ function showAuthModal(authUrl, authInfo) {
window.addEventListener('oauth_success_event', handleOAuthSuccess);
if (authWindow) {
- showToast('已打开授权窗口,请在窗口中完成操作', 'info');
+ showToast(t('common.info'), t('oauth.window.opened'), 'info');
// 添加手动输入回调 URL 的 UI
const urlSection = modal.querySelector('.auth-url-section');
if (urlSection && !modal.querySelector('.manual-callback-section')) {
const manualInputHtml = `
-
自动监听受阻?
-
如果授权窗口重定向后显示“无法访问”,请将该窗口地址栏的 完整 URL 粘贴到下方:
+
${t('oauth.manual.title')}
+
${t('oauth.manual.desc')}
-
+
- 提交
+ ${t('oauth.manual.submit')}
@@ -535,7 +536,7 @@ function showAuthModal(authUrl, authInfo) {
localUrl.hostname = window.location.hostname;
localUrl.protocol = window.location.protocol;
- showToast('正在完成授权...', 'info');
+ showToast(t('common.info'), t('oauth.processing'), 'info');
// 优先在子窗口中跳转(如果没关)
if (authWindow && !authWindow.closed) {
@@ -547,11 +548,11 @@ function showAuthModal(authUrl, authInfo) {
}
} else {
- showToast('该 URL 似乎不包含有效的授权代码', 'warning');
+ showToast(t('common.warning'), t('oauth.invalid.url'), 'warning');
}
} catch (err) {
console.error('处理回调失败:', err);
- showToast('无效的 URL 格式', 'error');
+ showToast(t('common.error'), t('oauth.error.format'), 'error');
}
};
@@ -576,7 +577,7 @@ function showAuthModal(authUrl, authInfo) {
}
}, 1000);
} else {
- showToast('授权窗口被浏览器拦截,请允许弹出窗口', 'error');
+ showToast(t('common.error'), t('oauth.window.blocked'), 'error');
}
});
diff --git a/static/app/routing-examples.js b/static/app/routing-examples.js
index f5aa18d..1dbb54f 100644
--- a/static/app/routing-examples.js
+++ b/static/app/routing-examples.js
@@ -1,6 +1,7 @@
// 路径路由示例功能模块
import { showToast } from './utils.js';
+import { t } from './i18n.js';
/**
* 初始化路径路由示例功能
@@ -67,7 +68,7 @@ function initCopyButtons() {
try {
await navigator.clipboard.writeText(path);
- showToast(`路径已复制: ${path}`, 'success');
+ showToast(t('common.success'), `${t('common.success')}: ${path}`, 'success');
// 临时更改按钮图标
const icon = button.querySelector('i');
@@ -84,7 +85,7 @@ function initCopyButtons() {
} catch (error) {
console.error('Failed to copy to clipboard:', error);
- showToast('复制失败', 'error');
+ showToast(t('common.error'), t('common.error'), 'error');
}
}
});
@@ -124,8 +125,8 @@ function getAvailableRoutes() {
openai: '/claude-custom/v1/chat/completions',
claude: '/claude-custom/v1/messages'
},
- description: '官方Claude API',
- badge: '官方API',
+ description: t('dashboard.routing.official'),
+ badge: t('dashboard.routing.official'),
badgeClass: 'official'
},
{
@@ -135,8 +136,8 @@ function getAvailableRoutes() {
openai: '/claude-kiro-oauth/v1/chat/completions',
claude: '/claude-kiro-oauth/v1/messages'
},
- description: '免费使用Claude Sonnet 4.5',
- badge: '免费使用',
+ description: t('dashboard.routing.free'),
+ badge: t('dashboard.routing.free'),
badgeClass: 'oauth'
},
{
@@ -146,8 +147,8 @@ function getAvailableRoutes() {
openai: '/openai-custom/v1/chat/completions',
claude: '/openai-custom/v1/messages'
},
- description: '官方OpenAI API',
- badge: '官方API',
+ description: t('dashboard.routing.official'),
+ badge: t('dashboard.routing.official'),
badgeClass: 'official'
},
{
@@ -157,8 +158,8 @@ function getAvailableRoutes() {
openai: '/gemini-cli-oauth/v1/chat/completions',
claude: '/gemini-cli-oauth/v1/messages'
},
- description: '突破Gemini免费限制',
- badge: '突破限制',
+ description: t('dashboard.routing.oauth'),
+ badge: t('dashboard.routing.oauth'),
badgeClass: 'oauth'
},
{
@@ -169,7 +170,7 @@ function getAvailableRoutes() {
claude: '/openai-qwen-oauth/v1/messages'
},
description: 'Qwen Code Plus',
- badge: '代码专用',
+ badge: t('dashboard.routing.oauth'),
badgeClass: 'oauth'
},
{
@@ -180,7 +181,7 @@ function getAvailableRoutes() {
claude: '/openaiResponses-custom/v1/messages'
},
description: '结构化对话API',
- badge: '结构化对话',
+ badge: 'Responses',
badgeClass: 'responses'
}
];
@@ -202,7 +203,7 @@ function highlightProviderRoute(provider) {
card.style.boxShadow = '';
}, 3000);
- showToast(`已定位到: ${provider}`, 'success');
+ showToast(t('common.success'), t('common.success') + `: ${provider}`, 'success');
}
}
@@ -216,7 +217,7 @@ async function copyCurlExample(provider, options = {}) {
const route = routes.find(r => r.provider === provider);
if (!route) {
- showToast('未找到对应的路由', 'error');
+ showToast(t('common.error'), t('common.error'), 'error');
return;
}
@@ -224,7 +225,7 @@ async function copyCurlExample(provider, options = {}) {
const path = route.paths[protocol];
if (!path) {
- showToast('未找到对应的协议路径', 'error');
+ showToast(t('common.error'), t('common.error'), 'error');
return;
}
@@ -322,10 +323,10 @@ async function copyCurlExample(provider, options = {}) {
try {
await navigator.clipboard.writeText(curlCommand);
- showToast('curl命令已复制到剪贴板', 'success');
+ showToast(t('common.success'), t('oauth.success.msg'), 'success');
} catch (error) {
console.error('Failed to copy curl command:', error);
- showToast('复制失败', 'error');
+ showToast(t('common.error'), t('common.error'), 'error');
}
}
diff --git a/static/app/styles.css b/static/app/styles.css
index c92f617..f6de28d 100644
--- a/static/app/styles.css
+++ b/static/app/styles.css
@@ -1495,7 +1495,7 @@ input:checked + .toggle-slider:before {
}
.btn-quick-link {
- background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%);
+ background: linear-gradient(135deg, var(--primary-color) 0%, var(--secondary-color) 100%);
color: white;
border: none;
border-radius: 4px;
@@ -1513,15 +1513,15 @@ input:checked + .toggle-slider:before {
.btn-quick-link:hover {
transform: translateY(-1px);
box-shadow: 0 4px 10px rgba(99, 102, 241, 0.4);
- background: linear-gradient(135deg, #4f46e5 0%, #7c3aed 100%);
+ background: linear-gradient(135deg, #047857 0%, var(--primary-color) 100%);
}
.btn-quick-link i {
font-size: 10px;
}
-.btn-batch-link-kiro {
- background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%);
+.btn-batch-link {
+ background: linear-gradient(135deg, var(--primary-color) 0%, var(--secondary-color) 100%);
color: white;
border: none;
border-radius: 6px;
@@ -1537,13 +1537,13 @@ input:checked + .toggle-slider:before {
font-weight: 500;
}
-.btn-batch-link-kiro:hover {
+.btn-batch-link:hover {
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(99, 102, 241, 0.4);
- background: linear-gradient(135deg, #4f46e5 0%, #7c3aed 100%);
+ background: linear-gradient(135deg, #047857 0%, var(--primary-color) 100%);
}
-.btn-batch-link-kiro i {
+.btn-batch-link i {
font-size: 11px;
}
@@ -4359,3 +4359,121 @@ input:checked + .toggle-slider:before {
padding: 0.5rem;
}
}
+
+/* ==================== 语言切换器样式 ==================== */
+.language-switcher {
+ position: relative;
+ display: inline-block;
+}
+
+.language-btn {
+ display: inline-flex;
+ align-items: center;
+ gap: 0.5rem;
+ padding: 0.5rem 1rem;
+ background: transparent;
+ color: var(--text-secondary);
+ border: 1px solid var(--border-color);
+ border-radius: 0.375rem;
+ cursor: pointer;
+ font-size: 0.875rem;
+ font-weight: 500;
+ transition: var(--transition);
+}
+
+.language-btn:hover {
+ background: var(--bg-tertiary);
+ color: var(--primary-color);
+ border-color: var(--primary-color);
+ transform: translateY(-1px);
+}
+
+.language-btn i {
+ font-size: 1rem;
+}
+
+.current-lang {
+ font-weight: 600;
+}
+
+.language-dropdown {
+ position: absolute;
+ top: calc(100% + 0.5rem);
+ right: 0;
+ background: var(--bg-primary);
+ border: 1px solid var(--border-color);
+ border-radius: 0.5rem;
+ box-shadow: var(--shadow-lg);
+ min-width: 150px;
+ opacity: 0;
+ visibility: hidden;
+ transform: translateY(-10px);
+ transition: all 0.3s ease;
+ z-index: 1000;
+}
+
+.language-dropdown.show {
+ opacity: 1;
+ visibility: visible;
+ transform: translateY(0);
+}
+
+.language-option {
+ display: flex;
+ align-items: center;
+ gap: 0.75rem;
+ width: 100%;
+ padding: 0.75rem 1rem;
+ background: none;
+ border: none;
+ text-align: left;
+ cursor: pointer;
+ font-size: 0.875rem;
+ color: var(--text-primary);
+ transition: var(--transition);
+ position: relative;
+}
+
+.language-option:first-child {
+ border-radius: 0.5rem 0.5rem 0 0;
+}
+
+.language-option:last-child {
+ border-radius: 0 0 0.5rem 0.5rem;
+}
+
+.language-option:hover {
+ background: var(--bg-secondary);
+}
+
+.language-option i {
+ font-size: 0.875rem;
+ color: var(--primary-color);
+ opacity: 0;
+ transition: opacity 0.3s ease;
+}
+
+.language-option.active i {
+ opacity: 1;
+}
+
+.language-option span {
+ flex: 1;
+}
+
+/* 响应式调整 */
+@media (max-width: 768px) {
+ .language-btn {
+ padding: 0.5rem 0.75rem;
+ font-size: 0.8rem;
+ }
+
+ .language-btn .current-lang {
+ display: none;
+ }
+
+ .language-dropdown {
+ right: auto;
+ left: 0;
+ }
+}
diff --git a/static/app/upload-config-manager.js b/static/app/upload-config-manager.js
index 3f436bf..84e002f 100644
--- a/static/app/upload-config-manager.js
+++ b/static/app/upload-config-manager.js
@@ -1,6 +1,7 @@
// 上传配置管理功能模块
import { showToast } from './utils.js';
+import { t } from './i18n.js';
let allConfigs = []; // 存储所有配置数据
let filteredConfigs = []; // 存储过滤后的配置数据
@@ -45,7 +46,7 @@ function renderConfigList() {
container.innerHTML = '';
if (!filteredConfigs.length) {
- container.innerHTML = '
';
+ container.innerHTML = `
`;
return;
}
@@ -69,7 +70,7 @@ function createConfigItemElement(config, index) {
item.dataset.index = index;
const statusIcon = config.isUsed ? 'fa-check-circle' : 'fa-circle';
- const statusText = config.isUsed ? '已关联' : '未关联';
+ 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' :
@@ -97,36 +98,36 @@ function createConfigItemElement(config, index) {
${formatDate(config.modified)}
- ${statusText}
+ ${statusText}
${quickLinkBtnHtml}
-
文件路径
+
文件路径
${config.path}
-
文件大小
+
文件大小
${formatFileSize(config.size)}
-
最后修改
+
最后修改
${formatDate(config.modified)}
-
关联状态
-
${statusText}
+
关联状态
+
${statusText}
${usageInfoHtml}
- 查看
+ ${t('upload.action.view')}
- 删除
+ ${t('upload.action.delete')}
@@ -186,17 +187,18 @@ function generateUsageInfoHtml(config) {
}
const typeLabels = {
- 'main_config': '主要配置',
- 'provider_pool': '提供商池',
- 'multiple': '多种用途'
+ 'main_config': t('upload.usage.mainConfig'),
+ 'provider_pool': t('upload.usage.providerPool'),
+ 'multiple': t('upload.usage.multiple')
};
- const typeLabel = typeLabels[usageType] || '未知用途';
+ const typeLabel = typeLabels[usageType] || (t('common.info') === 'Info' ? 'Unknown' : '未知用途');
let detailsHtml = '';
usageDetails.forEach(detail => {
- const icon = detail.type === '主要配置' ? 'fa-cog' : 'fa-network-wired';
- const usageTypeKey = detail.type === '主要配置' ? 'main_config' : 'provider_pool';
+ const isMain = detail.type === '主要配置' || detail.type === 'Main Config';
+ const icon = isMain ? 'fa-cog' : 'fa-network-wired';
+ const usageTypeKey = isMain ? 'main_config' : 'provider_pool';
detailsHtml += `
@@ -210,7 +212,7 @@ function generateUsageInfoHtml(config) {
${detailsHtml}
@@ -260,9 +262,18 @@ function updateStats() {
const usedEl = document.getElementById('usedConfigCount');
const unusedEl = document.getElementById('unusedConfigCount');
- if (totalEl) totalEl.textContent = `共 ${totalCount} 个配置文件`;
- if (usedEl) usedEl.textContent = `已关联: ${usedCount}`;
- if (unusedEl) unusedEl.textContent = `未关联: ${unusedCount}`;
+ if (totalEl) {
+ totalEl.textContent = t('upload.count', { count: totalCount });
+ totalEl.setAttribute('data-i18n-params', JSON.stringify({ count: totalCount.toString() }));
+ }
+ if (usedEl) {
+ usedEl.textContent = t('upload.usedCount', { count: usedCount });
+ usedEl.setAttribute('data-i18n-params', JSON.stringify({ count: usedCount.toString() }));
+ }
+ if (unusedEl) {
+ unusedEl.textContent = t('upload.unusedCount', { count: unusedCount });
+ unusedEl.setAttribute('data-i18n-params', JSON.stringify({ count: unusedCount.toString() }));
+ }
}
/**
@@ -285,10 +296,10 @@ async function loadConfigList() {
renderConfigList();
updateStats();
console.log('配置列表加载成功,共', allConfigs.length, '个项目');
- // showToast('配置文件列表已刷新', 'success');
+ // showToast(t('common.success'), t('upload.refresh') + '成功', 'success');
} catch (error) {
console.error('加载配置列表失败:', error);
- showToast('加载配置列表失败: ' + error.message, 'error');
+ showToast(t('common.error'), t('common.error') + ': ' + error.message, 'error');
// 使用模拟数据作为示例
allConfigs = generateMockConfigData();
@@ -375,7 +386,7 @@ async function viewConfig(path) {
showConfigModal(fileData);
} catch (error) {
console.error('查看配置失败:', error);
- showToast('查看配置失败: ' + error.message, 'error');
+ showToast(t('common.error'), t('upload.action.view.failed') + ': ' + error.message, 'error');
}
}
@@ -390,7 +401,7 @@ function showConfigModal(fileData) {
modal.innerHTML = `
@@ -477,7 +488,7 @@ async function copyConfigContent(path) {
// 尝试使用现代 Clipboard API
if (navigator.clipboard && navigator.clipboard.writeText) {
await navigator.clipboard.writeText(fileData.content);
- showToast('内容已复制到剪贴板', 'success');
+ showToast(t('common.success'), t('oauth.success.msg'), 'success');
} else {
// 降级方案:使用传统的 document.execCommand
const textarea = document.createElement('textarea');
@@ -490,20 +501,20 @@ async function copyConfigContent(path) {
try {
const successful = document.execCommand('copy');
if (successful) {
- showToast('内容已复制到剪贴板', 'success');
+ showToast(t('common.copy.success'), 'success');
} else {
- showToast('复制失败,请手动复制', 'error');
+ showToast(t('common.copy.failed'), 'error');
}
} catch (err) {
console.error('复制失败:', err);
- showToast('复制失败,请手动复制', 'error');
+ showToast(t('common.copy.failed'), 'error');
} finally {
document.body.removeChild(textarea);
}
}
} catch (error) {
console.error('复制失败:', error);
- showToast('复制失败: ' + error.message, 'error');
+ showToast(t('common.copy.failed') + ': ' + error.message, 'error');
}
}
@@ -525,7 +536,7 @@ function escapeHtml(text) {
function showDeleteConfirmModal(config) {
const isUsed = config.isUsed;
const modalClass = isUsed ? 'delete-confirm-modal used' : 'delete-confirm-modal unused';
- const title = isUsed ? '删除已关联配置' : '删除配置文件';
+ const title = isUsed ? t('upload.delete.confirmTitleUsed') : t('upload.delete.confirmTitle');
const icon = isUsed ? 'fas fa-exclamation-triangle' : 'fas fa-trash';
const buttonClass = isUsed ? 'btn btn-danger' : 'btn btn-warning';
@@ -535,7 +546,7 @@ function showDeleteConfirmModal(config) {
modal.innerHTML = `
${isUsed ?
- '
⚠️ 此配置已被系统使用
删除已关联的配置文件可能会影响系统正常运行。请确保您了解删除的后果。
' :
- '
🗑️ 确认删除配置文件
此操作将永久删除配置文件,且无法撤销。
'
+ `
${t('upload.delete.warningUsedTitle')}
${t('upload.delete.warningUsedDesc')}
` :
+ `
${t('upload.delete.warningUnusedTitle')}
${t('upload.delete.warningUnusedDesc')}
`
}
- 文件名:
+ 文件名:
${config.name}
- 文件路径:
+ 文件路径:
${config.path}
- 文件大小:
+ 文件大小:
${formatFileSize(config.size)}
- 关联状态:
-
- ${isUsed ? '已关联' : '未关联'}
+ 关联状态:
+
+ ${isUsed ? t('upload.statusFilter.used') : t('upload.statusFilter.unused')}
@@ -580,23 +591,23 @@ function showDeleteConfirmModal(config) {
-
关联详情
-
此配置文件正在被系统使用,删除后可能会导致:
+
${t('upload.delete.usageAlertTitle')}
+
${t('upload.delete.usageAlertDesc')}
- - 相关的AI服务无法正常工作
- - 配置管理中的设置失效
- - 提供商池配置丢失
+ - ${t('upload.delete.usageAlertItem1')}
+ - ${t('upload.delete.usageAlertItem2')}
+ - ${t('upload.delete.usageAlertItem3')}
-
建议:请先在配置管理中解除文件引用后再删除。
+
${t('upload.delete.usageAlertAdvice')}
` : ''}
@@ -658,7 +669,7 @@ function showDeleteConfirmModal(config) {
async function performDelete(path) {
try {
const result = await window.apiClient.delete(`/upload-configs/delete/${encodeURIComponent(path)}`);
- showToast(result.message, 'success');
+ showToast(t('common.success'), result.message, 'success');
// 从本地列表中移除
allConfigs = allConfigs.filter(c => c.path !== path);
@@ -667,7 +678,7 @@ async function performDelete(path) {
updateStats();
} catch (error) {
console.error('删除配置失败:', error);
- showToast('删除配置失败: ' + error.message, 'error');
+ showToast(t('common.error'), t('upload.action.delete.failed') + ': ' + error.message, 'error');
}
}
@@ -678,7 +689,7 @@ async function performDelete(path) {
async function deleteConfig(path) {
const config = filteredConfigs.find(c => c.path === path) || allConfigs.find(c => c.path === path);
if (!config) {
- showToast('配置文件不存在', 'error');
+ showToast(t('common.error'), t('upload.config.notExist'), 'error');
return;
}
@@ -695,6 +706,7 @@ function initUploadConfigManager() {
const searchBtn = document.getElementById('searchConfigBtn');
const statusFilter = document.getElementById('configStatusFilter');
const refreshBtn = document.getElementById('refreshConfigList');
+ const downloadAllBtn = document.getElementById('downloadAllConfigs');
if (searchInput) {
searchInput.addEventListener('input', debounce(() => {
@@ -724,6 +736,10 @@ function initUploadConfigManager() {
refreshBtn.addEventListener('click', loadConfigList);
}
+ if (downloadAllBtn) {
+ downloadAllBtn.addEventListener('click', downloadAllConfigs);
+ }
+
// 批量关联配置按钮
const batchLinkBtn = document.getElementById('batchLinkKiroBtn') || document.getElementById('batchLinkProviderBtn');
if (batchLinkBtn) {
@@ -746,7 +762,7 @@ async function reloadConfig() {
try {
const result = await window.apiClient.post('/reload-config');
- showToast(result.message, 'success');
+ showToast(t('common.success'), result.message, 'success');
// 重新加载配置列表以反映最新的关联状态
await loadConfigList();
@@ -758,7 +774,7 @@ async function reloadConfig() {
} catch (error) {
console.error('重载配置失败:', error);
- showToast('重载配置失败: ' + error.message, 'error');
+ showToast(t('common.error'), t('common.refresh.failed') + ': ' + error.message, 'error');
}
}
@@ -822,23 +838,23 @@ async function quickLinkProviderConfig(filePath) {
try {
const providerInfo = detectProviderFromPath(filePath);
if (!providerInfo) {
- showToast('无法识别配置文件对应的提供商类型', 'error');
+ showToast(t('common.error'), t('upload.link.failed.identify'), 'error');
return;
}
- showToast(`正在关联配置到 ${providerInfo.displayName}...`, 'info');
+ showToast(t('common.info'), t('upload.link.processing', { name: providerInfo.displayName }), 'info');
const result = await window.apiClient.post('/quick-link-provider', {
filePath: filePath
});
- showToast(result.message || '配置关联成功', 'success');
+ showToast(t('common.success'), result.message || t('upload.link.success'), 'success');
// 刷新配置列表
await loadConfigList();
} catch (error) {
console.error('一键关联失败:', error);
- showToast('关联失败: ' + error.message, 'error');
+ showToast(t('common.error'), t('upload.link.failed') + ': ' + error.message, 'error');
}
}
@@ -854,7 +870,7 @@ async function batchLinkProviderConfigs() {
});
if (unlinkedConfigs.length === 0) {
- showToast('没有需要关联的配置文件', 'info');
+ showToast(t('common.info'), t('upload.batchLink.none'), 'info');
return;
}
@@ -874,12 +890,12 @@ async function batchLinkProviderConfigs() {
.map(([name, count]) => `${name}: ${count}个`)
.join(', ');
- const confirmMsg = `确定要批量关联 ${unlinkedConfigs.length} 个配置吗?\n\n${providerSummary}`;
+ const confirmMsg = t('upload.batchLink.confirm', { count: unlinkedConfigs.length, summary: providerSummary });
if (!confirm(confirmMsg)) {
return;
}
- showToast(`正在批量关联 ${unlinkedConfigs.length} 个配置...`, 'info');
+ showToast(t('common.info'), t('upload.batchLink.processing', { count: unlinkedConfigs.length }), 'info');
let successCount = 0;
let failCount = 0;
@@ -900,9 +916,9 @@ async function batchLinkProviderConfigs() {
await loadConfigList();
if (failCount === 0) {
- showToast(`成功关联 ${successCount} 个配置`, 'success');
+ showToast(t('common.success'), t('upload.batchLink.success', { count: successCount }), 'success');
} else {
- showToast(`关联完成: 成功 ${successCount} 个, 失败 ${failCount} 个`, 'warning');
+ showToast(t('common.warning'), t('upload.batchLink.partial', { success: successCount, fail: failCount }), 'warning');
}
}
@@ -924,6 +940,53 @@ function debounce(func, wait) {
};
}
+/**
+ * 打包下载所有配置文件
+ */
+async function downloadAllConfigs() {
+ try {
+ showToast(t('common.info'), t('common.loading'), 'info');
+
+ // 使用 window.apiClient.get 获取 Blob 数据
+ // 由于 apiClient 默认可能是处理 JSON 的,我们需要直接调用 fetch 或者确保 apiClient 支持返回原始响应
+ const token = localStorage.getItem('authToken');
+ const headers = {
+ 'Authorization': token ? `Bearer ${token}` : ''
+ };
+
+ const response = await fetch('/api/upload-configs/download-all', { headers });
+
+ if (!response.ok) {
+ const errorData = await response.json();
+ throw new Error(errorData.error?.message || '下载失败');
+ }
+
+ const blob = await response.blob();
+ const url = window.URL.createObjectURL(blob);
+ const a = document.createElement('a');
+ a.href = url;
+
+ // 从 Content-Disposition 中提取文件名,或者使用默认名
+ const contentDisposition = response.headers.get('Content-Disposition');
+ let filename = `configs_backup_${new Date().toISOString().slice(0, 10)}.zip`;
+ if (contentDisposition && contentDisposition.indexOf('filename=') !== -1) {
+ const matches = /filename="([^"]+)"/.exec(contentDisposition);
+ if (matches && matches[1]) filename = matches[1];
+ }
+
+ a.download = filename;
+ document.body.appendChild(a);
+ a.click();
+ window.URL.revokeObjectURL(url);
+ document.body.removeChild(a);
+
+ showToast(t('common.success'), t('common.success'), 'success');
+ } catch (error) {
+ console.error('打包下载失败:', error);
+ showToast(t('common.error'), t('common.error') + ': ' + error.message, 'error');
+ }
+}
+
// 导出函数
export {
initUploadConfigManager,
diff --git a/static/app/usage-manager.js b/static/app/usage-manager.js
index f7d4783..195a58b 100644
--- a/static/app/usage-manager.js
+++ b/static/app/usage-manager.js
@@ -2,6 +2,7 @@
import { showToast } from './utils.js';
import { getAuthHeaders } from './auth.js';
+import { t, getCurrentLanguage } from './i18n.js';
/**
* 初始化用量管理功能
@@ -52,10 +53,15 @@ export async function loadUsage() {
// 更新最后更新时间
if (lastUpdateEl) {
+ const timeStr = new Date(data.timestamp || Date.now()).toLocaleString(getCurrentLanguage());
if (data.fromCache && data.timestamp) {
- lastUpdateEl.textContent = `缓存时间: ${new Date(data.timestamp).toLocaleString()}`;
+ lastUpdateEl.textContent = t('usage.lastUpdateCache', { time: timeStr });
+ lastUpdateEl.setAttribute('data-i18n', 'usage.lastUpdateCache');
+ lastUpdateEl.setAttribute('data-i18n-params', JSON.stringify({ time: timeStr }));
} else {
- lastUpdateEl.textContent = `上次更新: ${new Date().toLocaleString()}`;
+ lastUpdateEl.textContent = t('usage.lastUpdate', { time: timeStr });
+ lastUpdateEl.setAttribute('data-i18n', 'usage.lastUpdate');
+ lastUpdateEl.setAttribute('data-i18n-params', JSON.stringify({ time: timeStr }));
}
}
} catch (error) {
@@ -66,7 +72,7 @@ export async function loadUsage() {
errorEl.style.display = 'block';
const errorMsgEl = document.getElementById('usageErrorMessage');
if (errorMsgEl) {
- errorMsgEl.textContent = error.message || '获取用量数据失败';
+ errorMsgEl.textContent = error.message || t('usage.title') + '失败';
}
}
}
@@ -110,10 +116,13 @@ export async function refreshUsage() {
// 更新最后更新时间
if (lastUpdateEl) {
- lastUpdateEl.textContent = `上次更新: ${new Date().toLocaleString()}`;
+ const timeStr = new Date().toLocaleString(getCurrentLanguage());
+ lastUpdateEl.textContent = t('usage.lastUpdate', { time: timeStr });
+ lastUpdateEl.setAttribute('data-i18n', 'usage.lastUpdate');
+ lastUpdateEl.setAttribute('data-i18n-params', JSON.stringify({ time: timeStr }));
}
- showToast('用量数据已刷新', 'success');
+ showToast(t('common.success'), t('common.refresh.success'), 'success');
} catch (error) {
console.error('获取用量数据失败:', error);
@@ -122,11 +131,11 @@ export async function refreshUsage() {
errorEl.style.display = 'block';
const errorMsgEl = document.getElementById('usageErrorMessage');
if (errorMsgEl) {
- errorMsgEl.textContent = error.message || '获取用量数据失败';
+ errorMsgEl.textContent = error.message || t('usage.title') + '失败';
}
}
- showToast('获取用量数据失败: ' + error.message, 'error');
+ showToast(t('common.error'), t('common.refresh.failed') + ': ' + error.message, 'error');
} finally {
if (refreshBtn) refreshBtn.disabled = false;
}
@@ -147,7 +156,7 @@ function renderUsageData(data, container) {
container.innerHTML = `
-
暂无用量数据
+
${t('usage.noData')}
`;
return;
@@ -180,7 +189,7 @@ function renderUsageData(data, container) {
container.innerHTML = `
-
暂无已初始化的服务实例
+
${t('usage.noInstances')}
`;
return;
@@ -216,8 +225,8 @@ function createProviderGroup(providerType, instances) {
${providerDisplayName}
- ${instanceCount} 个实例
- ${successCount}/${instanceCount} 成功
+ ${t('usage.group.instances', { count: instanceCount })}
+ ${t('usage.group.success', { count: successCount, total: instanceCount })}
`;
@@ -268,10 +277,10 @@ function createInstanceUsageCard(instance, providerType) {
: '';
const healthBadge = instance.isDisabled
- ? '已禁用'
+ ? `${t('usage.card.status.disabled')}`
: (instance.isHealthy
- ? '健康'
- : '异常');
+ ? `${t('usage.card.status.healthy')}`
+ : `${t('usage.card.status.unhealthy')}`);
// 获取用户邮箱和订阅信息
const userEmail = instance.usage?.user?.email || '';
@@ -343,7 +352,7 @@ function renderUsageDetails(usage) {
totalSection.innerHTML = `
@@ -399,9 +408,9 @@ function createUsageBreakdownHTML(breakdown) {
if (breakdown.freeTrial && breakdown.freeTrial.status === 'ACTIVE') {
html += `
`;
}
@@ -414,7 +423,7 @@ function createUsageBreakdownHTML(breakdown) {
`;
}
@@ -522,7 +531,7 @@ function formatDate(dateStr) {
if (!dateStr) return '--';
try {
const date = new Date(dateStr);
- return date.toLocaleString('zh-CN', {
+ return date.toLocaleString(getCurrentLanguage(), {
year: 'numeric',
month: '2-digit',
day: '2-digit',
diff --git a/static/app/utils.js b/static/app/utils.js
index e7846f0..8bd3afe 100644
--- a/static/app/utils.js
+++ b/static/app/utils.js
@@ -1,4 +1,5 @@
// 工具函数
+import { t, getCurrentLanguage } from './i18n.js';
/**
* 格式化运行时间
@@ -10,6 +11,10 @@ function formatUptime(seconds) {
const hours = Math.floor((seconds % 86400) / 3600);
const minutes = Math.floor((seconds % 3600) / 60);
const secs = Math.floor(seconds % 60);
+
+ if (getCurrentLanguage() === 'en-US') {
+ return `${days}d ${hours}h ${minutes}m ${secs}s`;
+ }
return `${days}天 ${hours}小时 ${minutes}分 ${secs}秒`;
}
@@ -26,13 +31,22 @@ function escapeHtml(text) {
/**
* 显示提示消息
+ * @param {string} title - 提示标题 (可选,旧接口为 message)
* @param {string} message - 提示消息
* @param {string} type - 消息类型 (info, success, error)
*/
-function showToast(message, type = 'info') {
+function showToast(title, message, type = 'info') {
+ // 兼容旧接口 (message, type)
+ if (arguments.length === 2 && (message === 'success' || message === 'error' || message === 'info' || message === 'warning')) {
+ type = message;
+ message = title;
+ title = t(`common.${type}`);
+ }
+
const toast = document.createElement('div');
toast.className = `toast ${type}`;
toast.innerHTML = `
+
${escapeHtml(title)}
${escapeHtml(message)}
`;
@@ -53,19 +67,20 @@ function showToast(message, type = 'info') {
* @returns {string} 显示文案
*/
function getFieldLabel(key) {
+ const isEn = getCurrentLanguage() === 'en-US';
const labelMap = {
- 'customName': '自定义名称 (选填)',
- 'checkModelName': '检查模型名称 (选填)',
- 'checkHealth': '健康检查',
+ 'customName': t('modal.provider.customName') + ' ' + t('config.optional'),
+ 'checkModelName': t('modal.provider.checkModelName') + ' ' + t('config.optional'),
+ 'checkHealth': t('modal.provider.healthCheckLabel'),
'OPENAI_API_KEY': 'OpenAI API Key',
'OPENAI_BASE_URL': 'OpenAI Base URL',
'CLAUDE_API_KEY': 'Claude API Key',
'CLAUDE_BASE_URL': 'Claude Base URL',
- 'PROJECT_ID': '项目ID',
- 'GEMINI_OAUTH_CREDS_FILE_PATH': 'OAuth凭据文件路径',
- 'KIRO_OAUTH_CREDS_FILE_PATH': 'OAuth凭据文件路径',
- 'QWEN_OAUTH_CREDS_FILE_PATH': 'OAuth凭据文件路径',
- 'ANTIGRAVITY_OAUTH_CREDS_FILE_PATH': 'OAuth凭据文件路径',
+ 'PROJECT_ID': isEn ? 'Project ID' : '项目ID',
+ 'GEMINI_OAUTH_CREDS_FILE_PATH': isEn ? 'OAuth Credentials File Path' : 'OAuth凭据文件路径',
+ 'KIRO_OAUTH_CREDS_FILE_PATH': isEn ? 'OAuth Credentials File Path' : 'OAuth凭据文件路径',
+ 'QWEN_OAUTH_CREDS_FILE_PATH': isEn ? 'OAuth Credentials File Path' : 'OAuth凭据文件路径',
+ 'ANTIGRAVITY_OAUTH_CREDS_FILE_PATH': isEn ? 'OAuth Credentials File Path' : 'OAuth凭据文件路径',
'GEMINI_BASE_URL': 'Gemini Base URL',
'KIRO_BASE_URL': 'Base URL',
'KIRO_REFRESH_URL': 'Refresh URL',
@@ -84,6 +99,7 @@ function getFieldLabel(key) {
* @returns {Array} 字段配置数组
*/
function getProviderTypeFields(providerType) {
+ const isEn = getCurrentLanguage() === 'en-US';
const fieldConfigs = {
'openai-custom': [
{
@@ -130,19 +146,19 @@ function getProviderTypeFields(providerType) {
'gemini-cli-oauth': [
{
id: 'PROJECT_ID',
- label: '项目ID',
+ label: isEn ? 'Project ID' : '项目ID',
type: 'text',
- placeholder: 'Google Cloud项目ID'
+ placeholder: isEn ? 'Google Cloud Project ID' : 'Google Cloud项目ID'
},
{
id: 'GEMINI_OAUTH_CREDS_FILE_PATH',
- label: 'OAuth凭据文件路径',
+ label: isEn ? 'OAuth Credentials File Path' : 'OAuth凭据文件路径',
type: 'text',
- placeholder: '例如: ~/.gemini/oauth_creds.json'
+ placeholder: isEn ? 'e.g.: ~/.gemini/oauth_creds.json' : '例如: ~/.gemini/oauth_creds.json'
},
{
id: 'GEMINI_BASE_URL',
- label: 'Gemini Base URL
(选填)',
+ label: `Gemini Base URL
${t('config.optional')}`,
type: 'text',
placeholder: 'https://cloudcode-pa.googleapis.com'
}
@@ -150,25 +166,25 @@ function getProviderTypeFields(providerType) {
'claude-kiro-oauth': [
{
id: 'KIRO_OAUTH_CREDS_FILE_PATH',
- label: 'OAuth凭据文件路径',
+ label: isEn ? 'OAuth Credentials File Path' : 'OAuth凭据文件路径',
type: 'text',
- placeholder: '例如: ~/.aws/sso/cache/kiro-auth-token.json'
+ placeholder: isEn ? 'e.g.: ~/.aws/sso/cache/kiro-auth-token.json' : '例如: ~/.aws/sso/cache/kiro-auth-token.json'
},
{
id: 'KIRO_BASE_URL',
- label: 'Base URL
(选填)',
+ label: `Base URL
${t('config.optional')}`,
type: 'text',
placeholder: 'https://codewhisperer.{{region}}.amazonaws.com/generateAssistantResponse'
},
{
id: 'KIRO_REFRESH_URL',
- label: 'Refresh URL
(选填)',
+ label: `Refresh URL
${t('config.optional')}`,
type: 'text',
placeholder: 'https://prod.{{region}}.auth.desktop.kiro.dev/refreshToken'
},
{
id: 'KIRO_REFRESH_IDC_URL',
- label: 'Refresh IDC URL
(选填)',
+ label: `Refresh IDC URL
${t('config.optional')}`,
type: 'text',
placeholder: 'https://oidc.{{region}}.amazonaws.com/token'
}
@@ -176,19 +192,19 @@ function getProviderTypeFields(providerType) {
'openai-qwen-oauth': [
{
id: 'QWEN_OAUTH_CREDS_FILE_PATH',
- label: 'OAuth凭据文件路径',
+ label: isEn ? 'OAuth Credentials File Path' : 'OAuth凭据文件路径',
type: 'text',
- placeholder: '例如: ~/.qwen/oauth_creds.json'
+ placeholder: isEn ? 'e.g.: ~/.qwen/oauth_creds.json' : '例如: ~/.qwen/oauth_creds.json'
},
{
id: 'QWEN_BASE_URL',
- label: 'Qwen Base URL
(选填)',
+ label: `Qwen Base URL
${t('config.optional')}`,
type: 'text',
placeholder: 'https://portal.qwen.ai/v1'
},
{
id: 'QWEN_OAUTH_BASE_URL',
- label: 'OAuth Base URL
(选填)',
+ label: `OAuth Base URL
${t('config.optional')}`,
type: 'text',
placeholder: 'https://chat.qwen.ai'
}
@@ -196,25 +212,25 @@ function getProviderTypeFields(providerType) {
'gemini-antigravity': [
{
id: 'PROJECT_ID',
- label: '项目ID (选填)',
+ label: isEn ? 'Project ID (Optional)' : '项目ID (选填)',
type: 'text',
- placeholder: 'Google Cloud项目ID (留空自动发现)'
+ placeholder: isEn ? 'Google Cloud Project ID (Leave blank for discovery)' : 'Google Cloud项目ID (留空自动发现)'
},
{
id: 'ANTIGRAVITY_OAUTH_CREDS_FILE_PATH',
- label: 'OAuth凭据文件路径',
+ label: isEn ? 'OAuth Credentials File Path' : 'OAuth凭据文件路径',
type: 'text',
- placeholder: '例如: ~/.antigravity/oauth_creds.json'
+ placeholder: isEn ? 'e.g.: ~/.antigravity/oauth_creds.json' : '例如: ~/.antigravity/oauth_creds.json'
},
{
id: 'ANTIGRAVITY_BASE_URL_DAILY',
- label: 'Daily Base URL
(选填)',
+ label: `Daily Base URL
${t('config.optional')}`,
type: 'text',
placeholder: 'https://daily-cloudcode-pa.sandbox.googleapis.com'
},
{
id: 'ANTIGRAVITY_BASE_URL_AUTOPUSH',
- label: 'Autopush Base URL
(选填)',
+ label: `Autopush Base URL
${t('config.optional')}`,
type: 'text',
placeholder: 'https://autopush-cloudcode-pa.sandbox.googleapis.com'
}
diff --git a/static/index.html b/static/index.html
index 2f3303d..f14a7fe 100644
--- a/static/index.html
+++ b/static/index.html
@@ -1,11 +1,11 @@
-
+
-
-
AIClient2API - 管理控制台
+
+
AIClient2API - 管理控制台
@@ -15,17 +15,17 @@