mirror of
https://github.com/router-for-me/Cli-Proxy-API-Management-Center.git
synced 2026-02-19 03:00:49 +08:00
refactor(app): centralize UI constants and error handling
This commit is contained in:
61
app.js
61
app.js
@@ -1,3 +1,4 @@
|
|||||||
|
// 模块导入
|
||||||
import { themeModule } from './src/modules/theme.js';
|
import { themeModule } from './src/modules/theme.js';
|
||||||
import { navigationModule } from './src/modules/navigation.js';
|
import { navigationModule } from './src/modules/navigation.js';
|
||||||
import { languageModule } from './src/modules/language.js';
|
import { languageModule } from './src/modules/language.js';
|
||||||
@@ -11,6 +12,24 @@ import { usageModule } from './src/modules/usage.js';
|
|||||||
import { settingsModule } from './src/modules/settings.js';
|
import { settingsModule } from './src/modules/settings.js';
|
||||||
import { aiProvidersModule } from './src/modules/ai-providers.js';
|
import { aiProvidersModule } from './src/modules/ai-providers.js';
|
||||||
|
|
||||||
|
// 工具函数导入
|
||||||
|
import { escapeHtml } from './src/utils/html.js';
|
||||||
|
import { maskApiKey } from './src/utils/string.js';
|
||||||
|
import { normalizeArrayResponse } from './src/utils/array.js';
|
||||||
|
import {
|
||||||
|
CACHE_EXPIRY_MS,
|
||||||
|
MAX_LOG_LINES,
|
||||||
|
DEFAULT_API_PORT,
|
||||||
|
DEFAULT_AUTH_FILES_PAGE_SIZE,
|
||||||
|
MIN_AUTH_FILES_PAGE_SIZE,
|
||||||
|
MAX_AUTH_FILES_PAGE_SIZE,
|
||||||
|
OAUTH_CARD_IDS,
|
||||||
|
STORAGE_KEY_AUTH_FILES_PAGE_SIZE
|
||||||
|
} from './src/utils/constants.js';
|
||||||
|
|
||||||
|
// 核心服务导入
|
||||||
|
import { createErrorHandler } from './src/core/error-handler.js';
|
||||||
|
|
||||||
// CLI Proxy API 管理界面 JavaScript
|
// CLI Proxy API 管理界面 JavaScript
|
||||||
class CLIProxyManager {
|
class CLIProxyManager {
|
||||||
constructor() {
|
constructor() {
|
||||||
@@ -25,7 +44,7 @@ class CLIProxyManager {
|
|||||||
// 配置缓存
|
// 配置缓存
|
||||||
this.configCache = null;
|
this.configCache = null;
|
||||||
this.cacheTimestamp = null;
|
this.cacheTimestamp = null;
|
||||||
this.cacheExpiry = 30000; // 30秒缓存过期时间
|
this.cacheExpiry = CACHE_EXPIRY_MS;
|
||||||
|
|
||||||
// 状态更新定时器
|
// 状态更新定时器
|
||||||
this.statusUpdateTimer = null;
|
this.statusUpdateTimer = null;
|
||||||
@@ -35,7 +54,7 @@ class CLIProxyManager {
|
|||||||
|
|
||||||
// 当前展示的日志行
|
// 当前展示的日志行
|
||||||
this.displayedLogLines = [];
|
this.displayedLogLines = [];
|
||||||
this.maxDisplayLogLines = 10000;
|
this.maxDisplayLogLines = MAX_LOG_LINES;
|
||||||
|
|
||||||
// 日志时间戳(用于增量加载)
|
// 日志时间戳(用于增量加载)
|
||||||
this.latestLogTimestamp = null;
|
this.latestLogTimestamp = null;
|
||||||
@@ -44,13 +63,13 @@ class CLIProxyManager {
|
|||||||
this.currentAuthFileFilter = 'all';
|
this.currentAuthFileFilter = 'all';
|
||||||
this.cachedAuthFiles = [];
|
this.cachedAuthFiles = [];
|
||||||
this.authFilesPagination = {
|
this.authFilesPagination = {
|
||||||
pageSize: 9,
|
pageSize: DEFAULT_AUTH_FILES_PAGE_SIZE,
|
||||||
currentPage: 1,
|
currentPage: 1,
|
||||||
totalPages: 1
|
totalPages: 1
|
||||||
};
|
};
|
||||||
this.authFileStatsCache = {};
|
this.authFileStatsCache = {};
|
||||||
this.authFileSearchQuery = '';
|
this.authFileSearchQuery = '';
|
||||||
this.authFilesPageSizeKey = 'authFilesPageSize';
|
this.authFilesPageSizeKey = STORAGE_KEY_AUTH_FILES_PAGE_SIZE;
|
||||||
this.loadAuthFilePreferences();
|
this.loadAuthFilePreferences();
|
||||||
|
|
||||||
// Vertex AI credential import state
|
// Vertex AI credential import state
|
||||||
@@ -76,6 +95,9 @@ class CLIProxyManager {
|
|||||||
this.lastConfigFetchUrl = null;
|
this.lastConfigFetchUrl = null;
|
||||||
this.lastEditorConnectionState = null;
|
this.lastEditorConnectionState = null;
|
||||||
|
|
||||||
|
// 初始化错误处理器
|
||||||
|
this.errorHandler = createErrorHandler((message, type) => this.showNotification(message, type));
|
||||||
|
|
||||||
this.init();
|
this.init();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -94,9 +116,9 @@ class CLIProxyManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
normalizeAuthFilesPageSize(value) {
|
normalizeAuthFilesPageSize(value) {
|
||||||
const defaultSize = 9;
|
const defaultSize = DEFAULT_AUTH_FILES_PAGE_SIZE;
|
||||||
const minSize = 3;
|
const minSize = MIN_AUTH_FILES_PAGE_SIZE;
|
||||||
const maxSize = 60;
|
const maxSize = MAX_AUTH_FILES_PAGE_SIZE;
|
||||||
const parsed = parseInt(value, 10);
|
const parsed = parseInt(value, 10);
|
||||||
if (!Number.isFinite(parsed) || parsed <= 0) {
|
if (!Number.isFinite(parsed) || parsed <= 0) {
|
||||||
return defaultSize;
|
return defaultSize;
|
||||||
@@ -135,15 +157,7 @@ class CLIProxyManager {
|
|||||||
|
|
||||||
if (!isLocalhost) {
|
if (!isLocalhost) {
|
||||||
// 隐藏所有 OAuth 登录卡片
|
// 隐藏所有 OAuth 登录卡片
|
||||||
const oauthCards = [
|
OAUTH_CARD_IDS.forEach(cardId => {
|
||||||
'codex-oauth-card',
|
|
||||||
'anthropic-oauth-card',
|
|
||||||
'gemini-cli-oauth-card',
|
|
||||||
'qwen-oauth-card',
|
|
||||||
'iflow-oauth-card'
|
|
||||||
];
|
|
||||||
|
|
||||||
oauthCards.forEach(cardId => {
|
|
||||||
const card = document.getElementById(cardId);
|
const card = document.getElementById(cardId);
|
||||||
if (card) {
|
if (card) {
|
||||||
card.style.display = 'none';
|
card.style.display = 'none';
|
||||||
@@ -894,14 +908,6 @@ class CLIProxyManager {
|
|||||||
await this.renderOpenAIProviders(Array.isArray(config['openai-compatibility']) ? config['openai-compatibility'] : [], keyStats);
|
await this.renderOpenAIProviders(Array.isArray(config['openai-compatibility']) ? config['openai-compatibility'] : [], keyStats);
|
||||||
}
|
}
|
||||||
|
|
||||||
// HTML转义工具函数
|
|
||||||
escapeHtml(text) {
|
|
||||||
const div = document.createElement('div');
|
|
||||||
div.textContent = text;
|
|
||||||
return div.innerHTML;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// 显示添加API密钥模态框
|
// 显示添加API密钥模态框
|
||||||
showAddApiKeyModal() {
|
showAddApiKeyModal() {
|
||||||
const modal = document.getElementById('modal');
|
const modal = document.getElementById('modal');
|
||||||
@@ -1041,7 +1047,7 @@ class CLIProxyManager {
|
|||||||
return this.normalizeBase(`${protocol}//${hostname}${normalizedPort}`);
|
return this.normalizeBase(`${protocol}//${hostname}${normalizedPort}`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn('无法从当前地址检测 API 基础地址,使用默认设置', error);
|
console.warn('无法从当前地址检测 API 基础地址,使用默认设置', error);
|
||||||
return this.normalizeBase(this.apiBase || 'http://localhost:8317');
|
return this.normalizeBase(this.apiBase || `http://localhost:${DEFAULT_API_PORT}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1156,6 +1162,11 @@ Object.assign(
|
|||||||
aiProvidersModule
|
aiProvidersModule
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// 将工具函数绑定到原型上,供模块使用
|
||||||
|
CLIProxyManager.prototype.escapeHtml = escapeHtml;
|
||||||
|
CLIProxyManager.prototype.maskApiKey = maskApiKey;
|
||||||
|
CLIProxyManager.prototype.normalizeArrayResponse = normalizeArrayResponse;
|
||||||
|
|
||||||
// 全局管理器实例
|
// 全局管理器实例
|
||||||
let manager;
|
let manager;
|
||||||
|
|
||||||
|
|||||||
133
build.cjs
133
build.cjs
@@ -6,12 +6,12 @@ const path = require('path');
|
|||||||
const projectRoot = __dirname;
|
const projectRoot = __dirname;
|
||||||
const distDir = path.join(projectRoot, 'dist');
|
const distDir = path.join(projectRoot, 'dist');
|
||||||
|
|
||||||
const sourceFiles = {
|
const sourceFiles = {
|
||||||
html: path.join(projectRoot, 'index.html'),
|
html: path.join(projectRoot, 'index.html'),
|
||||||
css: path.join(projectRoot, 'styles.css'),
|
css: path.join(projectRoot, 'styles.css'),
|
||||||
i18n: path.join(projectRoot, 'i18n.js'),
|
i18n: path.join(projectRoot, 'i18n.js'),
|
||||||
app: path.join(projectRoot, 'app.js')
|
app: path.join(projectRoot, 'app.js')
|
||||||
};
|
};
|
||||||
|
|
||||||
const logoCandidates = ['logo.png', 'logo.jpg', 'logo.jpeg', 'logo.svg', 'logo.webp', 'logo.gif'];
|
const logoCandidates = ['logo.png', 'logo.jpg', 'logo.jpeg', 'logo.svg', 'logo.webp', 'logo.gif'];
|
||||||
const logoMimeMap = {
|
const logoMimeMap = {
|
||||||
@@ -78,44 +78,63 @@ function getVersion() {
|
|||||||
return 'v0.0.0-dev';
|
return 'v0.0.0-dev';
|
||||||
}
|
}
|
||||||
|
|
||||||
function ensureDistDir() {
|
function ensureDistDir() {
|
||||||
if (fs.existsSync(distDir)) {
|
if (fs.existsSync(distDir)) {
|
||||||
fs.rmSync(distDir, { recursive: true, force: true });
|
fs.rmSync(distDir, { recursive: true, force: true });
|
||||||
}
|
}
|
||||||
fs.mkdirSync(distDir);
|
fs.mkdirSync(distDir);
|
||||||
}
|
}
|
||||||
|
|
||||||
const importRegex = /^import\s+.+?from\s+['"](.+?)['"];?$/gm;
|
// 匹配各种 import 语句
|
||||||
const exportRegex = /^export\s+/gm;
|
const importRegex = /import\s+(?:{[^}]*}|[\w*\s,{}]+)\s+from\s+['"]([^'"]+)['"];?/gm;
|
||||||
|
// 匹配 export 关键字(包括 export const, export function, export class, export async function 等)
|
||||||
function bundleApp(entryPath) {
|
const exportRegex = /^export\s+(?=const|let|var|function|class|default|async)/gm;
|
||||||
const visited = new Set();
|
// 匹配单独的 export {} 或 export { ... } from '...'
|
||||||
|
const exportBraceRegex = /^export\s*{[^}]*}\s*(?:from\s+['"][^'"]+['"];?)?$/gm;
|
||||||
function inlineFile(filePath) {
|
|
||||||
let content = readFile(filePath);
|
function bundleApp(entryPath) {
|
||||||
const dir = path.dirname(filePath);
|
const visited = new Set();
|
||||||
|
const modules = [];
|
||||||
content = content.replace(importRegex, (match, specifier) => {
|
|
||||||
const targetPath = path.resolve(dir, specifier);
|
function inlineFile(filePath) {
|
||||||
const normalized = path.normalize(targetPath);
|
let content = readFile(filePath);
|
||||||
if (!fs.existsSync(normalized)) {
|
const dir = path.dirname(filePath);
|
||||||
throw new Error(`无法解析模块: ${specifier} (from ${filePath})`);
|
|
||||||
}
|
// 收集所有 import 语句
|
||||||
if (visited.has(normalized)) {
|
const imports = [];
|
||||||
return '';
|
content = content.replace(importRegex, (match, specifier) => {
|
||||||
}
|
const targetPath = path.resolve(dir, specifier);
|
||||||
visited.add(normalized);
|
const normalized = path.normalize(targetPath);
|
||||||
let moduleContent = inlineFile(normalized);
|
if (!fs.existsSync(normalized)) {
|
||||||
moduleContent = moduleContent.replace(exportRegex, '');
|
throw new Error(`无法解析模块: ${specifier} (from ${filePath})`);
|
||||||
const relativePath = path.relative(projectRoot, normalized);
|
}
|
||||||
return `\n// ${relativePath}\n${moduleContent}\n`;
|
if (!visited.has(normalized)) {
|
||||||
});
|
visited.add(normalized);
|
||||||
|
imports.push(normalized);
|
||||||
return content;
|
}
|
||||||
}
|
return ''; // 移除 import 语句
|
||||||
|
});
|
||||||
return inlineFile(entryPath);
|
|
||||||
}
|
// 移除 export 关键字
|
||||||
|
content = content.replace(exportRegex, '');
|
||||||
|
content = content.replace(exportBraceRegex, '');
|
||||||
|
|
||||||
|
// 处理依赖的模块
|
||||||
|
for (const importPath of imports) {
|
||||||
|
const moduleContent = inlineFile(importPath);
|
||||||
|
const relativePath = path.relative(projectRoot, importPath);
|
||||||
|
modules.push(`\n// ============ ${relativePath} ============\n${moduleContent}\n`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return content;
|
||||||
|
}
|
||||||
|
|
||||||
|
const mainContent = inlineFile(entryPath);
|
||||||
|
|
||||||
|
// 将所有模块内容组合在一起,模块在前,主文件在后
|
||||||
|
return modules.join('\n') + '\n// ============ Main ============\n' + mainContent;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
function loadLogoDataUrl() {
|
function loadLogoDataUrl() {
|
||||||
for (const candidate of logoCandidates) {
|
for (const candidate of logoCandidates) {
|
||||||
@@ -142,8 +161,8 @@ function build() {
|
|||||||
let html = readFile(sourceFiles.html);
|
let html = readFile(sourceFiles.html);
|
||||||
const css = escapeForStyle(readFile(sourceFiles.css));
|
const css = escapeForStyle(readFile(sourceFiles.css));
|
||||||
const i18n = escapeForScript(readFile(sourceFiles.i18n));
|
const i18n = escapeForScript(readFile(sourceFiles.i18n));
|
||||||
const bundledApp = bundleApp(sourceFiles.app);
|
const bundledApp = bundleApp(sourceFiles.app);
|
||||||
const app = escapeForScript(bundledApp);
|
const app = escapeForScript(bundledApp);
|
||||||
|
|
||||||
// 获取版本号并替换
|
// 获取版本号并替换
|
||||||
const version = getVersion();
|
const version = getVersion();
|
||||||
@@ -164,17 +183,17 @@ ${i18n}
|
|||||||
</script>`
|
</script>`
|
||||||
);
|
);
|
||||||
|
|
||||||
const scriptTagRegex = /<script[^>]*src="app\.js"[^>]*><\/script>/i;
|
const scriptTagRegex = /<script[^>]*src="app\.js"[^>]*><\/script>/i;
|
||||||
if (scriptTagRegex.test(html)) {
|
if (scriptTagRegex.test(html)) {
|
||||||
html = html.replace(
|
html = html.replace(
|
||||||
scriptTagRegex,
|
scriptTagRegex,
|
||||||
`<script>
|
`<script>
|
||||||
${app}
|
${app}
|
||||||
</script>`
|
</script>`
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
console.warn('未找到 app.js 脚本标签,未内联应用代码。');
|
console.warn('未找到 app.js 脚本标签,未内联应用代码。');
|
||||||
}
|
}
|
||||||
|
|
||||||
const logoDataUrl = loadLogoDataUrl();
|
const logoDataUrl = loadLogoDataUrl();
|
||||||
if (logoDataUrl) {
|
if (logoDataUrl) {
|
||||||
|
|||||||
231
src/core/error-handler.js
Normal file
231
src/core/error-handler.js
Normal file
@@ -0,0 +1,231 @@
|
|||||||
|
/**
|
||||||
|
* 错误处理器
|
||||||
|
* 统一管理应用中的错误处理逻辑
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { ERROR_MESSAGES } from '../utils/constants.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 错误处理器类
|
||||||
|
* 提供统一的错误处理接口,确保错误处理的一致性
|
||||||
|
*/
|
||||||
|
export class ErrorHandler {
|
||||||
|
/**
|
||||||
|
* 构造错误处理器
|
||||||
|
* @param {Object} notificationService - 通知服务对象
|
||||||
|
* @param {Function} notificationService.show - 显示通知的方法
|
||||||
|
*/
|
||||||
|
constructor(notificationService) {
|
||||||
|
this.notificationService = notificationService;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理更新操作失败
|
||||||
|
* 包括显示错误通知和执行UI回滚操作
|
||||||
|
*
|
||||||
|
* @param {Error} error - 错误对象
|
||||||
|
* @param {string} context - 操作上下文(如"调试模式"、"代理设置")
|
||||||
|
* @param {Function} [rollbackFn] - UI回滚函数
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* try {
|
||||||
|
* await this.makeRequest('/debug', { method: 'PATCH', body: JSON.stringify({ enabled: true }) });
|
||||||
|
* } catch (error) {
|
||||||
|
* this.errorHandler.handleUpdateError(
|
||||||
|
* error,
|
||||||
|
* '调试模式',
|
||||||
|
* () => document.getElementById('debug-toggle').checked = false
|
||||||
|
* );
|
||||||
|
* }
|
||||||
|
*/
|
||||||
|
handleUpdateError(error, context, rollbackFn) {
|
||||||
|
console.error(`更新${context}失败:`, error);
|
||||||
|
const message = `更新${context}失败: ${error.message || ERROR_MESSAGES.OPERATION_FAILED}`;
|
||||||
|
this.notificationService.show(message, 'error');
|
||||||
|
|
||||||
|
// 执行回滚操作
|
||||||
|
if (typeof rollbackFn === 'function') {
|
||||||
|
try {
|
||||||
|
rollbackFn();
|
||||||
|
} catch (rollbackError) {
|
||||||
|
console.error('UI回滚操作失败:', rollbackError);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理加载操作失败
|
||||||
|
*
|
||||||
|
* @param {Error} error - 错误对象
|
||||||
|
* @param {string} context - 加载内容的上下文(如"API密钥"、"使用统计")
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* try {
|
||||||
|
* const data = await this.makeRequest('/api-keys');
|
||||||
|
* this.renderApiKeys(data);
|
||||||
|
* } catch (error) {
|
||||||
|
* this.errorHandler.handleLoadError(error, 'API密钥');
|
||||||
|
* }
|
||||||
|
*/
|
||||||
|
handleLoadError(error, context) {
|
||||||
|
console.error(`加载${context}失败:`, error);
|
||||||
|
const message = `加载${context}失败,请检查连接`;
|
||||||
|
this.notificationService.show(message, 'error');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理删除操作失败
|
||||||
|
*
|
||||||
|
* @param {Error} error - 错误对象
|
||||||
|
* @param {string} context - 删除内容的上下文
|
||||||
|
*/
|
||||||
|
handleDeleteError(error, context) {
|
||||||
|
console.error(`删除${context}失败:`, error);
|
||||||
|
const message = `删除${context}失败: ${error.message || ERROR_MESSAGES.OPERATION_FAILED}`;
|
||||||
|
this.notificationService.show(message, 'error');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理添加操作失败
|
||||||
|
*
|
||||||
|
* @param {Error} error - 错误对象
|
||||||
|
* @param {string} context - 添加内容的上下文
|
||||||
|
*/
|
||||||
|
handleAddError(error, context) {
|
||||||
|
console.error(`添加${context}失败:`, error);
|
||||||
|
const message = `添加${context}失败: ${error.message || ERROR_MESSAGES.OPERATION_FAILED}`;
|
||||||
|
this.notificationService.show(message, 'error');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理网络错误
|
||||||
|
* 检测常见的网络问题并提供友好的错误提示
|
||||||
|
*
|
||||||
|
* @param {Error} error - 错误对象
|
||||||
|
*/
|
||||||
|
handleNetworkError(error) {
|
||||||
|
console.error('网络请求失败:', error);
|
||||||
|
|
||||||
|
let message = ERROR_MESSAGES.NETWORK_ERROR;
|
||||||
|
|
||||||
|
// 检测特定错误类型
|
||||||
|
if (error.name === 'TypeError' && error.message.includes('fetch')) {
|
||||||
|
message = ERROR_MESSAGES.NETWORK_ERROR;
|
||||||
|
} else if (error.message && error.message.includes('timeout')) {
|
||||||
|
message = ERROR_MESSAGES.TIMEOUT;
|
||||||
|
} else if (error.message && error.message.includes('401')) {
|
||||||
|
message = ERROR_MESSAGES.UNAUTHORIZED;
|
||||||
|
} else if (error.message && error.message.includes('404')) {
|
||||||
|
message = ERROR_MESSAGES.NOT_FOUND;
|
||||||
|
} else if (error.message && error.message.includes('500')) {
|
||||||
|
message = ERROR_MESSAGES.SERVER_ERROR;
|
||||||
|
} else if (error.message) {
|
||||||
|
message = `网络错误: ${error.message}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.notificationService.show(message, 'error');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理验证错误
|
||||||
|
*
|
||||||
|
* @param {string} fieldName - 字段名称
|
||||||
|
* @param {string} [message] - 自定义错误消息
|
||||||
|
*/
|
||||||
|
handleValidationError(fieldName, message) {
|
||||||
|
const errorMessage = message || `请输入有效的${fieldName}`;
|
||||||
|
this.notificationService.show(errorMessage, 'error');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理通用错误
|
||||||
|
* 当错误类型不明确时使用
|
||||||
|
*
|
||||||
|
* @param {Error} error - 错误对象
|
||||||
|
* @param {string} [defaultMessage] - 默认错误消息
|
||||||
|
*/
|
||||||
|
handleGenericError(error, defaultMessage) {
|
||||||
|
console.error('操作失败:', error);
|
||||||
|
const message = error.message || defaultMessage || ERROR_MESSAGES.OPERATION_FAILED;
|
||||||
|
this.notificationService.show(message, 'error');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建带错误处理的异步函数包装器
|
||||||
|
* 自动捕获并处理错误
|
||||||
|
*
|
||||||
|
* @param {Function} asyncFn - 异步函数
|
||||||
|
* @param {string} context - 操作上下文
|
||||||
|
* @param {Function} [rollbackFn] - 回滚函数
|
||||||
|
* @returns {Function} 包装后的函数
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* const safeUpdate = this.errorHandler.withErrorHandling(
|
||||||
|
* () => this.makeRequest('/debug', { method: 'PATCH', body: '...' }),
|
||||||
|
* '调试模式',
|
||||||
|
* () => document.getElementById('debug-toggle').checked = false
|
||||||
|
* );
|
||||||
|
* await safeUpdate();
|
||||||
|
*/
|
||||||
|
withErrorHandling(asyncFn, context, rollbackFn) {
|
||||||
|
return async (...args) => {
|
||||||
|
try {
|
||||||
|
return await asyncFn(...args);
|
||||||
|
} catch (error) {
|
||||||
|
this.handleUpdateError(error, context, rollbackFn);
|
||||||
|
throw error; // 重新抛出以便调用者处理
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建带重试机制的错误处理包装器
|
||||||
|
*
|
||||||
|
* @param {Function} asyncFn - 异步函数
|
||||||
|
* @param {number} [maxRetries=3] - 最大重试次数
|
||||||
|
* @param {number} [retryDelay=1000] - 重试延迟(毫秒)
|
||||||
|
* @returns {Function} 包装后的函数
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* const retryableFetch = this.errorHandler.withRetry(
|
||||||
|
* () => this.makeRequest('/config'),
|
||||||
|
* 3,
|
||||||
|
* 2000
|
||||||
|
* );
|
||||||
|
* const config = await retryableFetch();
|
||||||
|
*/
|
||||||
|
withRetry(asyncFn, maxRetries = 3, retryDelay = 1000) {
|
||||||
|
return async (...args) => {
|
||||||
|
let lastError;
|
||||||
|
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
||||||
|
try {
|
||||||
|
return await asyncFn(...args);
|
||||||
|
} catch (error) {
|
||||||
|
lastError = error;
|
||||||
|
console.warn(`尝试 ${attempt + 1}/${maxRetries} 失败:`, error);
|
||||||
|
|
||||||
|
if (attempt < maxRetries - 1) {
|
||||||
|
// 等待后重试
|
||||||
|
await new Promise(resolve => setTimeout(resolve, retryDelay));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 所有尝试都失败
|
||||||
|
throw lastError;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建错误处理器工厂函数
|
||||||
|
* 便于在不同模块中创建错误处理器实例
|
||||||
|
*
|
||||||
|
* @param {Function} showNotification - 显示通知的函数
|
||||||
|
* @returns {ErrorHandler} 错误处理器实例
|
||||||
|
*/
|
||||||
|
export function createErrorHandler(showNotification) {
|
||||||
|
return new ErrorHandler({
|
||||||
|
show: showNotification
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
// AI 提供商配置相关方法模块(当前只抽取 Gemini 相关逻辑)
|
// AI 提供商配置相关方法模块
|
||||||
// 这些函数依赖于 CLIProxyManager 实例上的 makeRequest/getConfig/clearCache/showNotification 等能力,
|
// 这些函数依赖于 CLIProxyManager 实例上的 makeRequest/getConfig/clearCache/showNotification 等能力,
|
||||||
// 以及 apiKeysModule 中的工具方法(如 applyHeadersToConfig/renderHeaderBadges)。
|
// 以及 apiKeysModule 中的工具方法(如 applyHeadersToConfig/renderHeaderBadges)。
|
||||||
|
|
||||||
|
|||||||
@@ -49,46 +49,8 @@ export const apiKeysModule = {
|
|||||||
}).join('');
|
}).join('');
|
||||||
},
|
},
|
||||||
|
|
||||||
// 遮蔽API密钥显示
|
// 注意: escapeHtml, maskApiKey, normalizeArrayResponse
|
||||||
maskApiKey(key) {
|
// 现在由 app.js 通过工具模块提供,通过 this 访问
|
||||||
if (key === null || key === undefined) {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
const normalizedKey = typeof key === 'string' ? key : String(key);
|
|
||||||
if (normalizedKey.length > 8) {
|
|
||||||
return normalizedKey.substring(0, 4) + '...' + normalizedKey.substring(normalizedKey.length - 4);
|
|
||||||
} else if (normalizedKey.length > 4) {
|
|
||||||
return normalizedKey.substring(0, 2) + '...' + normalizedKey.substring(normalizedKey.length - 2);
|
|
||||||
} else if (normalizedKey.length > 2) {
|
|
||||||
return normalizedKey.substring(0, 1) + '...' + normalizedKey.substring(normalizedKey.length - 1);
|
|
||||||
}
|
|
||||||
return normalizedKey;
|
|
||||||
},
|
|
||||||
|
|
||||||
// HTML 转义,防止 XSS
|
|
||||||
escapeHtml(value) {
|
|
||||||
if (value === null || value === undefined) return '';
|
|
||||||
return String(value)
|
|
||||||
.replace(/&/g, '&')
|
|
||||||
.replace(/</g, '<')
|
|
||||||
.replace(/>/g, '>')
|
|
||||||
.replace(/"/g, '"')
|
|
||||||
.replace(/'/g, ''');
|
|
||||||
},
|
|
||||||
|
|
||||||
// 兼容服务端返回的数组结构
|
|
||||||
normalizeArrayResponse(data, key) {
|
|
||||||
if (Array.isArray(data)) {
|
|
||||||
return data;
|
|
||||||
}
|
|
||||||
if (data && Array.isArray(data[key])) {
|
|
||||||
return data[key];
|
|
||||||
}
|
|
||||||
if (data && Array.isArray(data.items)) {
|
|
||||||
return data.items;
|
|
||||||
}
|
|
||||||
return [];
|
|
||||||
},
|
|
||||||
|
|
||||||
// 添加一行自定义请求头输入
|
// 添加一行自定义请求头输入
|
||||||
addHeaderField(wrapperId, header = {}) {
|
addHeaderField(wrapperId, header = {}) {
|
||||||
|
|||||||
172
src/utils/array.js
Normal file
172
src/utils/array.js
Normal file
@@ -0,0 +1,172 @@
|
|||||||
|
/**
|
||||||
|
* 数组工具函数模块
|
||||||
|
* 提供数组处理、规范化、排序等功能
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 规范化 API 响应中的数组数据
|
||||||
|
* 兼容多种服务端返回格式
|
||||||
|
*
|
||||||
|
* @param {*} data - API 响应数据
|
||||||
|
* @param {string} [key] - 数组字段的键名
|
||||||
|
* @returns {Array} 规范化后的数组
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* // 直接返回数组
|
||||||
|
* normalizeArrayResponse([1, 2, 3])
|
||||||
|
* // 返回: [1, 2, 3]
|
||||||
|
*
|
||||||
|
* // 从对象中提取数组
|
||||||
|
* normalizeArrayResponse({ 'api-keys': ['key1', 'key2'] }, 'api-keys')
|
||||||
|
* // 返回: ['key1', 'key2']
|
||||||
|
*
|
||||||
|
* // 从 items 字段提取
|
||||||
|
* normalizeArrayResponse({ items: ['a', 'b'] })
|
||||||
|
* // 返回: ['a', 'b']
|
||||||
|
*/
|
||||||
|
export function normalizeArrayResponse(data, key) {
|
||||||
|
// 如果本身就是数组,直接返回
|
||||||
|
if (Array.isArray(data)) {
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
// 如果指定了 key,尝试从对象中提取
|
||||||
|
if (key && data && Array.isArray(data[key])) {
|
||||||
|
return data[key];
|
||||||
|
}
|
||||||
|
// 尝试从 items 字段提取(通用分页格式)
|
||||||
|
if (data && Array.isArray(data.items)) {
|
||||||
|
return data.items;
|
||||||
|
}
|
||||||
|
// 默认返回空数组
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 数组去重
|
||||||
|
* @param {Array} arr - 原数组
|
||||||
|
* @param {Function} [keyFn] - 提取键的函数,用于对象数组去重
|
||||||
|
* @returns {Array} 去重后的数组
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* uniqueArray([1, 2, 2, 3])
|
||||||
|
* // 返回: [1, 2, 3]
|
||||||
|
*
|
||||||
|
* uniqueArray([{id: 1}, {id: 2}, {id: 1}], item => item.id)
|
||||||
|
* // 返回: [{id: 1}, {id: 2}]
|
||||||
|
*/
|
||||||
|
export function uniqueArray(arr, keyFn) {
|
||||||
|
if (!Array.isArray(arr)) return [];
|
||||||
|
|
||||||
|
if (keyFn) {
|
||||||
|
const seen = new Set();
|
||||||
|
return arr.filter(item => {
|
||||||
|
const key = keyFn(item);
|
||||||
|
if (seen.has(key)) return false;
|
||||||
|
seen.add(key);
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return [...new Set(arr)];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 数组分组
|
||||||
|
* @param {Array} arr - 原数组
|
||||||
|
* @param {Function} keyFn - 提取分组键的函数
|
||||||
|
* @returns {Object} 分组后的对象
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* groupBy([{type: 'a', val: 1}, {type: 'b', val: 2}, {type: 'a', val: 3}], item => item.type)
|
||||||
|
* // 返回: { a: [{type: 'a', val: 1}, {type: 'a', val: 3}], b: [{type: 'b', val: 2}] }
|
||||||
|
*/
|
||||||
|
export function groupBy(arr, keyFn) {
|
||||||
|
if (!Array.isArray(arr)) return {};
|
||||||
|
|
||||||
|
return arr.reduce((groups, item) => {
|
||||||
|
const key = keyFn(item);
|
||||||
|
if (!groups[key]) {
|
||||||
|
groups[key] = [];
|
||||||
|
}
|
||||||
|
groups[key].push(item);
|
||||||
|
return groups;
|
||||||
|
}, {});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 数组分块
|
||||||
|
* @param {Array} arr - 原数组
|
||||||
|
* @param {number} size - 每块大小
|
||||||
|
* @returns {Array<Array>} 分块后的二维数组
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* chunk([1, 2, 3, 4, 5], 2)
|
||||||
|
* // 返回: [[1, 2], [3, 4], [5]]
|
||||||
|
*/
|
||||||
|
export function chunk(arr, size) {
|
||||||
|
if (!Array.isArray(arr) || size < 1) return [];
|
||||||
|
|
||||||
|
const result = [];
|
||||||
|
for (let i = 0; i < arr.length; i += size) {
|
||||||
|
result.push(arr.slice(i, i + size));
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 数组排序(不改变原数组)
|
||||||
|
* @param {Array} arr - 原数组
|
||||||
|
* @param {Function} compareFn - 比较函数
|
||||||
|
* @returns {Array} 排序后的新数组
|
||||||
|
*/
|
||||||
|
export function sortArray(arr, compareFn) {
|
||||||
|
if (!Array.isArray(arr)) return [];
|
||||||
|
return [...arr].sort(compareFn);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按字段排序对象数组
|
||||||
|
* @param {Array} arr - 对象数组
|
||||||
|
* @param {string} key - 排序字段
|
||||||
|
* @param {string} order - 排序顺序 'asc' 或 'desc'
|
||||||
|
* @returns {Array} 排序后的新数组
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* sortByKey([{age: 25}, {age: 20}, {age: 30}], 'age', 'asc')
|
||||||
|
* // 返回: [{age: 20}, {age: 25}, {age: 30}]
|
||||||
|
*/
|
||||||
|
export function sortByKey(arr, key, order = 'asc') {
|
||||||
|
if (!Array.isArray(arr)) return [];
|
||||||
|
|
||||||
|
return [...arr].sort((a, b) => {
|
||||||
|
const aVal = a[key];
|
||||||
|
const bVal = b[key];
|
||||||
|
|
||||||
|
if (aVal < bVal) return order === 'asc' ? -1 : 1;
|
||||||
|
if (aVal > bVal) return order === 'asc' ? 1 : -1;
|
||||||
|
return 0;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 安全地获取数组元素
|
||||||
|
* @param {Array} arr - 数组
|
||||||
|
* @param {number} index - 索引
|
||||||
|
* @param {*} defaultValue - 默认值
|
||||||
|
* @returns {*} 数组元素或默认值
|
||||||
|
*/
|
||||||
|
export function safeGet(arr, index, defaultValue = undefined) {
|
||||||
|
if (!Array.isArray(arr) || index < 0 || index >= arr.length) {
|
||||||
|
return defaultValue;
|
||||||
|
}
|
||||||
|
return arr[index];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查数组是否为空
|
||||||
|
* @param {*} arr - 待检查的值
|
||||||
|
* @returns {boolean} 是否为空数组
|
||||||
|
*/
|
||||||
|
export function isEmptyArray(arr) {
|
||||||
|
return !Array.isArray(arr) || arr.length === 0;
|
||||||
|
}
|
||||||
274
src/utils/constants.js
Normal file
274
src/utils/constants.js
Normal file
@@ -0,0 +1,274 @@
|
|||||||
|
/**
|
||||||
|
* 常量配置文件
|
||||||
|
* 集中管理应用中的所有常量,避免魔法数字和硬编码字符串
|
||||||
|
*/
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// 时间相关常量(毫秒)
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 配置缓存过期时间(30秒)
|
||||||
|
* 用于减少服务器压力,避免频繁请求配置数据
|
||||||
|
*/
|
||||||
|
export const CACHE_EXPIRY_MS = 30 * 1000;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通知显示持续时间(3秒)
|
||||||
|
* 成功/错误/信息提示框的自动消失时间
|
||||||
|
*/
|
||||||
|
export const NOTIFICATION_DURATION_MS = 3 * 1000;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 状态更新定时器间隔(1秒)
|
||||||
|
* 连接状态和系统信息的更新频率
|
||||||
|
*/
|
||||||
|
export const STATUS_UPDATE_INTERVAL_MS = 1 * 1000;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 日志刷新延迟(500毫秒)
|
||||||
|
* 日志自动刷新的去抖延迟
|
||||||
|
*/
|
||||||
|
export const LOG_REFRESH_DELAY_MS = 500;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* OAuth 状态轮询间隔(2秒)
|
||||||
|
* 检查 OAuth 认证完成状态的轮询频率
|
||||||
|
*/
|
||||||
|
export const OAUTH_POLL_INTERVAL_MS = 2 * 1000;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* OAuth 最大轮询时间(5分钟)
|
||||||
|
* 超过此时间后停止轮询,认为授权超时
|
||||||
|
*/
|
||||||
|
export const OAUTH_MAX_POLL_DURATION_MS = 5 * 60 * 1000;
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// 数据限制常量
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 最大日志显示行数
|
||||||
|
* 限制内存占用,避免大量日志导致页面卡顿
|
||||||
|
*/
|
||||||
|
export const MAX_LOG_LINES = 10000;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 认证文件列表默认每页显示数量
|
||||||
|
*/
|
||||||
|
export const DEFAULT_AUTH_FILES_PAGE_SIZE = 9;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 认证文件每页最小显示数量
|
||||||
|
*/
|
||||||
|
export const MIN_AUTH_FILES_PAGE_SIZE = 3;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 认证文件每页最大显示数量
|
||||||
|
*/
|
||||||
|
export const MAX_AUTH_FILES_PAGE_SIZE = 60;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 使用统计图表最大数据点数
|
||||||
|
* 超过此数量将进行聚合,提高渲染性能
|
||||||
|
*/
|
||||||
|
export const MAX_CHART_DATA_POINTS = 100;
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// 网络相关常量
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 默认 API 服务器端口
|
||||||
|
*/
|
||||||
|
export const DEFAULT_API_PORT = 8317;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 默认 API 基础路径
|
||||||
|
*/
|
||||||
|
export const DEFAULT_API_BASE = `http://localhost:${DEFAULT_API_PORT}`;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 管理 API 路径前缀
|
||||||
|
*/
|
||||||
|
export const MANAGEMENT_API_PREFIX = '/v0/management';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 请求超时时间(30秒)
|
||||||
|
*/
|
||||||
|
export const REQUEST_TIMEOUT_MS = 30 * 1000;
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// OAuth 相关常量
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* OAuth 卡片元素 ID 列表
|
||||||
|
* 用于根据主机环境隐藏/显示不同的 OAuth 选项
|
||||||
|
*/
|
||||||
|
export const OAUTH_CARD_IDS = [
|
||||||
|
'codex-oauth-card',
|
||||||
|
'anthropic-oauth-card',
|
||||||
|
'gemini-cli-oauth-card',
|
||||||
|
'qwen-oauth-card',
|
||||||
|
'iflow-oauth-card'
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* OAuth 提供商名称映射
|
||||||
|
*/
|
||||||
|
export const OAUTH_PROVIDERS = {
|
||||||
|
CODEX: 'codex',
|
||||||
|
ANTHROPIC: 'anthropic',
|
||||||
|
GEMINI_CLI: 'gemini-cli',
|
||||||
|
QWEN: 'qwen',
|
||||||
|
IFLOW: 'iflow'
|
||||||
|
};
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// 本地存储键名
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 本地存储键名前缀
|
||||||
|
*/
|
||||||
|
export const STORAGE_PREFIX = 'cliProxyApi_';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 存储 API 基础地址的键名
|
||||||
|
*/
|
||||||
|
export const STORAGE_KEY_API_BASE = `${STORAGE_PREFIX}apiBase`;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 存储管理密钥的键名
|
||||||
|
*/
|
||||||
|
export const STORAGE_KEY_MANAGEMENT_KEY = `${STORAGE_PREFIX}managementKey`;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 存储主题偏好的键名
|
||||||
|
*/
|
||||||
|
export const STORAGE_KEY_THEME = `${STORAGE_PREFIX}theme`;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 存储语言偏好的键名
|
||||||
|
*/
|
||||||
|
export const STORAGE_KEY_LANGUAGE = `${STORAGE_PREFIX}language`;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 存储认证文件页大小的键名
|
||||||
|
*/
|
||||||
|
export const STORAGE_KEY_AUTH_FILES_PAGE_SIZE = `${STORAGE_PREFIX}authFilesPageSize`;
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// UI 相关常量
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 主题选项
|
||||||
|
*/
|
||||||
|
export const THEMES = {
|
||||||
|
LIGHT: 'light',
|
||||||
|
DARK: 'dark'
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 支持的语言
|
||||||
|
*/
|
||||||
|
export const LANGUAGES = {
|
||||||
|
ZH_CN: 'zh-CN',
|
||||||
|
EN_US: 'en-US'
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通知类型
|
||||||
|
*/
|
||||||
|
export const NOTIFICATION_TYPES = {
|
||||||
|
SUCCESS: 'success',
|
||||||
|
ERROR: 'error',
|
||||||
|
INFO: 'info',
|
||||||
|
WARNING: 'warning'
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 模态框尺寸
|
||||||
|
*/
|
||||||
|
export const MODAL_SIZES = {
|
||||||
|
SMALL: 'small',
|
||||||
|
MEDIUM: 'medium',
|
||||||
|
LARGE: 'large'
|
||||||
|
};
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// 正则表达式常量
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* URL 验证正则
|
||||||
|
*/
|
||||||
|
export const URL_PATTERN = /^https?:\/\/.+/;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Email 验证正则
|
||||||
|
*/
|
||||||
|
export const EMAIL_PATTERN = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 端口号验证正则(1-65535)
|
||||||
|
*/
|
||||||
|
export const PORT_PATTERN = /^([1-9][0-9]{0,3}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])$/;
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// 文件类型常量
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 支持的认证文件类型
|
||||||
|
*/
|
||||||
|
export const AUTH_FILE_TYPES = {
|
||||||
|
JSON: 'application/json',
|
||||||
|
YAML: 'application/x-yaml'
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 认证文件最大大小(10MB)
|
||||||
|
*/
|
||||||
|
export const MAX_AUTH_FILE_SIZE = 10 * 1024 * 1024;
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// API 端点常量
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 常用 API 端点路径
|
||||||
|
*/
|
||||||
|
export const API_ENDPOINTS = {
|
||||||
|
CONFIG: '/config',
|
||||||
|
DEBUG: '/debug',
|
||||||
|
API_KEYS: '/api-keys',
|
||||||
|
PROVIDERS: '/providers',
|
||||||
|
AUTH_FILES: '/auth-files',
|
||||||
|
LOGS: '/logs',
|
||||||
|
USAGE_STATS: '/usage-stats',
|
||||||
|
CONNECTION: '/connection',
|
||||||
|
CODEX_API_KEY: '/codex-api-key',
|
||||||
|
ANTHROPIC_API_KEY: '/anthropic-api-key',
|
||||||
|
GEMINI_API_KEY: '/gemini-api-key',
|
||||||
|
OPENAI_API_KEY: '/openai-api-key'
|
||||||
|
};
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// 错误消息常量
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通用错误消息
|
||||||
|
*/
|
||||||
|
export const ERROR_MESSAGES = {
|
||||||
|
NETWORK_ERROR: '网络连接失败,请检查服务器状态',
|
||||||
|
TIMEOUT: '请求超时,请稍后重试',
|
||||||
|
UNAUTHORIZED: '未授权,请检查管理密钥',
|
||||||
|
NOT_FOUND: '资源不存在',
|
||||||
|
SERVER_ERROR: '服务器错误,请联系管理员',
|
||||||
|
INVALID_INPUT: '输入数据无效',
|
||||||
|
OPERATION_FAILED: '操作失败,请稍后重试'
|
||||||
|
};
|
||||||
62
src/utils/html.js
Normal file
62
src/utils/html.js
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
/**
|
||||||
|
* HTML 工具函数模块
|
||||||
|
* 提供 HTML 字符串处理、XSS 防护等功能
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* HTML 转义,防止 XSS 攻击
|
||||||
|
* @param {*} value - 需要转义的值
|
||||||
|
* @returns {string} 转义后的字符串
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* escapeHtml('<script>alert("xss")</script>')
|
||||||
|
* // 返回: '<script>alert("xss")</script>'
|
||||||
|
*/
|
||||||
|
export function escapeHtml(value) {
|
||||||
|
if (value === null || value === undefined) return '';
|
||||||
|
return String(value)
|
||||||
|
.replace(/&/g, '&')
|
||||||
|
.replace(/</g, '<')
|
||||||
|
.replace(/>/g, '>')
|
||||||
|
.replace(/"/g, '"')
|
||||||
|
.replace(/'/g, ''');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* HTML 反转义
|
||||||
|
* @param {string} html - 需要反转义的 HTML 字符串
|
||||||
|
* @returns {string} 反转义后的字符串
|
||||||
|
*/
|
||||||
|
export function unescapeHtml(html) {
|
||||||
|
if (!html) return '';
|
||||||
|
const textarea = document.createElement('textarea');
|
||||||
|
textarea.innerHTML = html;
|
||||||
|
return textarea.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 去除 HTML 标签,只保留文本内容
|
||||||
|
* @param {string} html - HTML 字符串
|
||||||
|
* @returns {string} 纯文本内容
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* stripHtmlTags('<p>Hello <strong>World</strong></p>')
|
||||||
|
* // 返回: 'Hello World'
|
||||||
|
*/
|
||||||
|
export function stripHtmlTags(html) {
|
||||||
|
if (!html) return '';
|
||||||
|
const div = document.createElement('div');
|
||||||
|
div.innerHTML = html;
|
||||||
|
return div.textContent || div.innerText || '';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 安全地设置元素的 HTML 内容
|
||||||
|
* @param {HTMLElement} element - 目标元素
|
||||||
|
* @param {string} html - HTML 内容
|
||||||
|
* @param {boolean} escape - 是否转义(默认 true)
|
||||||
|
*/
|
||||||
|
export function setSafeHtml(element, html, escape = true) {
|
||||||
|
if (!element) return;
|
||||||
|
element.innerHTML = escape ? escapeHtml(html) : html;
|
||||||
|
}
|
||||||
116
src/utils/string.js
Normal file
116
src/utils/string.js
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
/**
|
||||||
|
* 字符串工具函数模块
|
||||||
|
* 提供字符串处理、格式化、掩码等功能
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 遮蔽 API 密钥显示,保护敏感信息
|
||||||
|
* @param {*} key - API 密钥
|
||||||
|
* @returns {string} 遮蔽后的密钥字符串
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* maskApiKey('sk-1234567890abcdef')
|
||||||
|
* // 返回: 'sk-1...cdef'
|
||||||
|
*/
|
||||||
|
export function maskApiKey(key) {
|
||||||
|
if (key === null || key === undefined) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
const normalizedKey = typeof key === 'string' ? key : String(key);
|
||||||
|
if (normalizedKey.length > 8) {
|
||||||
|
return normalizedKey.substring(0, 4) + '...' + normalizedKey.substring(normalizedKey.length - 4);
|
||||||
|
} else if (normalizedKey.length > 4) {
|
||||||
|
return normalizedKey.substring(0, 2) + '...' + normalizedKey.substring(normalizedKey.length - 2);
|
||||||
|
} else if (normalizedKey.length > 2) {
|
||||||
|
return normalizedKey.substring(0, 1) + '...' + normalizedKey.substring(normalizedKey.length - 1);
|
||||||
|
}
|
||||||
|
return normalizedKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 截断字符串到指定长度,超出部分用省略号代替
|
||||||
|
* @param {string} str - 原字符串
|
||||||
|
* @param {number} maxLength - 最大长度
|
||||||
|
* @param {string} suffix - 后缀(默认 '...')
|
||||||
|
* @returns {string} 截断后的字符串
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* truncateString('This is a very long string', 10)
|
||||||
|
* // 返回: 'This is...'
|
||||||
|
*/
|
||||||
|
export function truncateString(str, maxLength, suffix = '...') {
|
||||||
|
if (!str || str.length <= maxLength) return str || '';
|
||||||
|
return str.substring(0, maxLength - suffix.length) + suffix;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 格式化文件大小
|
||||||
|
* @param {number} bytes - 字节数
|
||||||
|
* @param {number} decimals - 小数位数(默认 2)
|
||||||
|
* @returns {string} 格式化后的大小字符串
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* formatFileSize(1536)
|
||||||
|
* // 返回: '1.50 KB'
|
||||||
|
*/
|
||||||
|
export function formatFileSize(bytes, decimals = 2) {
|
||||||
|
if (bytes === 0) return '0 Bytes';
|
||||||
|
if (!bytes || isNaN(bytes)) return '';
|
||||||
|
|
||||||
|
const k = 1024;
|
||||||
|
const dm = decimals < 0 ? 0 : decimals;
|
||||||
|
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB'];
|
||||||
|
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||||
|
|
||||||
|
return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 首字母大写
|
||||||
|
* @param {string} str - 原字符串
|
||||||
|
* @returns {string} 首字母大写后的字符串
|
||||||
|
*/
|
||||||
|
export function capitalize(str) {
|
||||||
|
if (!str) return '';
|
||||||
|
return str.charAt(0).toUpperCase() + str.slice(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成随机字符串
|
||||||
|
* @param {number} length - 字符串长度
|
||||||
|
* @param {string} charset - 字符集(默认字母数字)
|
||||||
|
* @returns {string} 随机字符串
|
||||||
|
*/
|
||||||
|
export function randomString(length = 8, charset = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789') {
|
||||||
|
let result = '';
|
||||||
|
for (let i = 0; i < length; i++) {
|
||||||
|
result += charset.charAt(Math.floor(Math.random() * charset.length));
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查字符串是否为空或仅包含空白字符
|
||||||
|
* @param {string} str - 待检查的字符串
|
||||||
|
* @returns {boolean} 是否为空
|
||||||
|
*/
|
||||||
|
export function isBlank(str) {
|
||||||
|
return !str || /^\s*$/.test(str);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将字符串转换为 kebab-case
|
||||||
|
* @param {string} str - 原字符串
|
||||||
|
* @returns {string} kebab-case 字符串
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* toKebabCase('helloWorld')
|
||||||
|
* // 返回: 'hello-world'
|
||||||
|
*/
|
||||||
|
export function toKebabCase(str) {
|
||||||
|
if (!str) return '';
|
||||||
|
return str
|
||||||
|
.replace(/([a-z])([A-Z])/g, '$1-$2')
|
||||||
|
.replace(/[\s_]+/g, '-')
|
||||||
|
.toLowerCase();
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user