diff --git a/.eslintrc.cjs b/.eslintrc.cjs
new file mode 100644
index 0000000..ce9b4a6
--- /dev/null
+++ b/.eslintrc.cjs
@@ -0,0 +1,20 @@
+module.exports = {
+ root: true,
+ env: { browser: true, es2020: true },
+ extends: [
+ 'eslint:recommended',
+ 'plugin:@typescript-eslint/recommended',
+ 'plugin:react-hooks/recommended',
+ ],
+ ignorePatterns: ['dist', '.eslintrc.cjs'],
+ parser: '@typescript-eslint/parser',
+ plugins: ['react-refresh'],
+ rules: {
+ 'react-refresh/only-export-components': [
+ 'warn',
+ { allowConstantExport: true },
+ ],
+ '@typescript-eslint/no-explicit-any': 'warn',
+ '@typescript-eslint/no-unused-vars': ['warn', { argsIgnorePattern: '^_' }],
+ },
+};
diff --git a/.gitignore b/.gitignore
index 00f5013..a547bf3 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,28 +1,24 @@
-# Node modules
-node_modules/
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+lerna-debug.log*
-# Build output
-dist/
+node_modules
+dist
+dist-ssr
+*.local
-# Temporary build files
-index.build.html
-
-# npm lock files
-package-lock.json
-
-# IDE and editor files
-.vscode/
-.idea/
-*.swp
-*.swo
-*~
-
-# OS files
+# Editor directories and files
+.vscode/*
+!.vscode/extensions.json
+.idea
.DS_Store
-Thumbs.db
-
-CLAUDE.md
-.claude
-AGENTS.md
-.codex
-.serena
\ No newline at end of file
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+*.sw?
diff --git a/.prettierrc b/.prettierrc
new file mode 100644
index 0000000..59eb508
--- /dev/null
+++ b/.prettierrc
@@ -0,0 +1,9 @@
+{
+ "semi": true,
+ "trailingComma": "es5",
+ "singleQuote": true,
+ "printWidth": 100,
+ "tabWidth": 2,
+ "useTabs": false,
+ "arrowParens": "always"
+}
diff --git a/BUILD_RELEASE.md b/BUILD_RELEASE.md
deleted file mode 100644
index 6250e05..0000000
--- a/BUILD_RELEASE.md
+++ /dev/null
@@ -1,69 +0,0 @@
-# Build and Release Instructions
-
-## Overview
-
-This project uses webpack to bundle all HTML, CSS, JavaScript, and images into a single all-in-one HTML file. The GitHub workflow automatically builds and releases this file when you create a new tag.
-
-## How to Create a Release
-
-1. Make sure all your changes are committed
-2. Create and push a new tag:
- ```bash
- git tag v1.0.0
- git push origin v1.0.0
- ```
-3. The GitHub workflow will automatically:
- - Install dependencies
- - Build the all-in-one HTML file using webpack
- - Create a new release with the tag
- - Upload the bundled HTML file to the release
-
-## Manual Build
-
-To build locally:
-
-```bash
-# Install dependencies
-npm install
-
-# Build the all-in-one HTML file
-npm run build
-```
-
-The output will be in the `dist/` directory as `index.html`.
-
-## How It Works
-
-1. **build-scripts/prepare-html.js**: Pre-build script
- - Reads the original `index.html`
- - Removes local CSS and JavaScript references
- - Generates temporary `index.build.html` for webpack
-
-2. **webpack.config.js**: Configures webpack to bundle all assets
- - Uses `style-loader` to inline CSS
- - Uses `asset/inline` to embed images as base64
- - Uses `html-inline-script-webpack-plugin` to inline JavaScript
- - Uses `index.build.html` as template (generated dynamically)
-
-3. **bundle-entry.js**: Entry point that imports all resources
- - Imports CSS files
- - Imports JavaScript modules
- - Imports and sets logo image
-
-4. **package.json scripts**:
- - `prebuild`: Automatically runs before build to generate `index.build.html`
- - `build`: Runs webpack to bundle everything
- - `postbuild`: Cleans up temporary `index.build.html` file
-
-5. **.github/workflows/release.yml**: GitHub workflow
- - Triggers on tag push
- - Builds the project (prebuild → build → postbuild)
- - Creates a release with the bundled HTML file
-
-## External Dependencies
-
-The bundled HTML file still relies on these CDN resources:
-- Font Awesome (icons)
-- Chart.js (charts and graphs)
-
-These are loaded from CDN to keep the file size reasonable and leverage browser caching.
diff --git a/CLEAR_STORAGE.html b/CLEAR_STORAGE.html
new file mode 100644
index 0000000..1c0a9b8
--- /dev/null
+++ b/CLEAR_STORAGE.html
@@ -0,0 +1,123 @@
+
+
+
+ Clear LocalStorage
+
+
+
+
+
+
🧹 LocalStorage 清理工具
+
用于清理旧版本的 CLI Proxy Web UI 遗留的 localStorage 数据
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/LICENSE b/LICENSE
deleted file mode 100644
index 0f988e2..0000000
--- a/LICENSE
+++ /dev/null
@@ -1,21 +0,0 @@
-MIT License
-
-Copyright (c) 2025 Supra4E8C
-
-Permission is hereby granted, free of charge, to any person obtaining a copy
-of this software and associated documentation files (the "Software"), to deal
-in the Software without restriction, including without limitation the rights
-to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-copies of the Software, and to permit persons to whom the Software is
-furnished to do so, subject to the following conditions:
-
-The above copyright notice and this permission notice shall be included in all
-copies or substantial portions of the Software.
-
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-SOFTWARE.
diff --git a/README.md b/README.md
index eef4a61..8ef8e6f 100644
--- a/README.md
+++ b/README.md
@@ -1,107 +1,180 @@
-# Cli-Proxy-API-Management-Center
-This is the modern WebUI for managing the CLI Proxy API.
+# CLI Proxy Web UI - React Version
-[中文文档](README_CN.md)
+CLI Proxy API Management Center 的 React + TypeScript 重构版本。
-Main Project: https://github.com/router-for-me/CLIProxyAPI
-Example URL: https://remote.router-for.me/
-Minimum required version: ≥ 6.3.0 (recommended ≥ 6.5.0)
+## ✨ 特性
-Since 6.0.19 the WebUI ships with the main program; access it via `/management.html` on the API port once the service is running.
+- 🎯 完全使用 TypeScript 编写,类型安全
+- ⚛️ 基于 React 18 + Vite 构建,开发体验极佳
+- 🎨 SCSS 模块化样式,支持亮色/暗色主题
+- 🌍 完整的国际化支持 (中文/英文)
+- 📦 单文件部署,无需构建服务器
+- 🔒 安全的本地存储,支持数据加密
+- 📱 响应式设计,支持移动端
-## Features
+## 🚀 快速开始
-### Capabilities
-- **Login & UX**: Auto-detects the current address (manual override/reset supported), encrypted auto-login, language/theme toggles, responsive layout with mobile sidebar.
-- **Basic Settings**: Debug, proxy URL, request retries, quota fallback (auto-switch project/preview models), usage-statistics toggle, request logging & logging-to-file switches, WebSocket `/ws/*` auth switch.
-- **Keys & Providers**: Manage proxy auth keys, Gemini/Codex/Claude configs, OpenAI-compatible providers (custom base URLs/headers/proxy/model aliases), Vertex AI credential import from service-account JSON with optional location.
-- **Auth Files & OAuth**: Upload/download/search/paginate JSON credentials; type filters (Qwen/Gemini/GeminiCLI/AIStudio/Claude/Codex/Antigravity/iFlow/Vertex/Empty); delete-all; OAuth/Device flows for Codex, Anthropic (Claude), Antigravity (Google), Gemini CLI (optional project), Qwen; iFlow OAuth and cookie login.
-- **Logs**: Live viewer with auto-refresh/incremental updates, download and clear; section appears when logging-to-file is enabled.
-- **Usage Analytics**: Overview cards, hourly/daily toggles, up to three model lines per chart, per-API stats table (Chart.js).
-- **Config Management**: In-browser CodeMirror YAML editor for `/config.yaml` with reload/save, syntax highlighting, and status feedback.
-- **System Info & Versioning**: Connection/config cache status, last refresh time, server version/build date, and UI version in the footer.
-- **Security & Preferences**: Masked secrets, secure local storage, persistent theme/language/sidebar state, real-time status feedback.
+### 开发模式
-## How to Use
-
-1) **After CLI Proxy API is running (recommended)**
- Visit `http://your-server:8317/management.html`.
-
-2) **Direct static use after build**
- The single file `dist/index.html` generated by `npm run build`
-
-3) **Local server**
```bash
+# 安装依赖
npm install
-npm start # http://localhost:3000
-npm run dev # optional dev port: 3090
-# or
-python -m http.server 8000
-```
- Then open the corresponding localhost URL.
-4) **Configure connection**
- The login page shows the detected address; you can override it, enter the management key, and click Connect. Saved credentials use encrypted local storage for auto-login.
+# 启动开发服务器
+npm run dev
-Tip: The Logs navigation item appears after enabling "Logging to file" in Basic Settings.
-
-## Tech Stack
-
-- **Frontend**: Plain HTML, CSS, JavaScript (ES6+)
-- **Styling**: CSS3 + Flexbox/Grid with CSS Variables
-- **Icons**: Font Awesome 6.4.0
-- **Charts**: Chart.js for interactive data visualization
-- **Editor/Parsing**: CodeMirror + js-yaml
-- **Fonts**: Segoe UI system font
-- **Internationalization**: Custom i18n (EN/CN) and theme system (light/dark)
-- **API**: RESTful management endpoints with automatic authentication
-- **Storage**: LocalStorage with lightweight encryption for preferences/credentials
-
-## Build & Development
-
-- `npm run build` bundles everything into `dist/index.html` via webpack (`build.cjs`, `bundle-entry.js`, `build-scripts/prepare-html.js`).
-- External CDNs remain for Font Awesome, Chart.js, and CodeMirror to keep the bundle lean.
-- Development servers: `npm start` (3000) or `npm run dev` (3090); Python `http.server` also works for static hosting.
-
-## Troubleshooting
-
-### Connection Issues
-1. Confirm that the CLI Proxy API service is running.
-2. Check if the API address is correct.
-3. Verify that the management key is valid.
-4. Ensure your firewall settings allow the connection.
-
-### Data Not Updating
-1. Click the "Refresh All" button.
-2. Check your network connection.
-3. Check the browser's console for any error messages.
-
-### Logs & Config Editor
-- Logs: Requires server-side logging-to-file; 404 indicates the server build is too old or logging is disabled.
-- Config editor: Requires `/config.yaml` endpoint; keep YAML valid before saving.
-
-### Usage Stats
-- Enable "Usage statistics" if charts stay empty; data resets on server restart.
-
-## Project Structure
-```
-├── index.html
-├── styles.css
-├── app.js
-├── i18n.js
-├── src/ # Core/modules/utils source code
-├── build.cjs # Webpack build script
-├── bundle-entry.js # Bundling entry
-├── build-scripts/ # Build utilities
-│ └── prepare-html.js
-├── dist/ # Bundled single-file output
-├── BUILD_RELEASE.md
-├── LICENSE
-├── README.md
-└── README_CN.md
+# 访问 http://localhost:5173
```
-## Contributing
-We welcome Issues and Pull Requests to improve this project! We encourage more developers to contribute to the enhancement of this WebUI!
+### 生产构建
-This project is licensed under the MIT License.
+```bash
+# 构建生产版本
+npm run build
+
+# 产物在 dist/index.html
+# 直接双击打开或部署到服务器
+```
+
+### 代码检查
+
+```bash
+# TypeScript 类型检查
+npm run type-check
+
+# ESLint 代码检查
+npm run lint
+```
+
+## 📁 项目结构
+
+```
+src/
+├── components/ # 公共组件
+│ ├── common/ # 基础组件 (Button, Input, Card, Modal...)
+│ └── layout/ # 布局组件 (MainLayout, Sidebar, Header...)
+├── pages/ # 页面组件
+│ ├── LoginPage.tsx
+│ ├── SettingsPage.tsx
+│ ├── ApiKeysPage.tsx
+│ ├── AiProvidersPage.tsx
+│ ├── AuthFilesPage.tsx
+│ ├── OAuthPage.tsx
+│ ├── UsagePage.tsx
+│ ├── ConfigPage.tsx
+│ ├── LogsPage.tsx
+│ └── SystemPage.tsx
+├── services/ # API 服务
+│ ├── api/ # API 客户端
+│ └── storage/ # 本地存储服务
+├── stores/ # Zustand 状态管理
+│ ├── useAuthStore.ts
+│ ├── useConfigStore.ts
+│ ├── useThemeStore.ts
+│ └── useLanguageStore.ts
+├── hooks/ # 自定义 Hooks
+├── types/ # TypeScript 类型定义
+├── utils/ # 工具函数
+├── i18n/ # 国际化配置
+├── styles/ # 全局样式
+└── router/ # 路由配置
+```
+
+## 🔧 技术栈
+
+- **框架**: React 18
+- **语言**: TypeScript 5
+- **构建工具**: Vite 7
+- **路由**: React Router 7 (Hash 模式)
+- **状态管理**: Zustand 5
+- **样式**: SCSS Modules
+- **国际化**: i18next
+- **HTTP 客户端**: Axios
+- **代码检查**: ESLint + TypeScript ESLint
+
+## 📝 使用说明
+
+### 首次使用
+
+1. **清理旧数据** (如果从旧版本升级)
+ - 打开 `CLEAR_STORAGE.html` 文件
+ - 点击"清空 LocalStorage"按钮
+ - 这将清理旧版本的存储数据
+
+2. **打开应用**
+ - 双击 `dist/index.html` 文件
+ - 或使用 HTTP 服务器访问 (推荐)
+
+3. **配置连接**
+ - 输入 CLI Proxy API 服务器地址
+ - 输入管理密钥
+ - 点击"连接"按钮
+
+### 部署方式
+
+#### 方式 1: 本地文件 (file:// 协议)
+直接双击 `dist/index.html` 即可使用。应用已配置为使用 Hash 路由,支持 file:// 协议。
+
+#### 方式 2: HTTP 服务器 (推荐)
+```bash
+# 使用 Python
+cd dist
+python -m http.server 8080
+
+# 使用 Node.js (需要安装 serve)
+npx serve dist
+
+# 访问 http://localhost:8080
+```
+
+#### 方式 3: Nginx/Apache
+将 `dist/index.html` 部署到 Web 服务器即可。
+
+## 🐛 故障排除
+
+### 白屏问题
+
+如果打开后显示白屏:
+
+1. 检查浏览器控制台是否有错误
+2. 确认是否清理了旧版本的 localStorage 数据
+3. 尝试使用 HTTP 服务器访问而不是 file:// 协议
+
+### LocalStorage 错误
+
+如果看到 "Failed to parse stored data" 错误:
+
+1. 打开 `CLEAR_STORAGE.html`
+2. 清空所有存储数据
+3. 刷新页面重新登录
+
+### 路由问题
+
+应用使用 Hash 路由 (#/login, #/settings),确保 URL 中包含 `#` 符号。
+
+## 📊 构建信息
+
+- **TypeScript**: 0 errors ✅
+- **ESLint**: 0 errors, 137 warnings ⚠️
+- **Bundle Size**: 473 KB (144 KB gzipped)
+- **Build Time**: ~5 seconds
+
+## 🔄 从旧版本迁移
+
+旧版本 (原生 JS) 的数据存储格式已变更。首次使用新版本时:
+
+1. 旧的 localStorage 数据会自动迁移
+2. 如果迁移失败,请手动清理 localStorage
+3. 重新输入连接信息即可
+
+## 📄 License
+
+Same as CLI Proxy API
+
+## 🤝 贡献
+
+欢迎提交 Issue 和 Pull Request!
+
+---
+
+**注意**: 此版本是原 CLI Proxy Web UI 的 React 重构版本,与原版功能保持一致。
diff --git a/README_CN.md b/README_CN.md
deleted file mode 100644
index 3b56bfd..0000000
--- a/README_CN.md
+++ /dev/null
@@ -1,106 +0,0 @@
-# Cli-Proxy-API-Management-Center
-这是一个用于管理 CLI Proxy API 的现代化 Web 界面。
-
-[English](README.md)
-
-主项目: https://github.com/router-for-me/CLIProxyAPI
-示例网站: https://remote.router-for.me/
-最低版本 ≥ 6.3.0(推荐 ≥ 6.5.0)
-
-自 6.0.19 起 WebUI 已集成到主程序中,启动后可通过 `/management.html` 访问。
-
-## 功能特点
-
-### 主要能力
-- **登录与体验**: 自动检测当前地址(可自定义/重置),加密自动登录,语言/主题切换,响应式布局与移动端侧边栏。
-- **基础设置**: 调试、代理 URL、请求重试,配额溢出自动切换项目/预览模型,使用统计开关,请求日志与文件日志开关,WebSocket `/ws/*` 鉴权开关。
-- **密钥与提供商**: 管理代理服务密钥,Gemini/Codex/Claude 配置,OpenAI 兼容提供商(自定义 Base URL/Headers/Proxy/模型别名),Vertex AI 服务账号导入(可选区域)。
-- **认证文件与 OAuth**: 上传/下载/搜索/分页 JSON 凭据,类型筛选(Qwen/Gemini/GeminiCLI/AIStudio/Claude/Codex/Antigravity/iFlow/Vertex/Empty),一键删除全部;Codex、Anthropic(Claude)、Antigravity(Google)、Gemini CLI(可选项目)、Qwen 设备码、iFlow OAuth 与 Cookie 登录。
-- **日志**: 实时查看并增量刷新,支持下载和清空;启用“写入日志文件”后出现日志栏目。
-- **使用统计**: 概览卡片、小时/天切换、最多三条模型曲线、按 API 统计表(Chart.js)。
-- **配置管理**: 内置 CodeMirror YAML 编辑器,在线读取/保存 `/config.yaml`,语法高亮与状态提示。
-- **系统与版本**: 连接/配置缓存状态、最后刷新时间,底栏显示服务版本、构建时间与 UI 版本。
-- **安全与偏好**: 密钥遮蔽、加密本地存储,主题/语言/侧边栏状态持久化,实时状态反馈。
-
-## 使用方法
-
-1) **主程序启动后使用(推荐)**
- 访问 `http://您的服务器:8317/management.html`。
-
-2) **构建后直接静态打开**
- `npm run build` 生成的 `dist/index.html` 单文件
-
-3) **本地服务器**
-```bash
-npm install
-npm start # 默认 http://localhost:3000
-npm run dev # 可选开发端口 3090
-# 或
-python -m http.server 8000
-```
- 然后在浏览器打开对应的 localhost 地址。
-
-4) **配置连接**
- 登录页会显示自动检测的地址,可自行修改,填入管理密钥后点击连接。凭据将加密保存以便下次自动登录。
-
-提示: 开启“写入日志文件”后才会显示“日志查看”导航。
-
-## 技术栈
-
-- **前端**: 纯 HTML、CSS、JavaScript (ES6+)
-- **样式**: CSS3 + Flexbox/Grid,支持 CSS 变量
-- **图标**: Font Awesome 6.4.0
-- **图表**: Chart.js 交互式数据可视化
-- **编辑/解析**: CodeMirror + js-yaml
-- **国际化**: 自定义 i18n(中/英)与主题系统(明/暗)
-- **API**: RESTful 管理接口,自动附加认证
-- **存储**: LocalStorage 轻量加密存储偏好与凭据
-
-## 构建与开发
-
-- `npm run build` 通过 webpack(`build.cjs`、`bundle-entry.js`、`build-scripts/prepare-html.js`)打包为 `dist/index.html`。
-- Font Awesome、Chart.js、CodeMirror 仍走 CDN,减小打包体积。
-- 开发可用 `npm start` (3000) / `npm run dev` (3090) 或 `python -m http.server` 静态托管。
-
-## 故障排除
-
-### 连接问题
-1. 确认 CLI Proxy API 服务正在运行
-2. 检查 API 地址是否正确
-3. 验证管理密钥是否有效
-4. 确认防火墙设置允许连接
-
-### 数据不更新
-1. 点击"刷新全部"按钮
-2. 检查网络连接
-3. 查看浏览器控制台错误信息
-
-### 日志与配置编辑
-- 日志: 需要服务端开启写文件日志;返回 404 说明版本过旧或未启用。
-- 配置编辑: 依赖 `/config.yaml` 接口,保存前请确保 YAML 语法正确。
-
-### 使用统计
-- 若图表为空,请开启“使用统计”;数据在服务重启后会清空。
-
-## 项目结构
-```
-├── index.html
-├── styles.css
-├── app.js
-├── i18n.js
-├── src/ # 核心/模块/工具源码
-├── build.cjs # Webpack 构建脚本
-├── bundle-entry.js # 打包入口
-├── build-scripts/ # 构建工具
-│ └── prepare-html.js
-├── dist/ # 打包输出单文件
-├── BUILD_RELEASE.md
-├── LICENSE
-├── README.md
-└── README_CN.md
-```
-
-## 贡献
-欢迎提交 Issue 和 Pull Request 来改进这个项目!我们欢迎更多的大佬来对这个 WebUI 进行更新!
-
-本项目采用 MIT 许可。
diff --git a/REFACTOR_PROGRESS.md b/REFACTOR_PROGRESS.md
new file mode 100644
index 0000000..f5cbd2e
--- /dev/null
+++ b/REFACTOR_PROGRESS.md
@@ -0,0 +1,21 @@
+# 重构进度追踪
+
+## 阶段完成状态
+- [x] 阶段 0: 准备工作
+- [x] 阶段 1: 项目初始化
+- [x] 阶段 2: 类型定义
+- [x] 阶段 3: 工具与服务
+- [x] 阶段 4: 状态管理
+- [x] 阶段 5: 自定义 Hooks
+- [x] 阶段 6: 国际化配置
+- [x] 阶段 7: 样式系统
+- [x] 阶段 8: 通用组件
+- [x] 阶段 9: 页面组件(API Keys / Providers / Auth Files / OAuth / Usage / Config / Logs / System)
+- [x] 阶段 10: 路由布局与构建验证
+
+## 错误记录
+
+
+## 待办事项
+- [ ] 深入回归交互细节(模型价格、图表、日志增量等)与基线对齐
+- [ ] 优化 Sass 导入与 darken 用法以消除警告
diff --git a/app.js b/app.js
deleted file mode 100644
index 06e7e13..0000000
--- a/app.js
+++ /dev/null
@@ -1,1065 +0,0 @@
-// 模块导入
-import { themeModule } from './src/modules/theme.js';
-import { navigationModule } from './src/modules/navigation.js';
-import { languageModule } from './src/modules/language.js';
-import { loginModule } from './src/modules/login.js';
-import { configEditorModule } from './src/modules/config-editor.js';
-import { logsModule } from './src/modules/logs.js';
-import { apiKeysModule } from './src/modules/api-keys.js';
-import { authFilesModule } from './src/modules/auth-files.js';
-import { oauthModule } from './src/modules/oauth.js';
-import { usageModule } from './src/modules/usage.js';
-import { settingsModule } from './src/modules/settings.js';
-import { aiProvidersModule } from './src/modules/ai-providers.js';
-
-// 工具函数导入
-import { escapeHtml } from './src/utils/html.js';
-import { maskApiKey, formatFileSize } from './src/utils/string.js';
-import { normalizeArrayResponse } from './src/utils/array.js';
-import { debounce } from './src/utils/dom.js';
-import {
- CACHE_EXPIRY_MS,
- MAX_LOG_LINES,
- LOG_FETCH_LIMIT,
- DEFAULT_AUTH_FILES_PAGE_SIZE,
- MIN_AUTH_FILES_PAGE_SIZE,
- MAX_AUTH_FILES_PAGE_SIZE,
- OAUTH_CARD_IDS,
- STORAGE_KEY_AUTH_FILES_PAGE_SIZE,
- NOTIFICATION_DURATION_MS
-} from './src/utils/constants.js';
-
-// 核心服务导入
-import { createErrorHandler } from './src/core/error-handler.js';
-import { connectionModule } from './src/core/connection.js';
-import { ApiClient } from './src/core/api-client.js';
-import { ConfigService } from './src/core/config-service.js';
-import { createEventBus } from './src/core/event-bus.js';
-
-// CLI Proxy API 管理界面 JavaScript
-class CLIProxyManager {
- constructor() {
- // 事件总线
- this.events = createEventBus();
-
- // API 客户端(规范化基础地址、封装请求)
- this.apiClient = new ApiClient({
- onVersionUpdate: (headers) => this.updateVersionFromHeaders(headers)
- });
- const detectedBase = this.detectApiBaseFromLocation();
- this.apiClient.setApiBase(detectedBase);
- this.apiBase = this.apiClient.apiBase;
- this.apiUrl = this.apiClient.apiUrl;
- this.managementKey = '';
- this.isConnected = false;
- this.isLoggedIn = false;
- this.uiVersion = null;
- this.serverVersion = null;
- this.serverBuildDate = null;
- this.latestVersion = null;
- this.versionCheckStatus = 'muted';
- this.versionCheckMessage = i18n.t('system_info.version_check_idle');
-
- // 配置缓存 - 改为分段缓存(交由 ConfigService 管理)
- this.cacheExpiry = CACHE_EXPIRY_MS;
- this.configService = new ConfigService({
- apiClient: this.apiClient,
- cacheExpiry: this.cacheExpiry
- });
- this.configCache = this.configService.cache;
- this.cacheTimestamps = this.configService.cacheTimestamps;
- this.availableModels = [];
- this.availableModelApiKeysCache = null;
- this.availableModelsLoading = false;
-
- // 状态更新定时器
- this.statusUpdateTimer = null;
- this.lastConnectionStatusEmitted = null;
- this.isGlobalRefreshInProgress = false;
-
- this.registerCoreEventHandlers();
-
- // 日志自动刷新定时器
- this.logsRefreshTimer = null;
-
- // 当前展示的日志行
- this.allLogLines = [];
- this.displayedLogLines = [];
- this.logSearchQuery = '';
- this.maxDisplayLogLines = MAX_LOG_LINES;
- this.logFetchLimit = LOG_FETCH_LIMIT;
-
- // 日志时间戳(用于增量加载)
- this.latestLogTimestamp = null;
-
- // Auth file filter state cache
- this.currentAuthFileFilter = 'all';
- this.cachedAuthFiles = [];
- this.authFilesPagination = {
- pageSize: DEFAULT_AUTH_FILES_PAGE_SIZE,
- currentPage: 1,
- totalPages: 1
- };
- this.authFileStatsCache = {};
- this.authFileSearchQuery = '';
- this.authFilesPageSizeKey = STORAGE_KEY_AUTH_FILES_PAGE_SIZE;
- this.loadAuthFilePreferences();
-
- // OAuth 模型排除列表状态
- this.oauthExcludedModels = {};
- this._oauthExcludedLoading = false;
-
- // Vertex AI credential import state
- this.vertexImportState = {
- file: null,
- loading: false,
- result: null
- };
-
- // 顶栏标题动画状态
- this.brandCollapseTimer = null;
- this.brandCollapseDelayMs = 5000;
- this.brandIsCollapsed = false;
- this.brandAnimationReady = false;
- this.brandElements = {
- toggle: null,
- wrapper: null,
- fullText: null,
- shortText: null
- };
- this.brandResizeHandler = null;
- this.brandToggleHandler = null;
-
- // 主题管理
- this.currentTheme = 'light';
-
- // 配置文件编辑器状态
- this.configYamlCache = '';
- this.isConfigEditorDirty = false;
- this.configEditorElements = {
- textarea: null,
- editorInstance: null,
- saveBtn: null,
- reloadBtn: null,
- statusEl: null
- };
- this.lastConfigFetchUrl = null;
- this.lastEditorConnectionState = null;
-
- // 初始化错误处理器
- this.errorHandler = createErrorHandler((message, type) => this.showNotification(message, type));
-
- this.init();
- }
-
- loadAuthFilePreferences() {
- try {
- if (typeof localStorage === 'undefined') {
- return;
- }
- const savedPageSize = parseInt(localStorage.getItem(this.authFilesPageSizeKey), 10);
- if (Number.isFinite(savedPageSize)) {
- this.authFilesPagination.pageSize = this.normalizeAuthFilesPageSize(savedPageSize);
- }
- } catch (error) {
- console.warn('Failed to restore auth file preferences:', error);
- }
- }
-
- normalizeAuthFilesPageSize(value) {
- const defaultSize = DEFAULT_AUTH_FILES_PAGE_SIZE;
- const minSize = MIN_AUTH_FILES_PAGE_SIZE;
- const maxSize = MAX_AUTH_FILES_PAGE_SIZE;
- const parsed = parseInt(value, 10);
- if (!Number.isFinite(parsed) || parsed <= 0) {
- return defaultSize;
- }
- return Math.min(maxSize, Math.max(minSize, parsed));
- }
-
- init() {
- this.initUiVersion();
- this.initializeTheme();
- this.registerCoreEventHandlers();
- this.registerSettingsListeners();
- this.registerUsageListeners();
- if (typeof this.registerLogsListeners === 'function') {
- this.registerLogsListeners();
- }
- if (typeof this.registerConfigEditorListeners === 'function') {
- this.registerConfigEditorListeners();
- }
- this.checkLoginStatus();
- this.bindEvents();
- this.setupNavigation();
- this.setupLanguageSwitcher();
- this.setupThemeSwitcher();
- this.setupConfigEditor();
- this.updateConfigEditorAvailability();
- // loadSettings 将在登录成功后调用
- this.updateLoginConnectionInfo();
- // 检查主机名,如果不是 localhost 或 127.0.0.1,则隐藏 OAuth 登录框
- this.checkHostAndHideOAuth();
- if (typeof this.registerAuthFilesListeners === 'function') {
- this.registerAuthFilesListeners();
- }
- }
-
- registerCoreEventHandlers() {
- if (!this.events || typeof this.events.on !== 'function') {
- return;
- }
- this.events.on('config:refresh-requested', async (event) => {
- const detail = event?.detail || {};
- const forceRefresh = detail.forceRefresh !== false;
- // 避免并发触发导致重复请求
- if (this.isGlobalRefreshInProgress) {
- return;
- }
- await this.runGlobalRefresh(forceRefresh);
- });
- }
-
- async runGlobalRefresh(forceRefresh = false) {
- this.isGlobalRefreshInProgress = true;
- try {
- await this.loadAllData(forceRefresh);
- } finally {
- this.isGlobalRefreshInProgress = false;
- }
- }
-
- // 检查主机名并隐藏 OAuth 登录框
- checkHostAndHideOAuth() {
- const hostname = window.location.hostname;
- const isLocalhost = hostname === 'localhost' || hostname === '127.0.0.1' || hostname === '::1';
-
- if (!isLocalhost) {
- // 隐藏所有 OAuth 登录卡片
- OAUTH_CARD_IDS.forEach(cardId => {
- const card = document.getElementById(cardId);
- if (card) {
- card.style.display = 'none';
- }
- });
-
- // 如果找不到具体的卡片 ID,尝试通过类名查找
- const oauthCardElements = document.querySelectorAll('.card');
- oauthCardElements.forEach(card => {
- const cardText = card.textContent || '';
- if (cardText.includes('Codex OAuth') ||
- cardText.includes('Anthropic OAuth') ||
- cardText.includes('Antigravity OAuth') ||
- cardText.includes('Gemini CLI OAuth') ||
- cardText.includes('Qwen OAuth') ||
- cardText.includes('iFlow OAuth')) {
- card.style.display = 'none';
- }
- });
-
- console.log(`当前主机名: ${hostname},已隐藏 OAuth 登录框`);
- }
- }
-
- // 检查登录状态
- // 处理登录表单提交
- // 事件绑定
- bindEvents() {
- // 登录相关(安全绑定)
- const loginSubmit = document.getElementById('login-submit');
- const logoutBtn = document.getElementById('logout-btn');
-
- if (loginSubmit) {
- loginSubmit.addEventListener('click', () => this.handleLogin());
- }
- if (logoutBtn) {
- logoutBtn.addEventListener('click', () => this.logout());
- }
-
- // 密钥可见性切换事件
- this.setupKeyVisibilityToggle();
-
- // 主页面元素(延迟绑定,在显示主页面时绑定)
- this.bindMainPageEvents();
- }
-
- // 设置密钥可见性切换
- setupKeyVisibilityToggle() {
- const toggleButtons = document.querySelectorAll('.toggle-key-visibility');
- toggleButtons.forEach(button => {
- button.addEventListener('click', () => this.toggleLoginKeyVisibility(button));
- });
- }
-
- // 绑定主页面事件
- bindMainPageEvents() {
- // 连接状态检查
- const connectionStatus = document.getElementById('connection-status');
- const refreshAll = document.getElementById('refresh-all');
- const availableModelsRefresh = document.getElementById('available-models-refresh');
- const versionCheckBtn = document.getElementById('version-check-btn');
-
- if (connectionStatus) {
- connectionStatus.addEventListener('click', () => this.checkConnectionStatus());
- }
- if (refreshAll) {
- refreshAll.addEventListener('click', () => this.refreshAllData());
- }
- if (availableModelsRefresh) {
- availableModelsRefresh.addEventListener('click', () => this.loadAvailableModels({ forceRefresh: true }));
- }
- if (versionCheckBtn) {
- versionCheckBtn.addEventListener('click', () => this.checkLatestVersion());
- }
-
- // 基础设置
- const debugToggle = document.getElementById('debug-toggle');
- const updateProxy = document.getElementById('update-proxy');
- const clearProxy = document.getElementById('clear-proxy');
- const updateRetry = document.getElementById('update-retry');
- const switchProjectToggle = document.getElementById('switch-project-toggle');
- const switchPreviewToggle = document.getElementById('switch-preview-model-toggle');
- const usageStatisticsToggle = document.getElementById('usage-statistics-enabled-toggle');
- const requestLogToggle = document.getElementById('request-log-toggle');
- const wsAuthToggle = document.getElementById('ws-auth-toggle');
-
- if (debugToggle) {
- debugToggle.addEventListener('change', (e) => this.updateDebug(e.target.checked));
- }
- if (updateProxy) {
- updateProxy.addEventListener('click', () => this.updateProxyUrl());
- }
- if (clearProxy) {
- clearProxy.addEventListener('click', () => this.clearProxyUrl());
- }
- if (updateRetry) {
- updateRetry.addEventListener('click', () => this.updateRequestRetry());
- }
- if (switchProjectToggle) {
- switchProjectToggle.addEventListener('change', (e) => this.updateSwitchProject(e.target.checked));
- }
- if (switchPreviewToggle) {
- switchPreviewToggle.addEventListener('change', (e) => this.updateSwitchPreviewModel(e.target.checked));
- }
- if (usageStatisticsToggle) {
- usageStatisticsToggle.addEventListener('change', (e) => this.updateUsageStatisticsEnabled(e.target.checked));
- }
- if (requestLogToggle) {
- requestLogToggle.addEventListener('change', (e) => this.updateRequestLog(e.target.checked));
- }
- if (wsAuthToggle) {
- wsAuthToggle.addEventListener('change', (e) => this.updateWsAuth(e.target.checked));
- }
-
- // 日志记录设置
- const loggingToFileToggle = document.getElementById('logging-to-file-toggle');
- if (loggingToFileToggle) {
- loggingToFileToggle.addEventListener('change', (e) => this.updateLoggingToFile(e.target.checked));
- }
-
- // 日志查看
- const refreshLogs = document.getElementById('refresh-logs');
- const selectErrorLog = document.getElementById('select-error-log');
- const downloadLogs = document.getElementById('download-logs');
- const clearLogs = document.getElementById('clear-logs');
- const logsAutoRefreshToggle = document.getElementById('logs-auto-refresh-toggle');
- const logsSearchInput = document.getElementById('logs-search-input');
-
- if (refreshLogs) {
- refreshLogs.addEventListener('click', () => this.refreshLogs());
- }
- if (selectErrorLog) {
- selectErrorLog.addEventListener('click', () => this.openErrorLogsModal());
- }
- if (downloadLogs) {
- downloadLogs.addEventListener('click', () => this.downloadLogs());
- }
- if (clearLogs) {
- clearLogs.addEventListener('click', () => this.clearLogs());
- }
- if (logsAutoRefreshToggle) {
- logsAutoRefreshToggle.addEventListener('change', (e) => this.toggleLogsAutoRefresh(e.target.checked));
- }
- if (logsSearchInput) {
- const debouncedLogSearch = this.debounce((value) => {
- this.updateLogSearchQuery(value);
- }, 200);
- logsSearchInput.addEventListener('input', (e) => {
- debouncedLogSearch(e?.target?.value ?? '');
- });
- }
-
- // API 密钥管理
- const addApiKey = document.getElementById('add-api-key');
- const addGeminiKey = document.getElementById('add-gemini-key');
- const addCodexKey = document.getElementById('add-codex-key');
- const addClaudeKey = document.getElementById('add-claude-key');
- const addOpenaiProvider = document.getElementById('add-openai-provider');
-
- if (addApiKey) {
- addApiKey.addEventListener('click', () => this.showAddApiKeyModal());
- }
- if (addGeminiKey) {
- addGeminiKey.addEventListener('click', () => this.showAddGeminiKeyModal());
- }
- if (addCodexKey) {
- addCodexKey.addEventListener('click', () => this.showAddCodexKeyModal());
- }
- if (addClaudeKey) {
- addClaudeKey.addEventListener('click', () => this.showAddClaudeKeyModal());
- }
- if (addOpenaiProvider) {
- addOpenaiProvider.addEventListener('click', () => this.showAddOpenAIProviderModal());
- }
-
-
- // 认证文件管理
- const uploadAuthFile = document.getElementById('upload-auth-file');
- const deleteAllAuthFiles = document.getElementById('delete-all-auth-files');
- const authFileInput = document.getElementById('auth-file-input');
-
- if (uploadAuthFile) {
- uploadAuthFile.addEventListener('click', () => this.uploadAuthFile());
- }
- if (deleteAllAuthFiles) {
- deleteAllAuthFiles.addEventListener('click', () => this.deleteAllAuthFiles());
- }
- if (authFileInput) {
- authFileInput.addEventListener('change', (e) => this.handleFileUpload(e));
- }
- this.bindAuthFilesPaginationEvents();
- this.bindAuthFilesSearchControl();
- this.bindAuthFilesPageSizeControl();
- this.syncAuthFileControls();
-
- // OAuth 排除列表
- const oauthExcludedAdd = document.getElementById('oauth-excluded-add');
- const oauthExcludedRefresh = document.getElementById('oauth-excluded-refresh');
-
- if (oauthExcludedAdd) {
- oauthExcludedAdd.addEventListener('click', () => this.openOauthExcludedEditor());
- }
- if (oauthExcludedRefresh) {
- oauthExcludedRefresh.addEventListener('click', () => this.loadOauthExcludedModels(true));
- }
-
- // Vertex AI credential import
- const vertexSelectFile = document.getElementById('vertex-select-file');
- const vertexFileInput = document.getElementById('vertex-file-input');
- const vertexImportBtn = document.getElementById('vertex-import-btn');
-
- if (vertexSelectFile) {
- vertexSelectFile.addEventListener('click', () => this.openVertexFilePicker());
- }
- if (vertexFileInput) {
- vertexFileInput.addEventListener('change', (e) => this.handleVertexFileSelection(e));
- }
- if (vertexImportBtn) {
- vertexImportBtn.addEventListener('click', () => this.importVertexCredential());
- }
- this.updateVertexFileDisplay();
- this.updateVertexImportButtonState();
- this.renderVertexImportResult(this.vertexImportState.result);
-
- // Codex OAuth
- const codexOauthBtn = document.getElementById('codex-oauth-btn');
- const codexOpenLink = document.getElementById('codex-open-link');
- const codexCopyLink = document.getElementById('codex-copy-link');
-
- if (codexOauthBtn) {
- codexOauthBtn.addEventListener('click', () => this.startCodexOAuth());
- }
- if (codexOpenLink) {
- codexOpenLink.addEventListener('click', () => this.openCodexLink());
- }
- if (codexCopyLink) {
- codexCopyLink.addEventListener('click', () => this.copyCodexLink());
- }
-
- // Anthropic OAuth
- const anthropicOauthBtn = document.getElementById('anthropic-oauth-btn');
- const anthropicOpenLink = document.getElementById('anthropic-open-link');
- const anthropicCopyLink = document.getElementById('anthropic-copy-link');
-
- if (anthropicOauthBtn) {
- anthropicOauthBtn.addEventListener('click', () => this.startAnthropicOAuth());
- }
- if (anthropicOpenLink) {
- anthropicOpenLink.addEventListener('click', () => this.openAnthropicLink());
- }
- if (anthropicCopyLink) {
- anthropicCopyLink.addEventListener('click', () => this.copyAnthropicLink());
- }
-
- // Antigravity OAuth
- const antigravityOauthBtn = document.getElementById('antigravity-oauth-btn');
- const antigravityOpenLink = document.getElementById('antigravity-open-link');
- const antigravityCopyLink = document.getElementById('antigravity-copy-link');
-
- if (antigravityOauthBtn) {
- antigravityOauthBtn.addEventListener('click', () => this.startAntigravityOAuth());
- }
- if (antigravityOpenLink) {
- antigravityOpenLink.addEventListener('click', () => this.openAntigravityLink());
- }
- if (antigravityCopyLink) {
- antigravityCopyLink.addEventListener('click', () => this.copyAntigravityLink());
- }
-
- // Gemini CLI OAuth
- const geminiCliOauthBtn = document.getElementById('gemini-cli-oauth-btn');
- const geminiCliOpenLink = document.getElementById('gemini-cli-open-link');
- const geminiCliCopyLink = document.getElementById('gemini-cli-copy-link');
-
- if (geminiCliOauthBtn) {
- geminiCliOauthBtn.addEventListener('click', () => this.startGeminiCliOAuth());
- }
- if (geminiCliOpenLink) {
- geminiCliOpenLink.addEventListener('click', () => this.openGeminiCliLink());
- }
- if (geminiCliCopyLink) {
- geminiCliCopyLink.addEventListener('click', () => this.copyGeminiCliLink());
- }
-
- // Qwen OAuth
- const qwenOauthBtn = document.getElementById('qwen-oauth-btn');
- const qwenOpenLink = document.getElementById('qwen-open-link');
- const qwenCopyLink = document.getElementById('qwen-copy-link');
-
- if (qwenOauthBtn) {
- qwenOauthBtn.addEventListener('click', () => this.startQwenOAuth());
- }
- if (qwenOpenLink) {
- qwenOpenLink.addEventListener('click', () => this.openQwenLink());
- }
- if (qwenCopyLink) {
- qwenCopyLink.addEventListener('click', () => this.copyQwenLink());
- }
-
- // iFlow OAuth
- const iflowOauthBtn = document.getElementById('iflow-oauth-btn');
- const iflowOpenLink = document.getElementById('iflow-open-link');
- const iflowCopyLink = document.getElementById('iflow-copy-link');
- const iflowCookieSubmit = document.getElementById('iflow-cookie-submit');
-
- if (iflowOauthBtn) {
- iflowOauthBtn.addEventListener('click', () => this.startIflowOAuth());
- }
- if (iflowOpenLink) {
- iflowOpenLink.addEventListener('click', () => this.openIflowLink());
- }
- if (iflowCopyLink) {
- iflowCopyLink.addEventListener('click', () => this.copyIflowLink());
- }
- if (iflowCookieSubmit) {
- iflowCookieSubmit.addEventListener('click', () => this.submitIflowCookieLogin());
- }
-
- // 使用统计
- const refreshUsageStats = document.getElementById('refresh-usage-stats');
- const requestsHourBtn = document.getElementById('requests-hour-btn');
- const requestsDayBtn = document.getElementById('requests-day-btn');
- const tokensHourBtn = document.getElementById('tokens-hour-btn');
- const tokensDayBtn = document.getElementById('tokens-day-btn');
- const costHourBtn = document.getElementById('cost-hour-btn');
- const costDayBtn = document.getElementById('cost-day-btn');
- const addChartLineBtn = document.getElementById('add-chart-line');
- const chartLineSelects = document.querySelectorAll('.chart-line-select');
- const chartLineDeleteButtons = document.querySelectorAll('.chart-line-delete');
- const modelPriceForm = document.getElementById('model-price-form');
- const resetModelPricesBtn = document.getElementById('reset-model-prices');
- const modelPriceSelect = document.getElementById('model-price-model-select');
-
- if (refreshUsageStats) {
- refreshUsageStats.addEventListener('click', () => this.loadUsageStats());
- }
- if (requestsHourBtn) {
- requestsHourBtn.addEventListener('click', () => this.switchRequestsPeriod('hour'));
- }
- if (requestsDayBtn) {
- requestsDayBtn.addEventListener('click', () => this.switchRequestsPeriod('day'));
- }
- if (tokensHourBtn) {
- tokensHourBtn.addEventListener('click', () => this.switchTokensPeriod('hour'));
- }
- if (tokensDayBtn) {
- tokensDayBtn.addEventListener('click', () => this.switchTokensPeriod('day'));
- }
- if (costHourBtn) {
- costHourBtn.addEventListener('click', () => this.switchCostPeriod('hour'));
- }
- if (costDayBtn) {
- costDayBtn.addEventListener('click', () => this.switchCostPeriod('day'));
- }
- if (addChartLineBtn) {
- addChartLineBtn.addEventListener('click', () => this.changeChartLineCount(1));
- }
- if (chartLineSelects.length) {
- chartLineSelects.forEach(select => {
- select.addEventListener('change', (event) => {
- const index = Number.parseInt(select.getAttribute('data-line-index'), 10);
- this.handleChartLineSelectionChange(Number.isNaN(index) ? -1 : index, event.target.value);
- });
- });
- }
- if (chartLineDeleteButtons.length) {
- chartLineDeleteButtons.forEach(button => {
- button.addEventListener('click', () => {
- const index = Number.parseInt(button.getAttribute('data-line-index'), 10);
- this.removeChartLine(Number.isNaN(index) ? -1 : index);
- });
- });
- }
- this.updateChartLineControlsUI();
- if (modelPriceForm) {
- modelPriceForm.addEventListener('submit', (event) => {
- event.preventDefault();
- this.handleModelPriceSubmit();
- });
- }
- if (resetModelPricesBtn) {
- resetModelPricesBtn.addEventListener('click', () => this.handleModelPriceReset());
- }
- if (modelPriceSelect) {
- modelPriceSelect.addEventListener('change', () => this.prefillModelPriceInputs());
- }
-
- // 模态框
- const closeBtn = document.querySelector('.close');
- if (closeBtn) {
- closeBtn.addEventListener('click', () => this.closeModal());
- }
-
- // 移动端菜单按钮
- const mobileMenuBtn = document.getElementById('mobile-menu-btn');
- const sidebarOverlay = document.getElementById('sidebar-overlay');
- const sidebar = document.getElementById('sidebar');
-
- if (mobileMenuBtn) {
- mobileMenuBtn.addEventListener('click', () => this.toggleMobileSidebar());
- }
-
- if (sidebarOverlay) {
- sidebarOverlay.addEventListener('click', () => this.closeMobileSidebar());
- }
-
- // 侧边栏收起/展开按钮(桌面端)
- const sidebarToggleBtnDesktop = document.getElementById('sidebar-toggle-btn-desktop');
- if (sidebarToggleBtnDesktop) {
- sidebarToggleBtnDesktop.addEventListener('click', () => this.toggleSidebar());
- }
-
- // 从本地存储恢复侧边栏状态
- this.restoreSidebarState();
-
- // 监听窗口大小变化
- window.addEventListener('resize', () => {
- const sidebar = document.getElementById('sidebar');
- const layout = document.getElementById('layout-container');
-
- if (window.innerWidth <= 1024) {
- // 移动端:移除收起状态
- if (sidebar && layout) {
- sidebar.classList.remove('collapsed');
- layout.classList.remove('sidebar-collapsed');
- }
- } else {
- // 桌面端:恢复保存的状态
- this.restoreSidebarState();
- }
- });
-
- // 点击侧边栏导航项时在移动端关闭侧边栏
- const navItems = document.querySelectorAll('.nav-item');
- navItems.forEach(item => {
- item.addEventListener('click', () => {
- if (window.innerWidth <= 1024) {
- this.closeMobileSidebar();
- }
- });
- });
- }
-
- // 顶栏标题动画与状态
- isMobileViewport() {
- return typeof window !== 'undefined' ? window.innerWidth <= 768 : false;
- }
-
- setupBrandTitleAnimation() {
- const mainPage = document.getElementById('main-page');
- if (mainPage && mainPage.style.display === 'none') {
- return;
- }
-
- const toggle = document.getElementById('brand-name-toggle');
- const wrapper = document.getElementById('brand-texts');
- const fullText = document.querySelector('.brand-text-full');
- const shortText = document.querySelector('.brand-text-short');
-
- if (!toggle || !wrapper || !fullText || !shortText) {
- return;
- }
-
- this.brandElements = { toggle, wrapper, fullText, shortText };
-
- if (!this.brandToggleHandler) {
- this.brandToggleHandler = () => this.handleBrandToggle();
- toggle.addEventListener('click', this.brandToggleHandler);
- }
- if (!this.brandResizeHandler) {
- this.brandResizeHandler = () => this.handleBrandResize();
- window.addEventListener('resize', this.brandResizeHandler);
- }
-
- if (this.isMobileViewport()) {
- this.applyMobileBrandState();
- } else {
- this.enableBrandAnimation();
- }
- }
-
- enableBrandAnimation() {
- const { toggle } = this.brandElements || {};
- if (toggle) {
- toggle.removeAttribute('aria-disabled');
- toggle.style.pointerEvents = '';
- }
- this.brandAnimationReady = true;
- }
-
- applyMobileBrandState() {
- const { toggle, wrapper, shortText } = this.brandElements || {};
- if (!toggle || !wrapper || !shortText) {
- return;
- }
-
- this.clearBrandCollapseTimer();
- this.brandIsCollapsed = true;
- this.brandAnimationReady = false;
-
- toggle.classList.add('collapsed');
- toggle.classList.remove('expanded');
- toggle.setAttribute('aria-disabled', 'true');
- toggle.style.pointerEvents = 'none';
-
- const targetWidth = this.getBrandTextWidth(shortText);
- this.applyBrandWidth(targetWidth, { animate: false });
- }
-
- getBrandTextWidth(element) {
- if (!element) {
- return 0;
- }
- const width = element.scrollWidth || element.getBoundingClientRect().width || 0;
- return Number.isFinite(width) ? Math.ceil(width) : 0;
- }
-
- applyBrandWidth(targetWidth, { animate = true } = {}) {
- const wrapper = this.brandElements?.wrapper;
- if (!wrapper || !Number.isFinite(targetWidth)) {
- return;
- }
-
- if (!animate) {
- const previousTransition = wrapper.style.transition;
- wrapper.style.transition = 'none';
- wrapper.style.width = `${targetWidth}px`;
- wrapper.getBoundingClientRect(); // 强制重绘以应用无动画的宽度
- wrapper.style.transition = previousTransition;
- return;
- }
-
- wrapper.style.width = `${targetWidth}px`;
- }
-
- updateBrandTextWidths(options = {}) {
- const { wrapper, fullText, shortText } = this.brandElements || {};
- if (!wrapper || !fullText || !shortText) {
- return;
- }
-
- const targetSpan = this.brandIsCollapsed ? shortText : fullText;
- const targetWidth = this.getBrandTextWidth(targetSpan);
- this.applyBrandWidth(targetWidth, { animate: !options.immediate });
- }
-
- setBrandCollapsed(collapsed, options = {}) {
- const { toggle, fullText, shortText } = this.brandElements || {};
- if (!toggle || !fullText || !shortText) {
- return;
- }
-
- this.brandIsCollapsed = collapsed;
- const targetSpan = collapsed ? shortText : fullText;
- const targetWidth = this.getBrandTextWidth(targetSpan);
-
- this.applyBrandWidth(targetWidth, { animate: options.animate !== false });
- toggle.classList.toggle('collapsed', collapsed);
- toggle.classList.toggle('expanded', !collapsed);
- }
-
- handleBrandResize() {
- if (!this.brandElements?.wrapper) {
- return;
- }
-
- if (this.isMobileViewport()) {
- this.applyMobileBrandState();
- return;
- }
-
- if (!this.brandAnimationReady) {
- this.enableBrandAnimation();
- this.brandIsCollapsed = false;
- this.setBrandCollapsed(false, { animate: false });
- this.scheduleBrandCollapse(this.brandCollapseDelayMs);
- return;
- }
-
- this.updateBrandTextWidths({ immediate: true });
- }
-
- scheduleBrandCollapse(delayMs = this.brandCollapseDelayMs) {
- this.clearBrandCollapseTimer();
- this.brandCollapseTimer = window.setTimeout(() => {
- this.setBrandCollapsed(true);
- this.brandCollapseTimer = null;
- }, delayMs);
- }
-
- clearBrandCollapseTimer() {
- if (this.brandCollapseTimer) {
- clearTimeout(this.brandCollapseTimer);
- this.brandCollapseTimer = null;
- }
- }
-
- startBrandCollapseCycle() {
- this.setupBrandTitleAnimation();
-
- if (this.isMobileViewport()) {
- this.applyMobileBrandState();
- return;
- }
-
- if (!this.brandAnimationReady) {
- return;
- }
-
- this.clearBrandCollapseTimer();
- this.brandIsCollapsed = false;
- this.setBrandCollapsed(false, { animate: false });
- this.scheduleBrandCollapse(this.brandCollapseDelayMs);
- }
-
- resetBrandTitleState() {
- this.clearBrandCollapseTimer();
- const mainPage = document.getElementById('main-page');
-
- if (this.isMobileViewport()) {
- this.applyMobileBrandState();
- return;
- }
-
- if (!this.brandAnimationReady || (mainPage && mainPage.style.display === 'none')) {
- this.brandIsCollapsed = false;
- return;
- }
-
- this.brandIsCollapsed = false;
- this.setBrandCollapsed(false, { animate: false });
- }
-
- refreshBrandTitleAfterTextChange() {
- if (this.isMobileViewport()) {
- this.applyMobileBrandState();
- return;
- }
-
- if (!this.brandAnimationReady) {
- return;
- }
- this.updateBrandTextWidths({ immediate: true });
- if (!this.brandIsCollapsed) {
- this.scheduleBrandCollapse(this.brandCollapseDelayMs);
- }
- }
-
- handleBrandToggle() {
- if (!this.brandAnimationReady) {
- return;
- }
-
- const nextCollapsed = !this.brandIsCollapsed;
- this.setBrandCollapsed(nextCollapsed);
- this.clearBrandCollapseTimer();
-
- if (!nextCollapsed) {
- // 展开后给用户留出一点时间阅读再收起
- this.scheduleBrandCollapse(this.brandCollapseDelayMs + 1500);
- }
- }
-
-
- // 显示通知
- showNotification(message, type = 'info') {
- const notification = document.getElementById('notification');
- notification.textContent = message;
- notification.className = `notification ${type}`;
- notification.classList.add('show');
-
- setTimeout(() => {
- notification.classList.remove('show');
- }, NOTIFICATION_DURATION_MS);
- }
-
- // 密钥可见性切换
- toggleKeyVisibility() {
- const keyInput = document.getElementById('management-key');
- const toggleButton = document.getElementById('toggle-key-visibility');
-
- if (keyInput.type === 'password') {
- keyInput.type = 'text';
- toggleButton.innerHTML = '';
- } else {
- keyInput.type = 'password';
- toggleButton.innerHTML = '';
- }
- }
-
- // ===== 使用统计相关方法 =====
-
- // 使用统计状态
- requestsChart = null;
- tokensChart = null;
- costChart = null;
- currentUsageData = null;
- chartLineMaxCount = 9;
- chartLineVisibleCount = 3;
- chartLineSelections = Array(3).fill('none');
- chartLineSelectionsInitialized = false;
- chartLineSelectIds = Array.from({ length: 9 }, (_, idx) => `chart-line-select-${idx}`);
- chartLineStyles = [
- { borderColor: '#3b82f6', backgroundColor: 'rgba(59, 130, 246, 0.15)' },
- { borderColor: '#a855f7', backgroundColor: 'rgba(168, 85, 247, 0.15)' },
- { borderColor: '#10b981', backgroundColor: 'rgba(16, 185, 129, 0.15)' },
- { borderColor: '#f97316', backgroundColor: 'rgba(249, 115, 22, 0.15)' },
- { borderColor: '#ec4899', backgroundColor: 'rgba(236, 72, 153, 0.15)' },
- { borderColor: '#14b8a6', backgroundColor: 'rgba(20, 184, 166, 0.15)' },
- { borderColor: '#8b5cf6', backgroundColor: 'rgba(139, 92, 246, 0.15)' },
- { borderColor: '#f59e0b', backgroundColor: 'rgba(245, 158, 11, 0.15)' },
- { borderColor: '#22c55e', backgroundColor: 'rgba(34, 197, 94, 0.15)' }
- ];
- modelPriceStorageKey = 'cli-proxy-model-prices-v2';
- modelPrices = {};
- modelPriceInitialized = false;
-
- showModal() {
- const modal = document.getElementById('modal');
- if (modal) {
- modal.style.display = 'block';
- }
- }
-
- // 关闭模态框
- closeModal() {
- document.getElementById('modal').style.display = 'none';
- if (typeof this.closeOpenAIModelDiscovery === 'function') {
- this.closeOpenAIModelDiscovery();
- }
- }
-
-}
-
-Object.assign(
- CLIProxyManager.prototype,
- themeModule,
- navigationModule,
- languageModule,
- loginModule,
- configEditorModule,
- logsModule,
- apiKeysModule,
- authFilesModule,
- oauthModule,
- usageModule,
- settingsModule,
- aiProvidersModule,
- connectionModule
-);
-
-// 将工具函数绑定到原型上,供模块使用
-CLIProxyManager.prototype.escapeHtml = escapeHtml;
-CLIProxyManager.prototype.maskApiKey = maskApiKey;
-CLIProxyManager.prototype.formatFileSize = formatFileSize;
-CLIProxyManager.prototype.normalizeArrayResponse = normalizeArrayResponse;
-CLIProxyManager.prototype.debounce = debounce;
-
-// 全局管理器实例
-let manager;
-
-// 让内联事件处理器可以访问到 manager 实例
-function exposeManagerInstance(instance) {
- if (typeof window !== 'undefined') {
- window.manager = instance;
- } else if (typeof globalThis !== 'undefined') {
- globalThis.manager = instance;
- }
-}
-
-// 尝试自动加载根目录 Logo(支持多种常见文件名/扩展名)
-function setupSiteLogo() {
- const img = document.getElementById('site-logo');
- const loginImg = document.getElementById('login-logo');
- if (!img && !loginImg) return;
-
- const inlineLogo = typeof window !== 'undefined' ? window.__INLINE_LOGO__ : null;
- if (inlineLogo) {
- if (img) {
- img.src = inlineLogo;
- img.style.display = 'inline-block';
- }
- if (loginImg) {
- loginImg.src = inlineLogo;
- loginImg.style.display = 'inline-block';
- }
- return;
- }
-
- const candidates = [
- '../logo.svg', '../logo.png', '../logo.jpg', '../logo.jpeg', '../logo.webp', '../logo.gif',
- 'logo.svg', 'logo.png', 'logo.jpg', 'logo.jpeg', 'logo.webp', 'logo.gif',
- '/logo.svg', '/logo.png', '/logo.jpg', '/logo.jpeg', '/logo.webp', '/logo.gif'
- ];
- let idx = 0;
- const tryNext = () => {
- if (idx >= candidates.length) return;
- const test = new Image();
- test.onload = () => {
- if (img) {
- img.src = test.src;
- img.style.display = 'inline-block';
- }
- if (loginImg) {
- loginImg.src = test.src;
- loginImg.style.display = 'inline-block';
- }
- };
- test.onerror = () => {
- idx++;
- tryNext();
- };
- test.src = candidates[idx];
- };
- tryNext();
-}
-
-// 页面加载完成后初始化
-document.addEventListener('DOMContentLoaded', () => {
- // 初始化国际化
- i18n.init();
-
- setupSiteLogo();
- manager = new CLIProxyManager();
- exposeManagerInstance(manager);
-});
diff --git a/build-scripts/prepare-html.js b/build-scripts/prepare-html.js
deleted file mode 100644
index e3f92d8..0000000
--- a/build-scripts/prepare-html.js
+++ /dev/null
@@ -1,30 +0,0 @@
-const fs = require('fs');
-const path = require('path');
-
-// Read the original index.html
-const indexPath = path.resolve(__dirname, '../index.html');
-const outputPath = path.resolve(__dirname, '../index.build.html');
-
-let htmlContent = fs.readFileSync(indexPath, 'utf8');
-
-// Remove local CSS reference
-htmlContent = htmlContent.replace(
- /\n?/g,
- ''
-);
-
-// Remove local JavaScript references
-htmlContent = htmlContent.replace(
- /',
- () => ``
- );
-
- const scriptTagRegex = /`
- );
- } else {
- console.warn('未找到 app.js 脚本标签,未内联应用代码。');
- }
-
- const logoDataUrl = loadLogoDataUrl();
- if (logoDataUrl) {
- const logoScript = ``;
- const closingBodyTag = '