feat(i18n): 添加多语言支持并实现国际化功能
实现中英文双语支持,包括: 1. 添加i18n.js核心模块处理语言切换和翻译 2. 创建语言切换器组件 3. 更新所有UI文本使用翻译键 4. 添加I18N_GUIDE.md文档说明使用方法 5. 修改样式适配语言切换器 6. 添加adm-zip依赖支持配置文件打包下载 7. 更新登录页面支持多语言 8. 重构toast消息显示支持多语言标题
This commit is contained in:
parent
f3761a4254
commit
fa8150701f
20 changed files with 1939 additions and 499 deletions
10
package-lock.json
generated
10
package-lock.json
generated
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
157
static/app/I18N_GUIDE.md
Normal file
157
static/app/I18N_GUIDE.md
Normal file
|
|
@ -0,0 +1,157 @@
|
|||
# 多语言支持使用指南
|
||||
|
||||
## 概述
|
||||
|
||||
本项目已实现中英文双语支持,可以通过页面右上角的语言切换按钮在简体中文和英文之间切换。
|
||||
|
||||
## 文件结构
|
||||
|
||||
```
|
||||
static/app/
|
||||
├── i18n.js # 多语言配置文件(包含所有翻译)
|
||||
├── language-switcher.js # 语言切换组件
|
||||
└── I18N_GUIDE.md # 本指南
|
||||
```
|
||||
|
||||
## 如何使用
|
||||
|
||||
### 1. 在 HTML 中添加多语言支持
|
||||
|
||||
使用 `data-i18n` 属性标记需要翻译的元素:
|
||||
|
||||
```html
|
||||
<!-- 文本内容 -->
|
||||
<h1 data-i18n="header.title">AIClient2API 管理控制台</h1>
|
||||
|
||||
<!-- 按钮文本 -->
|
||||
<button data-i18n="common.save">保存</button>
|
||||
|
||||
<!-- 输入框占位符 -->
|
||||
<input type="text" data-i18n="config.apiKeyPlaceholder" placeholder="请输入API密钥">
|
||||
|
||||
<!-- 带参数的翻译 -->
|
||||
<span data-i18n="upload.count" data-i18n-params='{"count": "10"}'>共 10 个配置文件</span>
|
||||
```
|
||||
|
||||
### 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
|
||||
<div class="form-group">
|
||||
<label data-i18n="config.apiKey">API密钥</label>
|
||||
<input
|
||||
type="password"
|
||||
id="apiKey"
|
||||
class="form-control"
|
||||
data-i18n="config.apiKeyPlaceholder"
|
||||
placeholder="请输入API密钥"
|
||||
>
|
||||
</div>
|
||||
```
|
||||
|
||||
对应的翻译配置:
|
||||
|
||||
```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');
|
||||
|
|
@ -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(() => {
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 = `
|
||||
<i class="fas fa-arrow-down"></i>
|
||||
自动滚动: ${newAutoScroll ? '开' : '关'}
|
||||
<span data-i18n="${newAutoScroll ? 'logs.autoScroll.on' : 'logs.autoScroll.off'}">${statusText}</span>
|
||||
`;
|
||||
});
|
||||
}
|
||||
|
|
@ -89,7 +91,7 @@ function initEventListeners() {
|
|||
elements.toggleAutoScrollBtn.dataset.enabled = false;
|
||||
elements.toggleAutoScrollBtn.innerHTML = `
|
||||
<i class="fas fa-arrow-down"></i>
|
||||
自动滚动: 关
|
||||
<span data-i18n="logs.autoScroll.off">${t('logs.autoScroll.off')}</span>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
|
@ -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');
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 = '<i class="fas fa-circle"></i> 已连接';
|
||||
statusBadge.innerHTML = `<i class="fas fa-circle"></i> <span data-i18n="header.status.connected">${t('header.status.connected')}</span>`;
|
||||
} else {
|
||||
statusBadge.classList.add('error');
|
||||
icon.style.color = 'var(--danger-color)';
|
||||
statusBadge.innerHTML = '<i class="fas fa-circle"></i> 连接断开';
|
||||
statusBadge.innerHTML = `<i class="fas fa-circle"></i> <span data-i18n="header.status.disconnected">${t('header.status.disconnected')}</span>`;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
838
static/app/i18n.js
Normal file
838
static/app/i18n.js
Normal file
|
|
@ -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': '如果授权窗口重定向后显示“无法访问”,请将该窗口地址栏的 <strong>完整 URL</strong> 粘贴到下方:',
|
||||
'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': '<strong>建议:</strong>请先在配置管理中解除文件引用后再删除。',
|
||||
'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 <strong>Full URL</strong> 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': '<strong>Advice:</strong> 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
|
||||
};
|
||||
105
static/app/language-switcher.js
Normal file
105
static/app/language-switcher.js
Normal file
|
|
@ -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 = `
|
||||
<button class="language-btn" id="languageBtn" aria-label="切换语言">
|
||||
<i class="fas fa-globe"></i>
|
||||
<span class="current-lang">${currentLang === 'zh-CN' ? '中文' : 'EN'}</span>
|
||||
</button>
|
||||
<div class="language-dropdown" id="languageDropdown">
|
||||
<button class="language-option ${currentLang === 'zh-CN' ? 'active' : ''}" data-lang="zh-CN">
|
||||
<i class="fas fa-check"></i>
|
||||
<span>简体中文</span>
|
||||
</button>
|
||||
<button class="language-option ${currentLang === 'en-US' ? 'active' : ''}" data-lang="en-US">
|
||||
<i class="fas fa-check"></i>
|
||||
<span>English</span>
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
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
|
||||
};
|
||||
|
|
@ -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 = `
|
||||
<div class="provider-modal-content">
|
||||
<div class="provider-modal-header">
|
||||
<h3><i class="fas fa-cogs"></i> 管理 ${providerType} 提供商配置</h3>
|
||||
<h3 data-i18n="modal.provider.manage" data-i18n-params='{"type":"${providerType}"}'><i class="fas fa-cogs"></i> 管理 ${providerType} 提供商配置</h3>
|
||||
<button class="modal-close" onclick="window.closeProviderModal(this)">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
|
|
@ -50,21 +51,21 @@ function showProviderManagerModal(data) {
|
|||
<div class="provider-modal-body">
|
||||
<div class="provider-summary">
|
||||
<div class="provider-summary-item">
|
||||
<span class="label">总账户数:</span>
|
||||
<span class="label" data-i18n="modal.provider.totalAccounts">总账户数:</span>
|
||||
<span class="value">${totalCount}</span>
|
||||
</div>
|
||||
<div class="provider-summary-item">
|
||||
<span class="label">健康账户:</span>
|
||||
<span class="label" data-i18n="modal.provider.healthyAccounts">健康账户:</span>
|
||||
<span class="value">${healthyCount}</span>
|
||||
</div>
|
||||
<div class="provider-summary-actions">
|
||||
<button class="btn btn-success" onclick="window.showAddProviderForm('${providerType}')">
|
||||
<i class="fas fa-plus"></i> 添加新提供商
|
||||
<i class="fas fa-plus"></i> <span data-i18n="modal.provider.add">添加新提供商</span>
|
||||
</button>
|
||||
<button class="btn btn-warning" onclick="window.resetAllProvidersHealth('${providerType}')" title="将所有节点的健康状态重置为健康">
|
||||
<button class="btn btn-warning" onclick="window.resetAllProvidersHealth('${providerType}')" data-i18n="modal.provider.resetHealth" title="将所有节点的健康状态重置为健康">
|
||||
<i class="fas fa-heartbeat"></i> 重置为健康
|
||||
</button>
|
||||
<button class="btn btn-info" onclick="window.performHealthCheck('${providerType}')" title="对所有节点执行健康检测">
|
||||
<button class="btn btn-info" onclick="window.performHealthCheck('${providerType}')" data-i18n="modal.provider.healthCheck" title="对所有节点执行健康检测">
|
||||
<i class="fas fa-stethoscope"></i> 健康检测
|
||||
</button>
|
||||
</div>
|
||||
|
|
@ -135,7 +136,7 @@ function renderPagination(page, totalPages, totalItems, position = 'top') {
|
|||
return `
|
||||
<div class="pagination-container ${position}" data-position="${position}">
|
||||
<div class="pagination-info">
|
||||
显示 ${startItem}-${endItem} / 共 ${totalItems} 条
|
||||
<span data-i18n="pagination.showing" data-i18n-params='{"start":"${startItem}","end":"${endItem}","total":"${totalItems}"}'>显示 ${startItem}-${endItem} / 共 ${totalItems} 条</span>
|
||||
</div>
|
||||
<div class="pagination-controls">
|
||||
<button class="page-btn nav-btn" onclick="window.goToProviderPage(${page - 1})" ${page <= 1 ? 'disabled' : ''}>
|
||||
|
|
@ -147,11 +148,11 @@ function renderPagination(page, totalPages, totalItems, position = 'top') {
|
|||
</button>
|
||||
</div>
|
||||
<div class="pagination-jump">
|
||||
<span>跳转到</span>
|
||||
<input type="number" min="1" max="${totalPages}" value="${page}"
|
||||
<span data-i18n="pagination.jumpTo">跳转到</span>
|
||||
<input type="number" min="1" max="${totalPages}" value="${page}"
|
||||
onkeypress="if(event.key==='Enter')window.goToProviderPage(parseInt(this.value))"
|
||||
class="page-jump-input">
|
||||
<span>页</span>
|
||||
<span data-i18n="pagination.page">页</span>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
|
@ -250,7 +251,7 @@ async function loadModelsForProviderType(providerType, providers) {
|
|||
providers.forEach(provider => {
|
||||
const container = document.querySelector(`.not-supported-models-container[data-uuid="${provider.uuid}"]`);
|
||||
if (container) {
|
||||
container.innerHTML = '<div class="error-message">加载模型列表失败</div>';
|
||||
container.innerHTML = `<div class="error-message">${t('common.error')}: 加载模型列表失败</div>`;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
@ -351,16 +352,16 @@ function renderProviderList(providers) {
|
|||
return providers.map(provider => {
|
||||
const isHealthy = provider.isHealthy;
|
||||
const isDisabled = provider.isDisabled || false;
|
||||
const lastUsed = provider.lastUsed ? new Date(provider.lastUsed).toLocaleString() : '从未使用';
|
||||
const lastHealthCheckTime = provider.lastHealthCheckTime ? new Date(provider.lastHealthCheckTime).toLocaleString() : '从未检测';
|
||||
const lastUsed = provider.lastUsed ? new Date(provider.lastUsed).toLocaleString() : t('modal.provider.neverUsed');
|
||||
const lastHealthCheckTime = provider.lastHealthCheckTime ? new Date(provider.lastHealthCheckTime).toLocaleString() : t('modal.provider.neverChecked');
|
||||
const lastHealthCheckModel = provider.lastHealthCheckModel || '-';
|
||||
const healthClass = isHealthy ? 'healthy' : 'unhealthy';
|
||||
const disabledClass = isDisabled ? 'disabled' : '';
|
||||
const healthIcon = isHealthy ? 'fas fa-check-circle text-success' : 'fas fa-exclamation-triangle text-warning';
|
||||
const healthText = isHealthy ? '正常' : '异常';
|
||||
const disabledText = isDisabled ? '已禁用' : '已启用';
|
||||
const healthText = isHealthy ? t('modal.provider.status.healthy') : t('modal.provider.status.unhealthy');
|
||||
const disabledText = isDisabled ? t('modal.provider.status.disabled') : t('modal.provider.status.enabled');
|
||||
const disabledIcon = isDisabled ? 'fas fa-ban text-muted' : 'fas fa-play text-success';
|
||||
const toggleButtonText = isDisabled ? '启用' : '禁用';
|
||||
const toggleButtonText = isDisabled ? t('modal.provider.enabled') : t('modal.provider.disabled');
|
||||
const toggleButtonIcon = isDisabled ? 'fas fa-play' : 'fas fa-ban';
|
||||
const toggleButtonClass = isDisabled ? 'btn-success' : 'btn-warning';
|
||||
|
||||
|
|
@ -371,7 +372,7 @@ function renderProviderList(providers) {
|
|||
errorInfoHtml = `
|
||||
<div class="provider-error-info">
|
||||
<i class="fas fa-exclamation-circle text-danger"></i>
|
||||
<span class="error-label">最后错误:</span>
|
||||
<span class="error-label" data-i18n="modal.provider.lastError">最后错误:</span>
|
||||
<span class="error-message" title="${escapedErrorMsg}">${escapedErrorMsg}</span>
|
||||
</div>
|
||||
`;
|
||||
|
|
@ -385,24 +386,24 @@ function renderProviderList(providers) {
|
|||
<div class="provider-meta">
|
||||
<span class="health-status">
|
||||
<i class="${healthIcon}"></i>
|
||||
健康状态: ${healthText}
|
||||
<span data-i18n="modal.provider.healthCheckLabel">健康状态</span>: <span data-i18n="${isHealthy ? 'modal.provider.status.healthy' : 'modal.provider.status.unhealthy'}">${healthText}</span>
|
||||
</span> |
|
||||
<span class="disabled-status">
|
||||
<i class="${disabledIcon}"></i>
|
||||
状态: ${disabledText}
|
||||
<span data-i18n="upload.detail.status">状态</span>: <span data-i18n="${isDisabled ? 'modal.provider.status.disabled' : 'modal.provider.status.enabled'}">${disabledText}</span>
|
||||
</span> |
|
||||
使用次数: ${provider.usageCount || 0} |
|
||||
失败次数: ${provider.errorCount || 0} |
|
||||
最后使用: ${lastUsed}
|
||||
<span data-i18n="modal.provider.usageCount">使用次数</span>: ${provider.usageCount || 0} |
|
||||
<span data-i18n="modal.provider.errorCount">失败次数</span>: ${provider.errorCount || 0} |
|
||||
<span data-i18n="modal.provider.lastUsed">最后使用</span>: ${lastUsed}
|
||||
</div>
|
||||
<div class="provider-health-meta">
|
||||
<span class="health-check-time">
|
||||
<i class="fas fa-clock"></i>
|
||||
最后检测: ${lastHealthCheckTime}
|
||||
<span data-i18n="modal.provider.lastCheck">最后检测</span>: ${lastHealthCheckTime}
|
||||
</span> |
|
||||
<span class="health-check-model">
|
||||
<i class="fas fa-cube"></i>
|
||||
检测模型: ${lastHealthCheckModel}
|
||||
<span data-i18n="modal.provider.checkModel">检测模型</span>: ${lastHealthCheckModel}
|
||||
</span>
|
||||
</div>
|
||||
${errorInfoHtml}
|
||||
|
|
@ -412,10 +413,10 @@ function renderProviderList(providers) {
|
|||
<i class="${toggleButtonIcon}"></i> ${toggleButtonText}
|
||||
</button>
|
||||
<button class="btn-small btn-edit" onclick="window.editProvider('${provider.uuid}', event)">
|
||||
<i class="fas fa-edit"></i> 编辑
|
||||
<i class="fas fa-edit"></i> <span data-i18n="modal.provider.edit">编辑</span>
|
||||
</button>
|
||||
<button class="btn-small btn-delete" onclick="window.deleteProvider('${provider.uuid}', event)">
|
||||
<i class="fas fa-trash"></i> 删除
|
||||
<i class="fas fa-trash"></i> <span data-i18n="modal.provider.delete">删除</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -478,8 +479,8 @@ function renderProviderConfig(provider) {
|
|||
data-config-key="${fieldKey}"
|
||||
data-config-value="${actualValue}"
|
||||
disabled>
|
||||
<option value="true" ${isEnabled ? 'selected' : ''}>启用</option>
|
||||
<option value="false" ${!isEnabled ? 'selected' : ''}>禁用</option>
|
||||
<option value="true" ${isEnabled ? 'selected' : ''} data-i18n="modal.provider.enabled">启用</option>
|
||||
<option value="false" ${!isEnabled ? 'selected' : ''} data-i18n="modal.provider.disabled">禁用</option>
|
||||
</select>
|
||||
</div>
|
||||
`;
|
||||
|
|
@ -633,12 +634,12 @@ function renderProviderConfig(provider) {
|
|||
html += `
|
||||
<div class="config-item not-supported-models-section">
|
||||
<label>
|
||||
<i class="fas fa-ban"></i> 不支持的模型
|
||||
<span class="help-text">选择此提供商不支持的模型,系统会自动排除这些模型</span>
|
||||
<i class="fas fa-ban"></i> <span data-i18n="modal.provider.unsupportedModels">不支持的模型</span>
|
||||
<span class="help-text" data-i18n="modal.provider.unsupportedModelsHelp">选择此提供商不支持的模型,系统会自动排除这些模型</span>
|
||||
</label>
|
||||
<div class="not-supported-models-container" data-uuid="${provider.uuid}">
|
||||
<div class="models-loading">
|
||||
<i class="fas fa-spinner fa-spin"></i> 加载模型列表...
|
||||
<i class="fas fa-spinner fa-spin"></i> <span data-i18n="modal.provider.loadingModels">加载模型列表...</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -790,10 +791,10 @@ function editProvider(uuid, event) {
|
|||
<i class="${toggleButtonIcon}"></i> ${toggleButtonText}
|
||||
</button>
|
||||
<button class="btn-small btn-save" onclick="window.saveProvider('${uuid}', event)">
|
||||
<i class="fas fa-save"></i> 保存
|
||||
<i class="fas fa-save"></i> <span data-i18n="modal.provider.save">保存</span>
|
||||
</button>
|
||||
<button class="btn-small btn-cancel" onclick="window.cancelEdit('${uuid}', event)">
|
||||
<i class="fas fa-times"></i> 取消
|
||||
<i class="fas fa-times"></i> <span data-i18n="modal.provider.cancel">取消</span>
|
||||
</button>
|
||||
`;
|
||||
}, 100);
|
||||
|
|
@ -857,10 +858,10 @@ function cancelEdit(uuid, event) {
|
|||
<i class="${toggleButtonIcon}"></i> ${toggleButtonText}
|
||||
</button>
|
||||
<button class="btn-small btn-edit" onclick="window.editProvider('${uuid}', event)">
|
||||
<i class="fas fa-edit"></i> 编辑
|
||||
<i class="fas fa-edit"></i> <span data-i18n="modal.provider.edit">编辑</span>
|
||||
</button>
|
||||
<button class="btn-small btn-delete" onclick="window.deleteProvider('${uuid}', event)">
|
||||
<i class="fas fa-trash"></i> 删除
|
||||
<i class="fas fa-trash"></i> <span data-i18n="modal.provider.delete">删除</span>
|
||||
</button>
|
||||
`;
|
||||
}
|
||||
|
|
@ -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 = `
|
||||
<h4><i class="fas fa-plus"></i> 添加新提供商配置</h4>
|
||||
<h4 data-i18n="modal.provider.addTitle"><i class="fas fa-plus"></i> 添加新提供商配置</h4>
|
||||
<div class="form-grid">
|
||||
<div class="form-group">
|
||||
<label>自定义名称 <span class="optional-mark">(选填)</span></label>
|
||||
<input type="text" id="newCustomName" placeholder="例如: 我的节点1">
|
||||
<label><span data-i18n="modal.provider.customName">自定义名称</span> <span class="optional-mark" data-i18n="config.optional">(选填)</span></label>
|
||||
<input type="text" id="newCustomName" data-i18n="modal.provider.customName" placeholder="例如: 我的节点1">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>检查模型名称 <span class="optional-mark">(选填)</span></label>
|
||||
<input type="text" id="newCheckModelName" placeholder="例如: gpt-3.5-turbo">
|
||||
<label><span data-i18n="modal.provider.checkModelName">检查模型名称</span> <span class="optional-mark" data-i18n="config.optional">(选填)</span></label>
|
||||
<input type="text" id="newCheckModelName" data-i18n="modal.provider.checkModelName" placeholder="例如: gpt-3.5-turbo">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>健康检查</label>
|
||||
<label data-i18n="modal.provider.healthCheckLabel">健康检查</label>
|
||||
<select id="newCheckHealth">
|
||||
<option value="false">禁用</option>
|
||||
<option value="true">启用</option>
|
||||
<option value="false" data-i18n="modal.provider.disabled">禁用</option>
|
||||
<option value="true" data-i18n="modal.provider.enabled">启用</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -1054,10 +1055,10 @@ function showAddProviderForm(providerType) {
|
|||
</div>
|
||||
<div class="form-actions" style="margin-top: 15px;">
|
||||
<button class="btn btn-success" onclick="window.addProvider('${providerType}')">
|
||||
<i class="fas fa-save"></i> 保存
|
||||
<i class="fas fa-save"></i> <span data-i18n="modal.provider.save">保存</span>
|
||||
</button>
|
||||
<button class="btn btn-secondary" onclick="this.closest('.add-provider-form').remove()">
|
||||
<i class="fas fa-times"></i> 取消
|
||||
<i class="fas fa-times"></i> <span data-i18n="modal.provider.cancel">取消</span>
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
|
|
@ -1184,7 +1185,7 @@ function addDynamicConfigFields(form, providerType) {
|
|||
fields += '</div>';
|
||||
}
|
||||
} else {
|
||||
fields = '<p>不支持的提供商类型</p>';
|
||||
fields = `<p data-i18n="modal.provider.noProviderType">${t('modal.provider.noProviderType')}</p>`;
|
||||
}
|
||||
|
||||
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 = '<div class="no-models">该提供商类型暂无可用模型列表</div>';
|
||||
container.innerHTML = `<div class="no-models" data-i18n="modal.provider.noModels">${t('modal.provider.noModels')}</div>`;
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 = `
|
||||
<div class="provider-header">
|
||||
|
|
@ -187,19 +188,19 @@ function renderProviders(providers) {
|
|||
</div>
|
||||
<div class="provider-stats">
|
||||
<div class="provider-stat">
|
||||
<span class="provider-stat-label">总账户</span>
|
||||
<span class="provider-stat-label" data-i18n="providers.stat.totalAccounts">${t('providers.stat.totalAccounts')}</span>
|
||||
<span class="provider-stat-value">${totalCount}</span>
|
||||
</div>
|
||||
<div class="provider-stat">
|
||||
<span class="provider-stat-label">健康账户</span>
|
||||
<span class="provider-stat-label" data-i18n="providers.stat.healthyAccounts">${t('providers.stat.healthyAccounts')}</span>
|
||||
<span class="provider-stat-value">${healthyCount}</span>
|
||||
</div>
|
||||
<div class="provider-stat">
|
||||
<span class="provider-stat-label">使用次数</span>
|
||||
<span class="provider-stat-label" data-i18n="providers.stat.usageCount">${t('providers.stat.usageCount')}</span>
|
||||
<span class="provider-stat-value">${usageCount}</span>
|
||||
</div>
|
||||
<div class="provider-stat">
|
||||
<span class="provider-stat-label">错误次数</span>
|
||||
<span class="provider-stat-label" data-i18n="providers.stat.errorCount">${t('providers.stat.errorCount')}</span>
|
||||
<span class="provider-stat-value">${errorCount}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -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 `
|
||||
<button class="generate-auth-btn" title="生成OAuth授权链接">
|
||||
<i class="fas fa-key"></i>
|
||||
<span>生成授权</span>
|
||||
<span data-i18n="providers.auth.generate">${t('providers.auth.generate')}</span>
|
||||
</button>
|
||||
`;
|
||||
}
|
||||
|
|
@ -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 = `
|
||||
<div class="auth-instructions">
|
||||
<h4>授权步骤:</h4>
|
||||
<h4 data-i18n="oauth.modal.steps">${t('oauth.modal.steps')}</h4>
|
||||
<ol>
|
||||
<li>点击下方按钮在浏览器中打开授权页面</li>
|
||||
<li>完成授权后,系统会自动获取凭据文件</li>
|
||||
<li>凭据文件可在上传配置管理中查看和管理</li>
|
||||
<li>授权有效期: ${Math.floor(authInfo.expiresIn / 60)} 分钟</li>
|
||||
<li data-i18n="oauth.modal.step1">${t('oauth.modal.step1')}</li>
|
||||
<li data-i18n="oauth.modal.step2.qwen">${t('oauth.modal.step2.qwen')}</li>
|
||||
<li data-i18n="oauth.modal.step3">${t('oauth.modal.step3')}</li>
|
||||
<li data-i18n="oauth.modal.step4.qwen" data-i18n-params='{"min":"${Math.floor(authInfo.expiresIn / 60)}"}'>${t('oauth.modal.step4.qwen', { min: Math.floor(authInfo.expiresIn / 60) })}</li>
|
||||
</ol>
|
||||
</div>
|
||||
`;
|
||||
} else {
|
||||
instructionsHtml = `
|
||||
<div class="auth-instructions">
|
||||
<h4>授权步骤:</h4>
|
||||
<h4 data-i18n="oauth.modal.steps">${t('oauth.modal.steps')}</h4>
|
||||
<ol>
|
||||
<li>点击下方按钮在浏览器中打开授权页面</li>
|
||||
<li>使用您的Google账号登录并授权</li>
|
||||
<li>授权完成后,凭据文件会自动保存</li>
|
||||
<li>凭据文件可在上传配置管理中查看和管理</li>
|
||||
<li data-i18n="oauth.modal.step1">${t('oauth.modal.step1')}</li>
|
||||
<li data-i18n="oauth.modal.step2.google">${t('oauth.modal.step2.google')}</li>
|
||||
<li data-i18n="oauth.modal.step4.google">${t('oauth.modal.step4.google')}</li>
|
||||
<li data-i18n="oauth.modal.step3">${t('oauth.modal.step3')}</li>
|
||||
</ol>
|
||||
</div>
|
||||
`;
|
||||
|
|
@ -424,18 +425,18 @@ function showAuthModal(authUrl, authInfo) {
|
|||
modal.innerHTML = `
|
||||
<div class="modal-content" style="max-width: 600px;">
|
||||
<div class="modal-header">
|
||||
<h3><i class="fas fa-key"></i>OAuth 授权</h3>
|
||||
<h3><i class="fas fa-key"></i> <span data-i18n="oauth.modal.title">${t('oauth.modal.title')}</span></h3>
|
||||
<button class="modal-close">×</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="auth-info">
|
||||
<p><strong>提供商:</strong> ${authInfo.provider}</p>
|
||||
<p><strong data-i18n="oauth.modal.provider">${t('oauth.modal.provider')}</strong> ${authInfo.provider}</p>
|
||||
${instructionsHtml}
|
||||
<div class="auth-url-section">
|
||||
<label>授权链接:</label>
|
||||
<label data-i18n="oauth.modal.urlLabel">${t('oauth.modal.urlLabel')}</label>
|
||||
<div class="auth-url-container">
|
||||
<input type="text" readonly value="${authUrl}" class="auth-url-input">
|
||||
<button class="copy-btn" title="复制链接">
|
||||
<button class="copy-btn" data-i18n="oauth.modal.copyTitle" title="复制链接">
|
||||
<i class="fas fa-copy"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
|
@ -443,10 +444,10 @@ function showAuthModal(authUrl, authInfo) {
|
|||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="modal-cancel">关闭</button>
|
||||
<button class="modal-cancel" data-i18n="modal.provider.cancel">${t('modal.provider.cancel')}</button>
|
||||
<button class="open-auth-btn">
|
||||
<i class="fas fa-external-link-alt"></i>
|
||||
在浏览器中打开
|
||||
<span data-i18n="oauth.modal.openInBrowser">${t('oauth.modal.openInBrowser')}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -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 = `
|
||||
<div class="manual-callback-section" style="margin-top: 20px; padding: 15px; background: #fffbeb; border: 1px solid #fef3c7; border-radius: 8px;">
|
||||
<h4 style="color: #92400e; margin-bottom: 8px;"><i class="fas fa-exclamation-circle"></i> 自动监听受阻?</h4>
|
||||
<p style="font-size: 0.875rem; color: #b45309; margin-bottom: 10px;">如果授权窗口重定向后显示“无法访问”,请将该窗口地址栏的 <strong>完整 URL</strong> 粘贴到下方:</p>
|
||||
<h4 style="color: #92400e; margin-bottom: 8px;"><i class="fas fa-exclamation-circle"></i> <span data-i18n="oauth.manual.title">${t('oauth.manual.title')}</span></h4>
|
||||
<p style="font-size: 0.875rem; color: #b45309; margin-bottom: 10px;" data-i18n-html="oauth.manual.desc">${t('oauth.manual.desc')}</p>
|
||||
<div class="auth-url-container" style="display: flex; gap: 5px;">
|
||||
<input type="text" class="manual-callback-input" placeholder="粘贴回调 URL (包含 code=...)" style="flex: 1; padding: 8px; border: 1px solid #fcd34d; border-radius: 4px; background: white; color: black;">
|
||||
<input type="text" class="manual-callback-input" data-i18n="oauth.manual.placeholder" placeholder="粘贴回调 URL (包含 code=...)" style="flex: 1; padding: 8px; border: 1px solid #fcd34d; border-radius: 4px; background: white; color: black;">
|
||||
<button class="btn btn-success apply-callback-btn" style="padding: 8px 15px; white-space: nowrap; background: #059669; color: white; border: none; border-radius: 4px; cursor: pointer;">
|
||||
<i class="fas fa-check"></i> 提交
|
||||
<i class="fas fa-check"></i> <span data-i18n="oauth.manual.submit">${t('oauth.manual.submit')}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -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');
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 = '<div class="no-configs"><p>未找到匹配的配置文件</p></div>';
|
||||
container.innerHTML = `<div class="no-configs"><p data-i18n="upload.noConfigs">${t('upload.noConfigs')}</p></div>`;
|
||||
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) {
|
|||
<div class="config-item-modified">${formatDate(config.modified)}</div>
|
||||
<div class="config-item-status">
|
||||
<i class="fas ${statusIcon}"></i>
|
||||
${statusText}
|
||||
<span data-i18n="${config.isUsed ? 'upload.statusFilter.used' : 'upload.statusFilter.unused'}">${statusText}</span>
|
||||
${quickLinkBtnHtml}
|
||||
</div>
|
||||
</div>
|
||||
<div class="config-item-details">
|
||||
<div class="config-details-grid">
|
||||
<div class="config-detail-item">
|
||||
<div class="config-detail-label">文件路径</div>
|
||||
<div class="config-detail-label" data-i18n="upload.detail.path">文件路径</div>
|
||||
<div class="config-detail-value">${config.path}</div>
|
||||
</div>
|
||||
<div class="config-detail-item">
|
||||
<div class="config-detail-label">文件大小</div>
|
||||
<div class="config-detail-label" data-i18n="upload.detail.size">文件大小</div>
|
||||
<div class="config-detail-value">${formatFileSize(config.size)}</div>
|
||||
</div>
|
||||
<div class="config-detail-item">
|
||||
<div class="config-detail-label">最后修改</div>
|
||||
<div class="config-detail-label" data-i18n="upload.detail.modified">最后修改</div>
|
||||
<div class="config-detail-value">${formatDate(config.modified)}</div>
|
||||
</div>
|
||||
<div class="config-detail-item">
|
||||
<div class="config-detail-label">关联状态</div>
|
||||
<div class="config-detail-value">${statusText}</div>
|
||||
<div class="config-detail-label" data-i18n="upload.detail.status">关联状态</div>
|
||||
<div class="config-detail-value" data-i18n="${config.isUsed ? 'upload.statusFilter.used' : 'upload.statusFilter.unused'}">${statusText}</div>
|
||||
</div>
|
||||
</div>
|
||||
${usageInfoHtml}
|
||||
<div class="config-item-actions">
|
||||
<button class="btn-small btn-view" data-path="${config.path}">
|
||||
<i class="fas fa-eye"></i> 查看
|
||||
<i class="fas fa-eye"></i> <span data-i18n="upload.action.view">${t('upload.action.view')}</span>
|
||||
</button>
|
||||
<button class="btn-small btn-delete-small" data-path="${config.path}">
|
||||
<i class="fas fa-trash"></i> 删除
|
||||
<i class="fas fa-trash"></i> <span data-i18n="upload.action.delete">${t('upload.action.delete')}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -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 += `
|
||||
<div class="usage-detail-item" data-usage-type="${usageTypeKey}">
|
||||
<i class="fas ${icon}"></i>
|
||||
|
|
@ -210,7 +212,7 @@ function generateUsageInfoHtml(config) {
|
|||
<div class="config-usage-info">
|
||||
<div class="usage-info-header">
|
||||
<i class="fas fa-link"></i>
|
||||
<span class="usage-info-title">关联详情 (${typeLabel})</span>
|
||||
<span class="usage-info-title" data-i18n="upload.usage.title" data-i18n-params='{"type":"${typeLabel}"}'>关联详情 (${typeLabel})</span>
|
||||
</div>
|
||||
<div class="usage-details-list">
|
||||
${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 = `
|
||||
<div class="config-modal-content">
|
||||
<div class="config-modal-header">
|
||||
<h3>配置文件: ${fileData.name}</h3>
|
||||
<h3><span data-i18n="nav.config">${t('nav.config')}</span>: ${fileData.name}</h3>
|
||||
<button class="modal-close">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
|
|
@ -398,27 +409,27 @@ function showConfigModal(fileData) {
|
|||
<div class="config-modal-body">
|
||||
<div class="config-file-info">
|
||||
<div class="file-info-item">
|
||||
<span class="info-label">文件路径:</span>
|
||||
<span class="info-label" data-i18n="upload.detail.path">${t('upload.detail.path')}:</span>
|
||||
<span class="info-value">${fileData.path}</span>
|
||||
</div>
|
||||
<div class="file-info-item">
|
||||
<span class="info-label">文件大小:</span>
|
||||
<span class="info-label" data-i18n="upload.detail.size">${t('upload.detail.size')}:</span>
|
||||
<span class="info-value">${formatFileSize(fileData.size)}</span>
|
||||
</div>
|
||||
<div class="file-info-item">
|
||||
<span class="info-label">最后修改:</span>
|
||||
<span class="info-label" data-i18n="upload.detail.modified">${t('upload.detail.modified')}:</span>
|
||||
<span class="info-value">${formatDate(fileData.modified)}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="config-content">
|
||||
<label>文件内容:</label>
|
||||
<label data-i18n="common.info">文件内容:</label>
|
||||
<pre class="config-content-display">${escapeHtml(fileData.content)}</pre>
|
||||
</div>
|
||||
</div>
|
||||
<div class="config-modal-footer">
|
||||
<button class="btn btn-secondary btn-close-modal">关闭</button>
|
||||
<button class="btn btn-secondary btn-close-modal" data-i18n="modal.provider.cancel">${t('modal.provider.cancel')}</button>
|
||||
<button class="btn btn-primary btn-copy-content" data-path="${fileData.path}">
|
||||
<i class="fas fa-copy"></i> 复制内容
|
||||
<i class="fas fa-copy"></i> <span data-i18n="oauth.modal.copyTitle">${t('oauth.modal.copyTitle')}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -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 = `
|
||||
<div class="delete-modal-content">
|
||||
<div class="delete-modal-header">
|
||||
<h3><i class="${icon}"></i> ${title}</h3>
|
||||
<h3 data-i18n="${isUsed ? 'upload.delete.confirmTitleUsed' : 'upload.delete.confirmTitle'}"><i class="${icon}"></i> ${title}</h3>
|
||||
<button class="modal-close">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
|
|
@ -547,29 +558,29 @@ function showDeleteConfirmModal(config) {
|
|||
</div>
|
||||
<div class="warning-content">
|
||||
${isUsed ?
|
||||
'<h4>⚠️ 此配置已被系统使用</h4><p>删除已关联的配置文件可能会影响系统正常运行。请确保您了解删除的后果。</p>' :
|
||||
'<h4>🗑️ 确认删除配置文件</h4><p>此操作将永久删除配置文件,且无法撤销。</p>'
|
||||
`<h4 data-i18n="upload.delete.warningUsedTitle">${t('upload.delete.warningUsedTitle')}</h4><p data-i18n="upload.delete.warningUsedDesc">${t('upload.delete.warningUsedDesc')}</p>` :
|
||||
`<h4 data-i18n="upload.delete.warningUnusedTitle">${t('upload.delete.warningUnusedTitle')}</h4><p data-i18n="upload.delete.warningUnusedDesc">${t('upload.delete.warningUnusedDesc')}</p>`
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="config-info">
|
||||
<div class="config-info-item">
|
||||
<span class="info-label">文件名:</span>
|
||||
<span class="info-label" data-i18n="upload.delete.fileName">文件名:</span>
|
||||
<span class="info-value">${config.name}</span>
|
||||
</div>
|
||||
<div class="config-info-item">
|
||||
<span class="info-label">文件路径:</span>
|
||||
<span class="info-label" data-i18n="upload.detail.path">文件路径:</span>
|
||||
<span class="info-value">${config.path}</span>
|
||||
</div>
|
||||
<div class="config-info-item">
|
||||
<span class="info-label">文件大小:</span>
|
||||
<span class="info-label" data-i18n="upload.detail.size">文件大小:</span>
|
||||
<span class="info-value">${formatFileSize(config.size)}</span>
|
||||
</div>
|
||||
<div class="config-info-item">
|
||||
<span class="info-label">关联状态:</span>
|
||||
<span class="info-value status-${isUsed ? 'used' : 'unused'}">
|
||||
${isUsed ? '已关联' : '未关联'}
|
||||
<span class="info-label" data-i18n="upload.detail.status">关联状态:</span>
|
||||
<span class="info-value status-${isUsed ? 'used' : 'unused'}" data-i18n="${isUsed ? 'upload.statusFilter.used' : 'upload.statusFilter.unused'}">
|
||||
${isUsed ? t('upload.statusFilter.used') : t('upload.statusFilter.unused')}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -580,23 +591,23 @@ function showDeleteConfirmModal(config) {
|
|||
<i class="fas fa-info-circle"></i>
|
||||
</div>
|
||||
<div class="alert-content">
|
||||
<h5>关联详情</h5>
|
||||
<p>此配置文件正在被系统使用,删除后可能会导致:</p>
|
||||
<h5 data-i18n="upload.delete.usageAlertTitle">${t('upload.delete.usageAlertTitle')}</h5>
|
||||
<p data-i18n="upload.delete.usageAlertDesc">${t('upload.delete.usageAlertDesc')}</p>
|
||||
<ul>
|
||||
<li>相关的AI服务无法正常工作</li>
|
||||
<li>配置管理中的设置失效</li>
|
||||
<li>提供商池配置丢失</li>
|
||||
<li data-i18n="upload.delete.usageAlertItem1">${t('upload.delete.usageAlertItem1')}</li>
|
||||
<li data-i18n="upload.delete.usageAlertItem2">${t('upload.delete.usageAlertItem2')}</li>
|
||||
<li data-i18n="upload.delete.usageAlertItem3">${t('upload.delete.usageAlertItem3')}</li>
|
||||
</ul>
|
||||
<p><strong>建议:</strong>请先在配置管理中解除文件引用后再删除。</p>
|
||||
<p data-i18n-html="upload.delete.usageAlertAdvice">${t('upload.delete.usageAlertAdvice')}</p>
|
||||
</div>
|
||||
</div>
|
||||
` : ''}
|
||||
</div>
|
||||
<div class="delete-modal-footer">
|
||||
<button class="btn btn-secondary btn-cancel-delete">取消</button>
|
||||
<button class="btn btn-secondary btn-cancel-delete" data-i18n="modal.provider.cancel">${t('modal.provider.cancel')}</button>
|
||||
<button class="${buttonClass} btn-confirm-delete" data-path="${config.path}">
|
||||
<i class="fas fa-${isUsed ? 'exclamation-triangle' : 'trash'}"></i>
|
||||
${isUsed ? '强制删除' : '确认删除'}
|
||||
<span data-i18n="${isUsed ? 'upload.delete.forceDelete' : 'upload.delete.confirmDelete'}">${isUsed ? t('upload.delete.forceDelete') : t('upload.delete.confirmDelete')}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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 = `
|
||||
<div class="usage-empty">
|
||||
<i class="fas fa-chart-bar"></i>
|
||||
<p>暂无用量数据</p>
|
||||
<p data-i18n="usage.noData">${t('usage.noData')}</p>
|
||||
</div>
|
||||
`;
|
||||
return;
|
||||
|
|
@ -180,7 +189,7 @@ function renderUsageData(data, container) {
|
|||
container.innerHTML = `
|
||||
<div class="usage-empty">
|
||||
<i class="fas fa-chart-bar"></i>
|
||||
<p>暂无已初始化的服务实例</p>
|
||||
<p data-i18n="usage.noInstances">${t('usage.noInstances')}</p>
|
||||
</div>
|
||||
`;
|
||||
return;
|
||||
|
|
@ -216,8 +225,8 @@ function createProviderGroup(providerType, instances) {
|
|||
<i class="fas fa-chevron-right toggle-icon"></i>
|
||||
<i class="${providerIcon} provider-icon"></i>
|
||||
<span class="provider-name">${providerDisplayName}</span>
|
||||
<span class="instance-count">${instanceCount} 个实例</span>
|
||||
<span class="success-count ${successCount === instanceCount ? 'all-success' : ''}">${successCount}/${instanceCount} 成功</span>
|
||||
<span class="instance-count" data-i18n="usage.group.instances" data-i18n-params='{"count":"${instanceCount}"}'>${t('usage.group.instances', { count: instanceCount })}</span>
|
||||
<span class="success-count ${successCount === instanceCount ? 'all-success' : ''}" data-i18n="usage.group.success" data-i18n-params='{"count":"${successCount}","total":"${instanceCount}"}'>${t('usage.group.success', { count: successCount, total: instanceCount })}</span>
|
||||
</div>
|
||||
`;
|
||||
|
||||
|
|
@ -268,10 +277,10 @@ function createInstanceUsageCard(instance, providerType) {
|
|||
: '<i class="fas fa-times-circle status-error"></i>';
|
||||
|
||||
const healthBadge = instance.isDisabled
|
||||
? '<span class="badge badge-disabled">已禁用</span>'
|
||||
? `<span class="badge badge-disabled" data-i18n="usage.card.status.disabled">${t('usage.card.status.disabled')}</span>`
|
||||
: (instance.isHealthy
|
||||
? '<span class="badge badge-healthy">健康</span>'
|
||||
: '<span class="badge badge-unhealthy">异常</span>');
|
||||
? `<span class="badge badge-healthy" data-i18n="usage.card.status.healthy">${t('usage.card.status.healthy')}</span>`
|
||||
: `<span class="badge badge-unhealthy" data-i18n="usage.card.status.unhealthy">${t('usage.card.status.unhealthy')}</span>`);
|
||||
|
||||
// 获取用户邮箱和订阅信息
|
||||
const userEmail = instance.usage?.user?.email || '';
|
||||
|
|
@ -343,7 +352,7 @@ function renderUsageDetails(usage) {
|
|||
|
||||
totalSection.innerHTML = `
|
||||
<div class="total-usage-header">
|
||||
<span class="total-label"><i class="fas fa-chart-pie"></i> 总用量</span>
|
||||
<span class="total-label"><i class="fas fa-chart-pie"></i> <span data-i18n="usage.card.totalUsage">${t('usage.card.totalUsage')}</span></span>
|
||||
<span class="total-value">${formatNumber(totalUsage.used)} / ${formatNumber(totalUsage.limit)}</span>
|
||||
</div>
|
||||
<div class="progress-bar ${progressClass}">
|
||||
|
|
@ -399,9 +408,9 @@ function createUsageBreakdownHTML(breakdown) {
|
|||
if (breakdown.freeTrial && breakdown.freeTrial.status === 'ACTIVE') {
|
||||
html += `
|
||||
<div class="extra-usage-info free-trial">
|
||||
<span class="extra-label"><i class="fas fa-gift"></i> 免费试用</span>
|
||||
<span class="extra-label"><i class="fas fa-gift"></i> <span data-i18n="usage.card.freeTrial">${t('usage.card.freeTrial')}</span></span>
|
||||
<span class="extra-value">${formatNumber(breakdown.freeTrial.currentUsage)} / ${formatNumber(breakdown.freeTrial.usageLimit)}</span>
|
||||
<span class="extra-expires">到期: ${formatDate(breakdown.freeTrial.expiresAt)}</span>
|
||||
<span class="extra-expires" data-i18n="usage.card.expires" data-i18n-params='{"time":"${formatDate(breakdown.freeTrial.expiresAt)}"}'>${t('usage.card.expires', { time: formatDate(breakdown.freeTrial.expiresAt) })}</span>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
|
@ -414,7 +423,7 @@ function createUsageBreakdownHTML(breakdown) {
|
|||
<div class="extra-usage-info bonus">
|
||||
<span class="extra-label"><i class="fas fa-star"></i> ${bonus.displayName || bonus.code}</span>
|
||||
<span class="extra-value">${formatNumber(bonus.currentUsage)} / ${formatNumber(bonus.usageLimit)}</span>
|
||||
<span class="extra-expires">到期: ${formatDate(bonus.expiresAt)}</span>
|
||||
<span class="extra-expires" data-i18n="usage.card.expires" data-i18n-params='{"time":"${formatDate(bonus.expiresAt)}"}'>${t('usage.card.expires', { time: formatDate(bonus.expiresAt) })}</span>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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 = `
|
||||
<div style="font-weight: 600; margin-bottom: 4px;">${escapeHtml(title)}</div>
|
||||
<div>${escapeHtml(message)}</div>
|
||||
`;
|
||||
|
||||
|
|
@ -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 <span class="optional-tag">(选填)</span>',
|
||||
label: `Gemini Base URL <span class="optional-tag">${t('config.optional')}</span>`,
|
||||
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 <span class="optional-tag">(选填)</span>',
|
||||
label: `Base URL <span class="optional-tag">${t('config.optional')}</span>`,
|
||||
type: 'text',
|
||||
placeholder: 'https://codewhisperer.{{region}}.amazonaws.com/generateAssistantResponse'
|
||||
},
|
||||
{
|
||||
id: 'KIRO_REFRESH_URL',
|
||||
label: 'Refresh URL <span class="optional-tag">(选填)</span>',
|
||||
label: `Refresh URL <span class="optional-tag">${t('config.optional')}</span>`,
|
||||
type: 'text',
|
||||
placeholder: 'https://prod.{{region}}.auth.desktop.kiro.dev/refreshToken'
|
||||
},
|
||||
{
|
||||
id: 'KIRO_REFRESH_IDC_URL',
|
||||
label: 'Refresh IDC URL <span class="optional-tag">(选填)</span>',
|
||||
label: `Refresh IDC URL <span class="optional-tag">${t('config.optional')}</span>`,
|
||||
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 <span class="optional-tag">(选填)</span>',
|
||||
label: `Qwen Base URL <span class="optional-tag">${t('config.optional')}</span>`,
|
||||
type: 'text',
|
||||
placeholder: 'https://portal.qwen.ai/v1'
|
||||
},
|
||||
{
|
||||
id: 'QWEN_OAUTH_BASE_URL',
|
||||
label: 'OAuth Base URL <span class="optional-tag">(选填)</span>',
|
||||
label: `OAuth Base URL <span class="optional-tag">${t('config.optional')}</span>`,
|
||||
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 <span class="optional-tag">(选填)</span>',
|
||||
label: `Daily Base URL <span class="optional-tag">${t('config.optional')}</span>`,
|
||||
type: 'text',
|
||||
placeholder: 'https://daily-cloudcode-pa.sandbox.googleapis.com'
|
||||
},
|
||||
{
|
||||
id: 'ANTIGRAVITY_BASE_URL_AUTOPUSH',
|
||||
label: 'Autopush Base URL <span class="optional-tag">(选填)</span>',
|
||||
label: `Autopush Base URL <span class="optional-tag">${t('config.optional')}</span>`,
|
||||
type: 'text',
|
||||
placeholder: 'https://autopush-cloudcode-pa.sandbox.googleapis.com'
|
||||
}
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -1,9 +1,9 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<html lang="zh-CN" id="html-root">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>登录 - AIClient2API</title>
|
||||
<title data-i18n="login.title">登录 - AIClient2API</title>
|
||||
<link rel="icon" type="image/x-icon" href="/favicon.ico">
|
||||
<style>
|
||||
* {
|
||||
|
|
@ -181,30 +181,34 @@
|
|||
}
|
||||
}
|
||||
</style>
|
||||
<link rel="stylesheet" href="app/styles.css">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
||||
</head>
|
||||
<body>
|
||||
<div style="position: absolute; top: 20px; right: 20px;" id="langSwitcherContainer"></div>
|
||||
<div class="login-container">
|
||||
<div class="logo">
|
||||
<img src="/favicon.ico" alt="Logo" onerror="this.style.display='none'">
|
||||
<h1>AIClient2API</h1>
|
||||
<p>请登录以继续</p>
|
||||
<p data-i18n="login.heading">请登录以继续</p>
|
||||
</div>
|
||||
|
||||
<form id="loginForm">
|
||||
<div class="form-group">
|
||||
<label for="password">密码</label>
|
||||
<input
|
||||
type="password"
|
||||
id="password"
|
||||
name="password"
|
||||
<label for="password" data-i18n="login.password">密码</label>
|
||||
<input
|
||||
type="password"
|
||||
id="password"
|
||||
name="password"
|
||||
data-i18n="login.passwordPlaceholder"
|
||||
placeholder="请输入密码"
|
||||
autocomplete="current-password"
|
||||
required
|
||||
>
|
||||
<div class="error-message" id="errorMessage">密码错误,请重试</div>
|
||||
<div class="error-message" id="errorMessage" data-i18n="login.error.incorrect">密码错误,请重试</div>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="login-button" id="loginButton">
|
||||
<button type="submit" class="login-button" id="loginButton" data-i18n="login.button">
|
||||
登录
|
||||
</button>
|
||||
</form>
|
||||
|
|
@ -214,7 +218,48 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
<script type="module">
|
||||
import { initI18n, t } from './app/i18n.js';
|
||||
import { initLanguageSwitcher } from './app/language-switcher.js';
|
||||
|
||||
// 初始化多语言
|
||||
initI18n();
|
||||
|
||||
// 初始化语言切换器
|
||||
const langContainer = document.getElementById('langSwitcherContainer');
|
||||
if (langContainer) {
|
||||
import('./app/language-switcher.js').then(module => {
|
||||
const switcher = module.createLanguageSwitcher();
|
||||
langContainer.appendChild(switcher);
|
||||
|
||||
// 绑定事件逻辑(由于是动态创建,复用逻辑)
|
||||
const languageBtn = switcher.querySelector('#languageBtn');
|
||||
const languageDropdown = switcher.querySelector('#languageDropdown');
|
||||
const languageOptions = switcher.querySelectorAll('.language-option');
|
||||
|
||||
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');
|
||||
module.setLanguage(lang);
|
||||
switcher.querySelector('.current-lang').textContent = lang === 'zh-CN' ? '中文' : 'EN';
|
||||
languageOptions.forEach(opt => opt.classList.remove('active'));
|
||||
option.classList.add('active');
|
||||
languageDropdown.classList.remove('show');
|
||||
});
|
||||
});
|
||||
|
||||
document.addEventListener('click', () => {
|
||||
languageDropdown.classList.remove('show');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
const loginForm = document.getElementById('loginForm');
|
||||
const passwordInput = document.getElementById('password');
|
||||
const errorMessage = document.getElementById('errorMessage');
|
||||
|
|
@ -229,13 +274,13 @@
|
|||
const password = passwordInput.value.trim();
|
||||
|
||||
if (!password) {
|
||||
showError('请输入密码');
|
||||
showError(t('login.error.empty'));
|
||||
return;
|
||||
}
|
||||
|
||||
// 禁用按钮并显示加载状态
|
||||
loginButton.disabled = true;
|
||||
loginButton.innerHTML = '<span class="loading"></span>登录中...';
|
||||
loginButton.innerHTML = `<span class="loading"></span>${t('login.loggingIn')}`;
|
||||
errorMessage.classList.remove('show');
|
||||
|
||||
try {
|
||||
|
|
@ -259,17 +304,17 @@
|
|||
// 跳转到主页
|
||||
window.location.href = '/';
|
||||
} else {
|
||||
showError(data.message || '密码错误,请重试');
|
||||
showError(data.message || t('login.error.incorrect'));
|
||||
loginButton.disabled = false;
|
||||
loginButton.innerHTML = '登录';
|
||||
loginButton.innerHTML = t('login.button');
|
||||
passwordInput.value = '';
|
||||
passwordInput.focus();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('登录错误:', error);
|
||||
showError('登录失败,请检查网络连接');
|
||||
showError(t('login.error.failed'));
|
||||
loginButton.disabled = false;
|
||||
loginButton.innerHTML = '登录';
|
||||
loginButton.innerHTML = t('login.button');
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue