chore: restructure project and add i18n support
- Reorganize project structure with new electron and shared directories - Add comprehensive i18n support with Chinese, English, and Japanese locales - Update build configurations and TypeScript paths for new structure - Add various UI components including chat interface and task management - Include Windows release binaries and localization files - Update dependencies and fix import paths throughout the codebase
This commit is contained in:
330
src/lib/WebSocketManager.ts
Normal file
330
src/lib/WebSocketManager.ts
Normal file
@@ -0,0 +1,330 @@
|
||||
/**
|
||||
* 简化的 WebSocket 管理器(Web 版)
|
||||
* 专门负责 WebSocket 连接和消息传输,不包含打字机逻辑
|
||||
*/
|
||||
|
||||
import { IdUtils, CallbackUtils, MessageUtils, TimerUtils } from './index'
|
||||
|
||||
/* =======================
|
||||
* 类型定义
|
||||
* ======================= */
|
||||
|
||||
export interface WebSocketCallbacks {
|
||||
onConnect?: (event?: Event) => void
|
||||
onDisconnect?: (event?: CloseEvent) => void
|
||||
onError?: (error: any) => void
|
||||
onMessage?: (message: any) => void
|
||||
getConversationId?: () => string
|
||||
getAgentId?: () => string
|
||||
}
|
||||
|
||||
export interface WebSocketManagerOptions extends WebSocketCallbacks {
|
||||
wsUrl?: string
|
||||
protocols?: string[]
|
||||
reconnectInterval?: number
|
||||
maxReconnectAttempts?: number
|
||||
heartbeatInterval?: number
|
||||
messageId?: string
|
||||
baseDelay?: number
|
||||
retries?: number
|
||||
maxDelay?: number
|
||||
tryReconnect?: boolean
|
||||
}
|
||||
|
||||
export interface QueuedMessage {
|
||||
[key: string]: any
|
||||
retryCount?: number
|
||||
timestamp?: number
|
||||
}
|
||||
|
||||
export interface WebSocketStats {
|
||||
messagesReceived: number
|
||||
messagesSent: number
|
||||
messagesDropped: number
|
||||
reconnectCount: number
|
||||
connectionStartTime: number | null
|
||||
lastMessageTime: number | null
|
||||
}
|
||||
|
||||
/* =======================
|
||||
* WebSocketManager
|
||||
* ======================= */
|
||||
|
||||
export class WebSocketManager {
|
||||
private wsUrl = ''
|
||||
private protocols: string[] = []
|
||||
private reconnectInterval = 3000
|
||||
private maxReconnectAttempts = 5
|
||||
private heartbeatInterval = 30000
|
||||
|
||||
private ws: WebSocket | null = null
|
||||
private reconnectAttempts = 0
|
||||
private isConnecting = false
|
||||
private connectionState = false
|
||||
|
||||
private heartbeatTimer: number | null = null
|
||||
private reconnectTimer: number | null = null
|
||||
|
||||
private callbacks: Required<WebSocketCallbacks>
|
||||
|
||||
private messageQueue: QueuedMessage[] = []
|
||||
|
||||
private stats: WebSocketStats = {
|
||||
messagesReceived: 0,
|
||||
messagesSent: 0,
|
||||
messagesDropped: 0,
|
||||
reconnectCount: 0,
|
||||
connectionStartTime: null,
|
||||
lastMessageTime: null,
|
||||
}
|
||||
|
||||
constructor(options: WebSocketManagerOptions = {}) {
|
||||
this.wsUrl = options.wsUrl ?? ''
|
||||
this.protocols = options.protocols ?? []
|
||||
this.reconnectInterval = options.reconnectInterval ?? 3000
|
||||
this.maxReconnectAttempts = options.maxReconnectAttempts ?? 5
|
||||
this.heartbeatInterval = options.heartbeatInterval ?? 30000
|
||||
|
||||
this.callbacks = {
|
||||
onConnect: options.onConnect ?? (() => { }),
|
||||
onDisconnect: options.onDisconnect ?? (() => { }),
|
||||
onError: options.onError ?? (() => { }),
|
||||
onMessage: options.onMessage ?? (() => { }),
|
||||
getConversationId: options.getConversationId ?? (() => ''),
|
||||
getAgentId: options.getAgentId ?? (() => ''),
|
||||
}
|
||||
}
|
||||
|
||||
/* =======================
|
||||
* 内部工具
|
||||
* ======================= */
|
||||
|
||||
private safeCall(name: keyof WebSocketCallbacks, ...args: any[]): void {
|
||||
CallbackUtils.safeCall(this.callbacks, name as string, ...args)
|
||||
}
|
||||
|
||||
/* =======================
|
||||
* 连接管理
|
||||
* ======================= */
|
||||
|
||||
async init(wsUrl?: string): Promise<void> {
|
||||
if (wsUrl) this.wsUrl = wsUrl
|
||||
if (!this.wsUrl) throw new Error('WebSocket URL is required')
|
||||
await this.connect()
|
||||
}
|
||||
|
||||
// 改进方案:让connect()真正等待连接
|
||||
async connect(): Promise<void> {
|
||||
console.log('[WebSocket] connect() called, isConnecting:', this.isConnecting, 'connectionState:', this.connectionState)
|
||||
if (this.isConnecting || this.connectionState) {
|
||||
console.log('[WebSocket] Already connecting or connected, returning early')
|
||||
return
|
||||
}
|
||||
this.isConnecting = true
|
||||
console.log('[WebSocket] Starting connection...')
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
try {
|
||||
console.log('[WebSocket] About to create new WebSocket with URL:', this.wsUrl)
|
||||
this.ws = new WebSocket(this.wsUrl, this.protocols)
|
||||
console.log('[WebSocket] WebSocket object created, readyState:', this.ws?.readyState)
|
||||
|
||||
// 包装handleOpen以resolve Promise
|
||||
this.ws.onopen = (event: Event) => {
|
||||
console.log('[WebSocket] onopen event fired')
|
||||
this.handleOpen(event)
|
||||
resolve() // ← 真正的连接成功
|
||||
}
|
||||
|
||||
this.ws.onmessage = this.handleMessage
|
||||
this.ws.onclose = (event: CloseEvent) => {
|
||||
console.log('[WebSocket] onclose event fired, code:', event.code, 'reason:', event.reason)
|
||||
this.handleClose(event)
|
||||
}
|
||||
this.ws.onerror = (error: Event) => {
|
||||
console.log('[WebSocket] onerror event fired', error)
|
||||
this.handleError(error)
|
||||
reject(error) // ← Promise拒绝
|
||||
}
|
||||
} catch (error) {
|
||||
this.isConnecting = false
|
||||
this.safeCall('onError', error)
|
||||
this.scheduleReconnect()
|
||||
reject(error)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private handleOpen = (event: Event): void => {
|
||||
this.isConnecting = false
|
||||
this.connectionState = true
|
||||
this.reconnectAttempts = 0
|
||||
this.stats.connectionStartTime = Date.now()
|
||||
|
||||
this.startHeartbeat()
|
||||
this.safeCall('onConnect', event)
|
||||
this.processQueue()
|
||||
}
|
||||
|
||||
private handleMessage = (event: MessageEvent): void => {
|
||||
const raw = event.data
|
||||
if (MessageUtils.isPongMessage(raw)) return
|
||||
|
||||
const data =
|
||||
typeof raw === 'string'
|
||||
? MessageUtils.safeParseJSON(raw)
|
||||
: raw
|
||||
|
||||
if (!data) return
|
||||
|
||||
this.stats.messagesReceived++
|
||||
this.stats.lastMessageTime = Date.now()
|
||||
|
||||
this.safeCall('onMessage', data)
|
||||
}
|
||||
|
||||
private handleClose = (event: CloseEvent): void => {
|
||||
this.connectionState = false
|
||||
this.isConnecting = false
|
||||
this.stopHeartbeat()
|
||||
|
||||
this.safeCall('onDisconnect', event)
|
||||
|
||||
if (event.code !== 1000 && this.reconnectAttempts < this.maxReconnectAttempts) {
|
||||
this.scheduleReconnect()
|
||||
}
|
||||
}
|
||||
|
||||
private handleError = (error: Event): void => {
|
||||
this.connectionState = false
|
||||
this.isConnecting = false
|
||||
|
||||
this.safeCall('onError', {
|
||||
type: 'WEBSOCKET_ERROR',
|
||||
error,
|
||||
})
|
||||
|
||||
if (!this.reconnectTimer) {
|
||||
this.scheduleReconnect()
|
||||
}
|
||||
}
|
||||
|
||||
/* =======================
|
||||
* 消息发送
|
||||
* ======================= */
|
||||
|
||||
sendMessage(message: QueuedMessage): boolean {
|
||||
const data = {
|
||||
...message,
|
||||
timestamp: Date.now(),
|
||||
retryCount: message.retryCount ?? 0,
|
||||
}
|
||||
|
||||
if (!this.isConnected()) {
|
||||
this.messageQueue.push(data)
|
||||
this.connect()
|
||||
return false
|
||||
}
|
||||
|
||||
try {
|
||||
this.ws!.send(JSON.stringify(data))
|
||||
this.stats.messagesSent++
|
||||
return true
|
||||
} catch (error) {
|
||||
this.messageQueue.push(data)
|
||||
this.safeCall('onError', error)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
private processQueue(): void {
|
||||
while (this.messageQueue.length) {
|
||||
const msg = this.messageQueue.shift()!
|
||||
if ((msg.retryCount ?? 0) >= 3) {
|
||||
this.stats.messagesDropped++
|
||||
continue
|
||||
}
|
||||
msg.retryCount = (msg.retryCount ?? 0) + 1
|
||||
this.sendMessage(msg)
|
||||
}
|
||||
}
|
||||
|
||||
/* =======================
|
||||
* 心跳 & 重连
|
||||
* ======================= */
|
||||
|
||||
private startHeartbeat(): void {
|
||||
this.stopHeartbeat()
|
||||
this.heartbeatTimer = window.setInterval(() => {
|
||||
if (!this.isConnected()) return
|
||||
|
||||
this.sendMessage({
|
||||
messageType: '3',
|
||||
messageContent: 'heartbeat',
|
||||
messageId: IdUtils.generateMessageId(),
|
||||
conversationId: this.callbacks.getConversationId(),
|
||||
agentId: this.callbacks.getAgentId(),
|
||||
})
|
||||
}, this.heartbeatInterval)
|
||||
}
|
||||
|
||||
private stopHeartbeat(): void {
|
||||
this.heartbeatTimer = TimerUtils.clearTimer(this.heartbeatTimer, 'interval')
|
||||
}
|
||||
|
||||
private scheduleReconnect(): void {
|
||||
if (this.reconnectAttempts >= this.maxReconnectAttempts) return
|
||||
|
||||
this.reconnectAttempts++
|
||||
this.stats.reconnectCount++
|
||||
|
||||
const delay = Math.min(
|
||||
this.reconnectInterval * Math.pow(1.5, this.reconnectAttempts - 1),
|
||||
30000
|
||||
)
|
||||
|
||||
this.reconnectTimer = window.setTimeout(() => {
|
||||
this.reconnectTimer = null
|
||||
this.connect()
|
||||
}, delay)
|
||||
}
|
||||
|
||||
/* =======================
|
||||
* 对外 API
|
||||
* ======================= */
|
||||
|
||||
isConnected(): boolean {
|
||||
return (
|
||||
this.connectionState &&
|
||||
!!this.ws &&
|
||||
this.ws.readyState === WebSocket.OPEN
|
||||
)
|
||||
}
|
||||
|
||||
getStats() {
|
||||
return {
|
||||
...this.stats,
|
||||
queueLength: this.messageQueue.length,
|
||||
isConnected: this.isConnected(),
|
||||
}
|
||||
}
|
||||
|
||||
close(): void {
|
||||
this.stopHeartbeat()
|
||||
TimerUtils.clearTimer(this.reconnectTimer)
|
||||
this.reconnectTimer = null
|
||||
|
||||
this.ws?.close(1000)
|
||||
this.ws = null
|
||||
|
||||
this.connectionState = false
|
||||
this.isConnecting = false
|
||||
this.messageQueue = []
|
||||
}
|
||||
|
||||
destroy(): void {
|
||||
this.close()
|
||||
}
|
||||
}
|
||||
|
||||
export default WebSocketManager
|
||||
115
src/lib/constants.ts
Normal file
115
src/lib/constants.ts
Normal file
@@ -0,0 +1,115 @@
|
||||
// 渲染进程|主进程常量定义
|
||||
|
||||
export enum IPC_EVENTS {
|
||||
EXTERNAL_OPEN = 'external-open',
|
||||
WINDOW_MINIMIZE = 'window-minimize',
|
||||
WINDOW_MAXIMIZE = 'window-maximize',
|
||||
WINDOW_CLOSE = 'window-close',
|
||||
IS_WINDOW_MAXIMIZED = 'is-window-maximized',
|
||||
APP_SET_FRAMELESS = 'app:set-frameless',
|
||||
APP_LOAD_PAGE = 'app:load-page',
|
||||
TAB_CREATE = 'tab:create',
|
||||
TAB_LIST = 'tab:list',
|
||||
TAB_NAVIGATE = 'tab:navigate',
|
||||
TAB_RELOAD = 'tab:reload',
|
||||
TAB_BACK = 'tab:back',
|
||||
TAB_FORWARD = 'tab:forward',
|
||||
TAB_SWITCH = 'tab:switch',
|
||||
TAB_CLOSE = 'tab:close',
|
||||
LOG_TO_MAIN = 'log-to-main',
|
||||
READ_FILE = 'read-file',
|
||||
INVOKE = 'ipc:invoke',
|
||||
INVOKE_ASYNC = 'ipc:invokeAsync',
|
||||
APP_MINIMIZE ='app:minimize',
|
||||
APP_MAXIMIZE ='app:maximize',
|
||||
APP_QUIT ='app:quit',
|
||||
FILE_READ = 'file:read',
|
||||
FILE_WRITE = 'file:write',
|
||||
GET_WINDOW_ID='get-window-id',
|
||||
CUSTOM_EVENT ='custom:event',
|
||||
TIME_UPDATE = 'time:update',
|
||||
RENDERER_IS_READY = 'renderer-ready',
|
||||
SHOW_CONTEXT_MENU = 'show-context-menu',
|
||||
START_A_DIALOGUE = 'start-a-dialogue',
|
||||
|
||||
// 打开窗口
|
||||
OPEN_WINDOW = 'open-window',
|
||||
|
||||
// 发送日志
|
||||
LOG_DEBUG = 'log-debug',
|
||||
LOG_INFO = 'log-info',
|
||||
LOG_WARN = 'log-warn',
|
||||
LOG_ERROR = 'log-error',
|
||||
|
||||
// 设置
|
||||
CONFIG_UPDATED = 'config-updated',
|
||||
SET_CONFIG = 'set-config',
|
||||
GET_CONFIG = 'get-config',
|
||||
UPDATE_CONFIG = 'update-config',
|
||||
|
||||
// 主题
|
||||
SET_THEME_MODE = 'set-theme-mode',
|
||||
GET_THEME_MODE = 'get-theme-mode',
|
||||
IS_DARK_THEME = 'is-dark-theme',
|
||||
THEME_MODE_UPDATED = 'theme-mode-updated',
|
||||
|
||||
// 执行脚本
|
||||
EXECUTE_SCRIPT = 'execute-script',
|
||||
|
||||
// 打开渠道
|
||||
OPEN_CHANNEL = 'open-channel',
|
||||
}
|
||||
|
||||
export const MAIN_WIN_SIZE = {
|
||||
width: 1440,
|
||||
height: 900,
|
||||
minWidth: 1440,
|
||||
minHeight: 900,
|
||||
} as const
|
||||
|
||||
export enum WINDOW_NAMES {
|
||||
MAIN = 'main',
|
||||
SETTING = 'setting',
|
||||
DIALOG = 'dialog',
|
||||
LOADING = 'loading',
|
||||
}
|
||||
|
||||
export enum CONFIG_KEYS {
|
||||
THEME_MODE = 'themeMode',
|
||||
PRIMARY_COLOR = 'primaryColor',
|
||||
LANGUAGE = 'language',
|
||||
FONT_SIZE = 'fontSize',
|
||||
MINIMIZE_TO_TRAY = 'minimizeToTray',
|
||||
PROVIDER = 'provider',
|
||||
DEFAULT_MODEL = 'defaultModel',
|
||||
}
|
||||
|
||||
export enum MENU_IDS {
|
||||
CONVERSATION_ITEM = 'conversation-item',
|
||||
CONVERSATION_LIST = 'conversation-list',
|
||||
MESSAGE_ITEM = 'message-item',
|
||||
}
|
||||
|
||||
export enum CONVERSATION_ITEM_MENU_IDS {
|
||||
PIN = 'pin',
|
||||
RENAME = 'rename',
|
||||
DEL = 'del',
|
||||
}
|
||||
|
||||
export enum CONVERSATION_LIST_MENU_IDS {
|
||||
NEW_CONVERSATION = 'newConversation',
|
||||
SORT_BY = 'sortBy',
|
||||
SORT_BY_CREATE_TIME = 'sortByCreateTime',
|
||||
SORT_BY_UPDATE_TIME = 'sortByUpdateTime',
|
||||
SORT_BY_NAME = 'sortByName',
|
||||
SORT_BY_MODEL = 'sortByModel',
|
||||
SORT_ASCENDING = 'sortAscending',
|
||||
SORT_DESCENDING = 'sortDescending',
|
||||
BATCH_OPERATIONS = 'batchOperations',
|
||||
}
|
||||
|
||||
export enum MESSAGE_ITEM_MENU_IDS {
|
||||
COPY = 'copy',
|
||||
DELETE = 'delete',
|
||||
SELECT = 'select',
|
||||
}
|
||||
331
src/lib/index.ts
Normal file
331
src/lib/index.ts
Normal file
@@ -0,0 +1,331 @@
|
||||
/**
|
||||
* 工具函数集合
|
||||
* 包含打字机效果、ID生成、回调安全调用等通用工具函数
|
||||
*/
|
||||
|
||||
/* =======================
|
||||
* ID 生成工具
|
||||
* ======================= */
|
||||
export class IdUtils {
|
||||
/**
|
||||
* 生成消息 ID
|
||||
*/
|
||||
static generateMessageId(): string {
|
||||
const timestamp = Date.now()
|
||||
const chars = 'abcdefghijklmnopqrstuvwxyz'
|
||||
const randomStr = Array.from({ length: 4 }, () =>
|
||||
chars.charAt(Math.floor(Math.random() * chars.length))
|
||||
).join('')
|
||||
return `mid${randomStr}${timestamp}`
|
||||
}
|
||||
}
|
||||
|
||||
/* =======================
|
||||
* 回调安全调用工具
|
||||
* ======================= */
|
||||
|
||||
export type CallbackMap = Record<string, (...args: any[]) => any>
|
||||
|
||||
export interface BatchCallbackConfig {
|
||||
name: string
|
||||
args?: any[]
|
||||
}
|
||||
|
||||
export class CallbackUtils {
|
||||
/**
|
||||
* 安全调用回调函数
|
||||
*/
|
||||
static safeCall(
|
||||
callbacks: CallbackMap | null | undefined,
|
||||
callbackName: string,
|
||||
...args: any[]
|
||||
): void {
|
||||
const cb = callbacks?.[callbackName]
|
||||
if (typeof cb === 'function') {
|
||||
try {
|
||||
cb(...args)
|
||||
} catch (error) {
|
||||
console.error(`回调函数 ${callbackName} 执行出错:`, error)
|
||||
}
|
||||
} else {
|
||||
console.warn(`回调函数 ${callbackName} 不可用`)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量安全调用回调函数
|
||||
*/
|
||||
static safeBatchCall(
|
||||
callbacks: CallbackMap | null | undefined,
|
||||
callbackConfigs: BatchCallbackConfig[]
|
||||
): void {
|
||||
callbackConfigs.forEach(({ name, args = [] }) => {
|
||||
this.safeCall(callbacks, name, ...args)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/* =======================
|
||||
* 消息处理工具
|
||||
* ======================= */
|
||||
|
||||
export interface BaseMessage {
|
||||
type: string
|
||||
content?: any
|
||||
timestamp: number
|
||||
isComplete?: boolean
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
export class MessageUtils {
|
||||
/**
|
||||
* 验证消息格式
|
||||
*/
|
||||
static validateMessage(message: unknown): message is BaseMessage {
|
||||
return (
|
||||
typeof message === 'object' &&
|
||||
message !== null &&
|
||||
'type' in message
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化消息
|
||||
*/
|
||||
static formatMessage<T = any>(
|
||||
type: string,
|
||||
content: T,
|
||||
options: Partial<BaseMessage> = {}
|
||||
): BaseMessage {
|
||||
return {
|
||||
type,
|
||||
content,
|
||||
timestamp: Date.now(),
|
||||
...options,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否为完整消息
|
||||
*/
|
||||
static isCompleteMessage(message: Partial<BaseMessage> | null | undefined): boolean {
|
||||
return message?.isComplete === true
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否为心跳 pong
|
||||
*/
|
||||
static isPongMessage(messageData: unknown): boolean {
|
||||
if (typeof messageData === 'string') {
|
||||
return messageData === 'pong' || messageData.toLowerCase().includes('pong')
|
||||
}
|
||||
if (typeof messageData === 'object' && messageData !== null) {
|
||||
return (messageData as any).type === 'pong'
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* 安全解析 JSON
|
||||
*/
|
||||
static safeParseJSON<T = any>(messageStr: string): T | null {
|
||||
try {
|
||||
return JSON.parse(messageStr) as T
|
||||
} catch {
|
||||
console.warn('JSON 解析失败:', messageStr)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建打字机消息
|
||||
*/
|
||||
static createTypewriterMessage(
|
||||
content: string,
|
||||
isComplete = false,
|
||||
type = 'typewriter'
|
||||
): BaseMessage {
|
||||
return {
|
||||
type,
|
||||
content,
|
||||
isComplete,
|
||||
timestamp: Date.now(),
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建加载消息
|
||||
*/
|
||||
static createLoadingMessage(content = '加载中...'): BaseMessage {
|
||||
return {
|
||||
type: 'loading',
|
||||
content,
|
||||
timestamp: Date.now(),
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建错误消息
|
||||
*/
|
||||
static createErrorMessage(error: unknown): BaseMessage {
|
||||
return {
|
||||
type: 'error',
|
||||
content:
|
||||
error instanceof Error ? error.message : String(error ?? '未知错误'),
|
||||
timestamp: Date.now(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* =======================
|
||||
* 定时器工具
|
||||
* ======================= */
|
||||
|
||||
export type TimerType = 'timeout' | 'interval'
|
||||
|
||||
export interface CancelableTimer {
|
||||
cancel(): void
|
||||
isActive(): boolean
|
||||
}
|
||||
|
||||
export class TimerUtils {
|
||||
static safeClear(
|
||||
timerId: number | null,
|
||||
type: TimerType = 'timeout'
|
||||
): null {
|
||||
if (timerId !== null) {
|
||||
type === 'interval' ? clearInterval(timerId) : clearTimeout(timerId)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
static clearTimer(
|
||||
timerId: number | null,
|
||||
type: TimerType = 'timeout'
|
||||
): null {
|
||||
return this.safeClear(timerId, type)
|
||||
}
|
||||
|
||||
static createCancelableTimeout(
|
||||
callback: () => void,
|
||||
delay: number
|
||||
): CancelableTimer {
|
||||
let timerId: number | null = window.setTimeout(callback, delay)
|
||||
return {
|
||||
cancel() {
|
||||
if (timerId !== null) {
|
||||
clearTimeout(timerId)
|
||||
timerId = null
|
||||
}
|
||||
},
|
||||
isActive() {
|
||||
return timerId !== null
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
static createCancelableInterval(
|
||||
callback: () => void,
|
||||
interval: number
|
||||
): CancelableTimer {
|
||||
let timerId: number | null = window.setInterval(callback, interval)
|
||||
return {
|
||||
cancel() {
|
||||
if (timerId !== null) {
|
||||
clearInterval(timerId)
|
||||
timerId = null
|
||||
}
|
||||
},
|
||||
isActive() {
|
||||
return timerId !== null
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* =======================
|
||||
* 防抖工具
|
||||
* ======================= */
|
||||
|
||||
export class DebounceUtils {
|
||||
static createDebounce<T extends (...args: any[]) => void>(
|
||||
func: T,
|
||||
delay: number
|
||||
): (...args: Parameters<T>) => void {
|
||||
let timerId: number | null = null
|
||||
|
||||
return function (this: unknown, ...args: Parameters<T>) {
|
||||
if (timerId !== null) {
|
||||
clearTimeout(timerId)
|
||||
}
|
||||
timerId = window.setTimeout(() => func.apply(this, args), delay)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* =======================
|
||||
* 节流工具
|
||||
* ======================= */
|
||||
|
||||
export class ThrottleUtils {
|
||||
static createThrottle<T extends (...args: any[]) => void>(
|
||||
func: T,
|
||||
delay: number
|
||||
): (...args: Parameters<T>) => void {
|
||||
let prev = Date.now()
|
||||
|
||||
return function (this: unknown, ...args: Parameters<T>) {
|
||||
const now = Date.now()
|
||||
if (now - prev >= delay) {
|
||||
func.apply(this, args)
|
||||
prev = now
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* =======================
|
||||
* 日期工具
|
||||
* ======================= */
|
||||
|
||||
export class DateUtils {
|
||||
static formatDate(
|
||||
date: Date = new Date(),
|
||||
format: string = 'yyyy-MM-dd'
|
||||
): string {
|
||||
const year = date.getFullYear()
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0')
|
||||
const day = String(date.getDate()).padStart(2, '0')
|
||||
|
||||
return format
|
||||
.replace('yyyy', String(year))
|
||||
.replace('MM', month)
|
||||
.replace('dd', day)
|
||||
}
|
||||
}
|
||||
|
||||
/* =======================
|
||||
* 手机号校验
|
||||
* ======================= */
|
||||
|
||||
export class PhoneUtils {
|
||||
static validatePhone(phone: string): boolean {
|
||||
const phoneRegex = /^1[3-9]\d{9}$/
|
||||
return phoneRegex.test(phone)
|
||||
}
|
||||
}
|
||||
|
||||
/* =======================
|
||||
* 默认导出
|
||||
* ======================= */
|
||||
|
||||
export default {
|
||||
IdUtils,
|
||||
CallbackUtils,
|
||||
MessageUtils,
|
||||
TimerUtils,
|
||||
DateUtils,
|
||||
DebounceUtils,
|
||||
ThrottleUtils,
|
||||
PhoneUtils,
|
||||
}
|
||||
38
src/lib/types.ts
Normal file
38
src/lib/types.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { WINDOW_NAMES, CONFIG_KEYS } from './constants';
|
||||
|
||||
export type WindowNames = `${WINDOW_NAMES}`;
|
||||
export type ConfigKeys = `${CONFIG_KEYS}`;
|
||||
|
||||
export interface IConfig {
|
||||
// 主题模式配置
|
||||
[CONFIG_KEYS.THEME_MODE]: ThemeMode;
|
||||
// 高亮色
|
||||
[CONFIG_KEYS.PRIMARY_COLOR]: string;
|
||||
// 语言
|
||||
[CONFIG_KEYS.LANGUAGE]: 'zh' | 'en';
|
||||
// 字体大小
|
||||
[CONFIG_KEYS.FONT_SIZE]: number;
|
||||
// 关闭时最小化到托盘
|
||||
[CONFIG_KEYS.MINIMIZE_TO_TRAY]: boolean;
|
||||
// provider 配置 JSON
|
||||
[CONFIG_KEYS.PROVIDER]?: string;
|
||||
// 默认模型
|
||||
[CONFIG_KEYS.DEFAULT_MODEL]?: string | null;
|
||||
}
|
||||
|
||||
export interface Provider {
|
||||
id: number;
|
||||
name: string;
|
||||
visible?: boolean;
|
||||
title?: string;
|
||||
type?: 'OpenAI';
|
||||
openAISetting?: string;
|
||||
createdAt: number;
|
||||
updatedAt: number;
|
||||
models: string[];
|
||||
}
|
||||
|
||||
export interface OpenAISetting {
|
||||
baseURL?: string;
|
||||
apiKey?: string;
|
||||
}
|
||||
94
src/lib/utils.ts
Normal file
94
src/lib/utils.ts
Normal file
@@ -0,0 +1,94 @@
|
||||
import type { OpenAISetting } from './types'
|
||||
import { encode, decode } from 'js-base64'
|
||||
/**
|
||||
* 防抖函数
|
||||
* @param fn 需要执行的函数
|
||||
* @param delay 延迟时间(毫秒)
|
||||
* @returns 防抖处理后的函数
|
||||
*/
|
||||
export function debounce<T extends (...args: any[]) => any>(fn: T, delay: number): (...args: Parameters<T>) => void {
|
||||
let timer: NodeJS.Timeout | null = null;
|
||||
return function (this: any, ...args: Parameters<T>) {
|
||||
if (timer) {
|
||||
clearTimeout(timer);
|
||||
}
|
||||
timer = setTimeout(() => {
|
||||
fn.apply(this, args);
|
||||
}, delay);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 节流函数
|
||||
* @param fn 需要执行的函数
|
||||
* @param interval 间隔时间(毫秒)
|
||||
* @returns 节流处理后的函数
|
||||
*/
|
||||
export function throttle<T extends (...args: any[]) => any>(fn: T, interval: number): (...args: Parameters<T>) => void {
|
||||
let lastTime = 0;
|
||||
return function (this: any, ...args: Parameters<T>) {
|
||||
const now = Date.now();
|
||||
if (now - lastTime >= interval) {
|
||||
fn.apply(this, args);
|
||||
lastTime = now;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function cloneDeep<T>(obj: T): T {
|
||||
if (obj === null || typeof obj !== 'object') {
|
||||
return obj;
|
||||
}
|
||||
|
||||
if (Array.isArray(obj)) {
|
||||
return obj.map(item => cloneDeep(item)) as T;
|
||||
}
|
||||
|
||||
const clone = Object.assign({}, obj);
|
||||
for (const key in clone) {
|
||||
if (Object.prototype.hasOwnProperty.call(clone, key)) {
|
||||
clone[key] = cloneDeep(clone[key]);
|
||||
}
|
||||
}
|
||||
return clone;
|
||||
}
|
||||
|
||||
export function simpleCloneDeep<T>(obj: T): T {
|
||||
try {
|
||||
return JSON.parse(JSON.stringify(obj));
|
||||
} catch (error) {
|
||||
console.error('simpleCloneDeep failed:', error);
|
||||
return obj;
|
||||
}
|
||||
}
|
||||
|
||||
export function stringifyOpenAISetting(setting: OpenAISetting) {
|
||||
try {
|
||||
return encode(JSON.stringify(setting));
|
||||
} catch (error) {
|
||||
console.error('stringifyOpenAISetting failed:', error);
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
export function parseOpenAISetting(setting: string): OpenAISetting {
|
||||
try {
|
||||
return JSON.parse(decode(setting));
|
||||
} catch (error) {
|
||||
console.error('parseOpenAISetting failed:', error);
|
||||
return {} as OpenAISetting;
|
||||
}
|
||||
}
|
||||
|
||||
export function uniqueByKey<T extends Record<string, any>>(arr: T[], key: keyof T): T[] {
|
||||
const seen = new Map<any, boolean>();
|
||||
|
||||
return arr.filter(item => {
|
||||
const keyValue = item[key];
|
||||
if (seen.has(keyValue)) {
|
||||
return false;
|
||||
}
|
||||
seen.set(keyValue, true);
|
||||
return true;
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user