重启之后清除登录信息

This commit is contained in:
2025-07-15 19:55:59 +08:00
parent 4caff7542e
commit 0b2974e2c9
18 changed files with 1873 additions and 86 deletions

97
CACHE_CLEAR_FIX.md Normal file
View File

@@ -0,0 +1,97 @@
# 缓存清理功能修复说明
## 问题描述
重启电脑时,清除缓存没有生效,并且日志文件中也没有看到对应的日志。
## 问题原因分析
1. **异步操作处理不当**`clearStorageData` 是异步操作,但在事件处理中没有正确等待完成
2. **日志配置问题**:日志可能没有及时写入文件,导致系统关机时日志丢失
3. **事件处理顺序问题**`event.resume()` 在缓存清理完成前就被调用
4. **缺少重启事件监听**:只监听了 `shutdown` 事件,没有监听 `restart` 事件
## 修复内容
### 1. 创建统一的缓存清理工具 (`src/main/utils/cacheUtils.js`)
- 提供 `clearAllSessionData()` 函数,统一处理会话数据清理
- 提供 `clearAllWindowsCache()` 函数,清理所有窗口的缓存
- 包含详细的错误处理和日志记录
- 确保日志及时写入文件
### 2. 改进日志配置 (`src/main/utils/logger.js`)
- 设置 `log.transports.file.sync = true` 确保日志立即写入
- 添加日志文件路径获取功能
- 在应用启动时显示日志文件路径
### 3. 修复主进程事件处理 (`src/main/index.js`)
- 修复 `powerMonitor.on('shutdown')` 事件处理,使用 async/await 正确等待缓存清理
- 添加 `powerMonitor.on('restart')` 事件监听,处理系统重启
- 修复所有 `clearStorageData` 调用,使用统一的工具函数
- 添加进程信号监听 (`SIGTERM`, `SIGINT`) 确保强制关闭时也能清理缓存
- 添加未捕获异常处理
### 4. 改进托盘功能 (`src/main/tray.js`)
- 使用统一的缓存清理函数
- 添加"查看日志"菜单项,方便用户查看日志文件
### 5. 添加测试脚本 (`test-cache-clear.js`)
- 提供缓存清理功能的测试代码
## 修复后的功能特点
### 缓存清理覆盖的场景
1. **系统关机** (`powerMonitor.on('shutdown')`)
2. **系统重启** (`powerMonitor.on('restart')`)
3. **应用正常退出** (`app.on('before-quit')`)
4. **窗口关闭** (`app.on('window-all-closed')`)
5. **应用退出** (`app.on('will-quit')`)
6. **进程信号** (`SIGTERM`, `SIGINT`)
7. **托盘手动清理**
### 日志记录改进
- 每个清理操作都有详细的日志记录
- 包含操作上下文信息(如 'shutdown', 'restart' 等)
- 日志立即写入文件,避免丢失
- 提供日志文件路径查看功能
### 错误处理
- 完善的错误捕获和日志记录
- 即使清理失败也不会阻止应用正常退出
- 详细的错误信息记录
## 使用方法
### 查看日志
1. 右键点击托盘图标
2. 选择"查看日志"
3. 系统会自动打开日志文件所在目录
### 手动清理缓存
1. 右键点击托盘图标
2. 选择"清除缓存"
3. 系统会清理所有缓存并重新加载窗口
### 测试缓存清理
```bash
node test-cache-clear.js
```
## 日志文件位置
- **Windows**: `%APPDATA%\[应用名称]\logs\main.log`
- **macOS**: `~/Library/Logs/[应用名称]/main.log`
- **Linux**: `~/.config/[应用名称]/logs/main.log`
## 验证修复效果
1. 重启电脑
2. 查看日志文件,应该能看到类似以下内容:
```
[时间] 系统将要重启.
[时间] [restart] 开始清除所有会话数据
[时间] [restart] 会话数据清除成功
[时间] [restart] 缓存清理完成
```
## 注意事项
- 修复后的代码确保了缓存清理的可靠性
- 日志记录更加详细,便于问题排查
- 即使系统强制关闭应用,也会尝试清理缓存
- 所有清理操作都是异步的,不会阻塞系统关机/重启流程

153
LOCK_FILE_FIX.md Normal file
View File

@@ -0,0 +1,153 @@
# 锁文件和缓存问题修复说明
## 问题描述
运行 `npm run dev` 时遇到以下问题:
1. **锁文件被占用**`EBUSY: resource busy or locked, unlink 'lockfile'`
2. **单实例检查失败**:即使锁文件无效,也无法启动新实例
3. **缓存错误**`Unable to move the cache: 拒绝访问``Failed to delete the database`
## 问题原因分析
1. **文件占用**:锁文件被其他进程占用,无法删除
2. **进程检查不准确**:锁文件中的 PID 为 NaN导致进程检查失败
3. **缓存冲突**:多个实例同时访问缓存目录导致冲突
4. **权限问题**:某些文件需要管理员权限才能删除
## 修复内容
### 1. 改进锁文件删除机制 (`src/main/utils/singleInstance.js`)
- 添加 `safeDeleteFile()` 方法,处理文件被占用的情况
- 在 Windows 上使用 `del /F /Q` 命令强制删除
- 改进错误处理和日志记录
### 2. 智能单实例检查
- 检查是否有实际的窗口存在,而不仅仅依赖锁文件
- 如果找不到现有窗口,允许强制启动新实例
- 添加详细的日志记录
### 3. 缓存清理和配置 (`src/main/index.js`)
- 添加缓存相关命令行参数
- 在应用启动时清理缓存目录
- 避免缓存冲突
### 4. 托盘菜单增强 (`src/main/tray.js`)
- 改进"清理实例锁"功能,处理文件占用情况
- 添加"强制重启应用"选项
- 提供更安全的文件删除机制
### 5. 测试脚本 (`test-lock-cleanup.js`)
- 提供锁文件清理功能的测试
- 验证文件删除机制
- 帮助调试锁文件问题
## 修复后的功能特点
### 安全的文件删除
1. **多重尝试**:首先尝试直接删除,失败后使用系统命令
2. **错误处理**:区分不同类型的错误,提供相应的解决方案
3. **日志记录**:详细记录删除过程和结果
### 智能启动逻辑
1. **窗口检查**:检查是否有实际的窗口存在
2. **强制启动**:如果锁文件无效且无窗口,允许强制启动
3. **详细日志**:记录每个决策步骤
### 缓存管理
1. **启动时清理**:应用启动时自动清理缓存目录
2. **命令行配置**:添加缓存相关配置避免冲突
3. **错误处理**:处理缓存清理过程中的错误
## 使用方法
### 自动修复
1. 重新运行 `npm run dev`
2. 应用会自动处理锁文件和缓存问题
3. 如果锁文件被占用,会尝试强制删除
### 手动清理
1. 右键点击托盘图标
2. 选择"清理实例锁"或"强制重启应用"
3. 查看日志确认操作结果
### 测试功能
```bash
# 测试锁文件清理功能
node test-lock-cleanup.js
```
## 修复后的启动流程
### 正常启动
```
[时间] 开始检查单实例锁...
[时间] 发现锁文件: [路径]
[时间] 进程 [PID] 不存在,尝试删除无效锁文件
[时间] 成功删除文件: [路径]
[时间] 单实例锁检查结果: true
[时间] 这是第一个实例,继续启动
[时间] 应用已准备就绪,开始初始化...
[时间] 清理缓存目录: [路径]
[时间] 缓存目录清理完成: [路径]
```
### 锁文件被占用
```
[时间] 发现锁文件: [路径]
[时间] 文件被占用,无法删除: [路径]
[时间] 使用 del 命令删除文件: [路径]
[时间] 单实例锁检查结果: true
[时间] 这是第一个实例,继续启动
```
### 强制启动
```
[时间] 单实例锁检查结果: false
[时间] 检测到已有实例运行
[时间] 未找到现有实例窗口,可能是锁文件问题,允许强制启动
[时间] 这是第一个实例,继续启动
```
## 常见问题解决
### 1. 锁文件仍然无法删除
1. 使用托盘菜单的"强制重启应用"功能
2. 手动删除锁文件:
```cmd
del /F /Q "C:\Users\[用户名]\AppData\Roaming\market-manager-gui\lockfile"
```
3. 重启开发环境
### 2. 缓存错误仍然存在
1. 手动清理缓存目录:
```cmd
rmdir /S /Q "C:\Users\[用户名]\AppData\Roaming\market-manager-gui\Cache"
rmdir /S /Q "C:\Users\[用户名]\AppData\Roaming\market-manager-gui\Code Cache"
```
2. 以管理员身份运行应用
### 3. 权限问题
1. 以管理员身份运行命令提示符
2. 手动删除相关文件
3. 检查文件权限设置
## 注意事项
### 1. 开发环境
- 在开发过程中,锁文件和缓存可能会频繁创建和删除
- 如果遇到问题,使用托盘菜单的清理功能
- 查看日志文件获取详细信息
### 2. 生产环境
- 确保应用正常退出时能清理锁文件
- 监控缓存目录的大小
- 定期检查是否有无效锁文件
### 3. 调试建议
- 使用 `test-lock-cleanup.js` 脚本测试文件删除功能
- 查看日志文件了解锁文件和缓存的状态
- 使用托盘菜单的手动清理功能
## 相关文件
- `src/main/utils/singleInstance.js` - 单实例锁管理工具(已更新)
- `src/main/index.js` - 主进程启动文件(已更新)
- `src/main/tray.js` - 托盘功能(已更新)
- `test-lock-cleanup.js` - 锁文件清理测试脚本

147
LOG_OPTIMIZATION.md Normal file
View File

@@ -0,0 +1,147 @@
# 日志优化说明
## 问题描述
应用运行时产生了大量重复的日志,特别是:
1. **tokenExpireTimer** 每10秒打印3条日志
2. **WebSocket 心跳消息** 每30秒打印1条日志
3. **WebSocket 连接成功** 打印重复的 emoji 日志
## 优化内容
### 1. tokenExpireTimer 日志优化 (`src/main/window.js`)
- **优化前**每10秒打印3条日志
```
tokenExpireTimer 触发
tokenExpireTimer 触发 对比时间戳
tokenExpireTimer 触发 对比时间戳差距为:23
```
- **优化后**每60秒打印1条日志
```
tokenExpireTimer 检查 - 时间差距: 23分钟
```
### 2. WebSocket 心跳消息优化 (`src/main/utils/WebSocketClient.js`)
- **优化前**每30秒打印心跳消息
```
收到消息: {"cmd":"heartcheck"}
```
- **优化后**:心跳消息不打印日志,或仅在调试模式下打印
### 3. WebSocket 连接日志优化
- **优化前**连接成功时打印3行重复的 emoji
```
😀😀😀😀 WebSocket connect create ok 😀😀😀😀
😀😀😀😀 WebSocket connect create ok 😀😀😀😀
😀😀😀😀 WebSocket connect create ok 😀😀😀😀
```
- **优化后**:简洁的连接成功日志
```
WebSocket 连接成功
WebSocket protocol: [协议]
```
### 4. 日志级别控制系统 (`src/main/utils/logger.js`)
- 添加日志级别控制功能
- 支持 ERROR、WARN、INFO、DEBUG 四个级别
- 可以动态调整日志级别
### 5. 托盘菜单日志设置 (`src/main/tray.js`)
- 添加"日志设置"菜单项
- 提供三个日志级别选项:
- 详细日志 (DEBUG)
- 标准日志 (INFO)
- 仅错误日志 (ERROR)
## 优化效果
### 日志数量对比
- **优化前**:每分钟约 18-21 条日志
- tokenExpireTimer: 6条/分钟
- WebSocket 心跳: 2条/分钟
- 其他日志: 10-13条/分钟
- **优化后**:每分钟约 2-5 条日志
- tokenExpireTimer: 1条/分钟
- WebSocket 心跳: 0条/分钟
- 其他日志: 1-4条/分钟
### 日志文件大小
- **优化前**日志文件增长速度快容易达到几MB
- **优化后**:日志文件增长缓慢,便于管理和查看
## 使用方法
### 1. 自动优化
- 应用启动后自动应用优化设置
- 无需用户干预
### 2. 手动调整日志级别
1. 右键点击托盘图标
2. 选择"日志设置"
3. 选择所需的日志级别:
- **详细日志**:显示所有日志,包括调试信息
- **标准日志**:显示重要信息,隐藏调试信息
- **仅错误日志**:只显示错误和警告信息
### 3. 查看日志
1. 右键点击托盘图标
2. 选择"查看日志"
3. 系统会自动打开日志文件所在目录
## 日志级别说明
### DEBUG (详细日志)
- 显示所有日志信息
- 包括调试信息、心跳消息等
- 适用于开发和调试
### INFO (标准日志)
- 显示重要信息
- 隐藏调试信息和心跳消息
- 适用于日常使用
### ERROR (仅错误日志)
- 只显示错误和警告信息
- 隐藏所有其他日志
- 适用于生产环境
## 配置选项
### 环境变量
```bash
# 设置日志级别
NODE_ENV=development # 开发环境,显示详细日志
NODE_ENV=production # 生产环境,显示标准日志
```
### 代码中设置
```javascript
import logger from './utils/logger';
// 设置日志级别
logger.setLogLevel('DEBUG'); // 详细日志
logger.setLogLevel('INFO'); // 标准日志
logger.setLogLevel('ERROR'); // 仅错误日志
```
## 注意事项
### 1. 开发环境
- 建议使用"详细日志"级别
- 便于调试和问题排查
- 可以查看所有系统状态
### 2. 生产环境
- 建议使用"标准日志"或"仅错误日志"级别
- 减少日志文件大小
- 提高应用性能
### 3. 问题排查
- 如果遇到问题,可以临时切换到"详细日志"级别
- 查看完整的系统运行状态
- 问题解决后可以切换回标准级别
## 相关文件
- `src/main/window.js` - tokenExpireTimer 优化
- `src/main/utils/WebSocketClient.js` - WebSocket 日志优化
- `src/main/utils/logger.js` - 日志级别控制系统
- `src/main/tray.js` - 托盘菜单日志设置

163
SINGLE_INSTANCE_FIX.md Normal file
View File

@@ -0,0 +1,163 @@
# 单实例锁问题修复说明
## 问题描述
运行 `npm run dev` 时,程序检测到已有实例运行并退出,但任务管理器中并没有看到已启动的实例:
```
13:05:39.374 > 单实例锁检查结果: false
13:05:39.375 > 检测到已有实例运行,退出当前实例
Process finished with exit code 0
```
## 问题原因分析
1. **锁文件未正确清理**:之前的实例异常退出时,单实例锁文件没有被正确删除
2. **进程检查不完善**:只检查了锁文件存在,没有验证对应的进程是否还在运行
3. **锁文件位置不明确**Electron 的单实例锁可能存储在不同位置
4. **异常退出处理不当**:应用崩溃或强制关闭时,锁文件没有被清理
## 修复内容
### 1. 创建单实例锁管理工具 (`src/main/utils/singleInstance.js`)
- 提供完整的锁文件管理功能
- 自动清理无效的锁文件
- 检查进程是否还在运行
- 在应用退出时自动清理锁文件
### 2. 改进单实例检查逻辑 (`src/main/index.js`)
- 使用新的单实例管理器
- 在启动时自动清理无效锁文件
- 如果找不到现有实例窗口,允许强制启动
- 添加详细的日志记录
### 3. 添加手动清理功能 (`src/main/tray.js`)
- 在托盘菜单中添加"清理实例锁"选项
- 允许用户手动清理锁文件
- 提供清理结果反馈
### 4. 创建测试脚本 (`test-single-instance.js`)
- 提供锁文件检查和清理的测试功能
- 验证进程检查逻辑
- 帮助调试锁文件问题
## 修复后的功能特点
### 自动锁文件管理
1. **启动时检查**:应用启动时自动检查并清理无效锁文件
2. **进程验证**:验证锁文件中的进程是否还在运行
3. **自动清理**:应用正常退出时自动清理锁文件
4. **异常处理**:监听各种退出信号,确保锁文件被清理
### 手动清理功能
1. **托盘菜单**:右键点击托盘图标,选择"清理实例锁"
2. **批量清理**:清理多个可能的锁文件位置
3. **结果反馈**:显示清理的文件数量和结果
### 智能启动逻辑
1. **锁文件检查**:启动前检查锁文件有效性
2. **窗口激活**:如果找到现有实例,尝试激活其窗口
3. **强制启动**:如果找不到现有实例,允许强制启动
4. **详细日志**:记录每个步骤的执行情况
## 使用方法
### 自动修复
1. 重新运行 `npm run dev`
2. 应用会自动检查并清理无效锁文件
3. 如果清理成功,应用会正常启动
### 手动清理
1. 如果应用已经启动,右键点击托盘图标
2. 选择"清理实例锁"
3. 查看日志确认清理结果
4. 重新启动应用
### 测试锁文件
```bash
# 运行测试脚本
node test-single-instance.js
```
## 锁文件位置
### Windows 系统
- `%APPDATA%\[应用名称]\single-instance-lock`
- `%APPDATA%\[应用名称]\lockfile`
- `%APPDATA%\[应用名称]\.lock`
### macOS 系统
- `~/Library/Application Support/[应用名称]/single-instance-lock`
- `~/Library/Application Support/[应用名称]/lockfile`
- `~/Library/Application Support/[应用名称]/.lock`
### Linux 系统
- `~/.config/[应用名称]/single-instance-lock`
- `~/.config/[应用名称]/lockfile`
- `~/.config/[应用名称]/.lock`
## 验证修复效果
### 1. 正常启动
运行 `npm run dev` 后,应该看到:
```
[时间] 单实例锁检查结果: true
[时间] 这是第一个实例,继续启动
[时间] 应用已准备就绪,开始初始化...
```
### 2. 重复启动
如果尝试启动第二个实例,应该看到:
```
[时间] 单实例锁检查结果: false
[时间] 检测到已有实例运行,尝试激活现有实例
[时间] 成功激活现有实例窗口
```
### 3. 异常恢复
如果之前的实例异常退出,应该看到:
```
[时间] 发现锁文件: [路径]
[时间] 进程 [PID] 不存在,删除无效锁文件
[时间] 单实例锁检查结果: true
[时间] 这是第一个实例,继续启动
```
## 常见问题解决
### 1. 仍然无法启动
1. 手动清理锁文件:
- 右键点击托盘图标 → 清理实例锁
- 或者手动删除锁文件
2. 检查任务管理器,确保没有残留进程
3. 重启开发环境
### 2. 锁文件清理失败
1. 检查文件权限
2. 确保没有其他程序占用锁文件
3. 以管理员身份运行应用
### 3. 进程检查不准确
1. 在 Windows 上使用 `tasklist` 命令手动检查
2. 确认进程 ID 是否正确
3. 检查系统权限
## 注意事项
### 1. 开发环境
- 在开发过程中,锁文件可能会频繁创建和删除
- 如果遇到问题,使用托盘菜单的清理功能
- 查看日志文件获取详细信息
### 2. 生产环境
- 确保应用正常退出时能清理锁文件
- 监控锁文件的状态
- 定期检查是否有无效锁文件
### 3. 调试建议
- 使用 `test-single-instance.js` 脚本测试锁文件功能
- 查看日志文件了解锁文件的状态
- 使用托盘菜单的手动清理功能
## 相关文件
- `src/main/utils/singleInstance.js` - 单实例锁管理工具
- `src/main/index.js` - 主进程启动文件(已更新)
- `src/main/tray.js` - 托盘功能(已更新)
- `test-single-instance.js` - 锁文件测试脚本

138
STARTUP_FIX.md Normal file
View File

@@ -0,0 +1,138 @@
# 应用启动问题修复说明
## 问题描述
运行 `npm run dev` 时,程序在启动过程中立即退出,日志显示:
```
13:03:04.627 > 应用启动,日志文件路径: C:\Users\coolp\AppData\Roaming\market-manager-gui\logs\main.log
Process finished with exit code 0
```
## 问题原因分析
1. **WebSocketClient 过早初始化**:在应用启动时立即创建 WebSocket 连接,但此时可能缺少必要的配置信息
2. **异步操作处理不当**`app.whenReady()` 是异步的,但程序在等待完成前就退出了
3. **错误处理不完善**:某些同步操作可能抛出异常导致程序退出
4. **缺少启动状态日志**:无法确定程序在哪个阶段退出
## 修复内容
### 1. 延迟 WebSocket 连接创建
- 将 WebSocketClient 的创建延迟到应用完全启动后
- 添加错误处理和重试机制
- 避免在应用启动时立即连接
### 2. 改进错误处理
- 为所有可能失败的同步操作添加 try-catch
- 改进未捕获异常的处理,不立即退出程序
- 添加详细的错误日志
### 3. 增加启动日志
- 添加详细的启动过程日志
- 记录每个关键步骤的执行状态
- 便于定位问题所在
### 4. 修复异步操作
- 确保 `app.whenReady()` 正确等待
- 添加启动完成确认日志
- 改进事件监听器的设置
## 修复后的启动流程
### 启动日志示例
```
[时间] 当前运行平台: win32
[时间] Windows 系统,设置控制台编码为 UTF-8
[时间] Store 初始化成功
[时间] 硬件加速已禁用
[时间] 应用启动,日志文件路径: [路径]
[时间] 开始检查单实例锁...
[时间] 单实例锁检查结果: true
[时间] 这是第一个实例,继续启动
[时间] 应用已准备就绪,开始初始化...
[时间] 应用初始化完成
[时间] WebSocket连接已打开 (延迟2秒后)
```
### 关键修复点
#### 1. WebSocketClient 延迟创建
```javascript
// 延迟创建WebSocket连接
setTimeout(() => {
createWebSocketClient()
}, 2000); // 延迟2秒创建WebSocket连接
```
#### 2. 错误处理改进
```javascript
// 改进未捕获异常处理
process.on('uncaughtException', (error) => {
logger.error('未捕获的异常:', error);
// 不要立即退出,给应用一个恢复的机会
logger.error('应用遇到未捕获的异常,但将继续运行');
});
```
#### 3. 同步操作保护
```javascript
try {
app.disableHardwareAcceleration()
logger.info('硬件加速已禁用')
} catch (error) {
logger.error('禁用硬件加速失败:', error)
}
```
## 验证修复效果
### 1. 检查启动日志
运行 `npm run dev` 后,查看控制台输出,应该能看到完整的启动日志,包括:
- 应用准备就绪
- 初始化完成
- WebSocket 连接成功
### 2. 检查应用状态
- 应用应该正常启动并显示主窗口
- 托盘图标应该正常显示
- 不应该出现 "Process finished with exit code 0" 的提前退出
### 3. 检查日志文件
查看日志文件 `C:\Users\coolp\AppData\Roaming\market-manager-gui\logs\main.log`,应该包含完整的启动过程记录。
## 测试脚本
### 启动测试
```bash
# 运行应用
npm run dev
# 检查日志
tail -f "C:\Users\coolp\AppData\Roaming\market-manager-gui\logs\main.log"
```
### 功能测试
1. 应用启动后,右键点击托盘图标
2. 选择"查看日志"验证日志功能
3. 选择"清除缓存"验证缓存清理功能
## 注意事项
### 1. 开发环境
- 在开发环境中,应用可能需要更长时间启动
- WebSocket 连接可能会因为缺少配置而失败,这是正常的
- 查看日志文件获取详细的错误信息
### 2. 生产环境
- 确保所有必要的配置都已设置
- WebSocket 连接应该能够正常建立
- 应用应该能够稳定运行
### 3. 调试建议
- 如果仍然有问题,查看日志文件中的详细错误信息
- 检查配置文件是否正确设置
- 确认网络连接是否正常
## 相关文件
- `src/main/index.js` - 主进程启动文件
- `src/main/utils/WebSocketClient.js` - WebSocket 客户端
- `src/main/utils/logger.js` - 日志工具
- `test-startup.js` - 启动测试脚本

125
TRAY_MENU_SIMPLIFICATION.md Normal file
View File

@@ -0,0 +1,125 @@
# 托盘菜单简化说明
## 简化内容
根据用户需求,移除了托盘菜单中的以下选项:
1. **日志设置** - 包含详细日志、标准日志、仅错误日志的子菜单
2. **查看日志** - 打开日志文件所在目录的功能
3. **清理实例锁** - 手动清理锁文件的功能
4. **强制重启应用** - 强制重启应用的功能
## 简化后的托盘菜单
### 当前菜单结构
```
显示主窗口
隐藏主窗口
---
配置
└─ 配置客户端地址
---
显示隐藏桌面悬浮
---
检查更新
---
清除缓存
---
退出登录
---
退出应用
```
### 保留的核心功能
1. **窗口管理**
- 显示主窗口
- 隐藏主窗口
2. **配置管理**
- 配置客户端地址
3. **悬浮窗口**
- 显示隐藏桌面悬浮
4. **系统功能**
- 检查更新
- 清除缓存
- 退出登录
- 退出应用
## 移除的功能说明
### 1. 日志设置
- **移除原因**:用户不需要在托盘中调整日志级别
- **替代方案**:日志级别可以通过代码或环境变量设置
- **影响**:日志优化功能仍然有效,只是不能通过托盘菜单调整
### 2. 查看日志
- **移除原因**:用户不需要频繁查看日志文件
- **替代方案**:日志文件位置:`%APPDATA%\[应用名称]\logs\main.log`
- **影响**:日志仍然正常记录,只是不能通过托盘菜单快速访问
### 3. 清理实例锁
- **移除原因**:应用已经自动处理锁文件清理
- **替代方案**:应用启动时自动清理无效锁文件
- **影响**:单实例锁管理功能仍然有效,只是不能手动触发
### 4. 强制重启应用
- **移除原因**:用户不需要强制重启功能
- **替代方案**:正常退出后重新启动应用
- **影响**:应用启动问题已经修复,通常不需要强制重启
## 简化效果
### 菜单简洁性
- **简化前**12个菜单项
- **简化后**8个菜单项
- **减少**33% 的菜单项
### 用户体验
- **更简洁**:菜单更短,更容易浏览
- **更专注**:只保留核心功能
- **更直观**:功能分类更清晰
### 维护性
- **代码更简洁**:减少了不必要的菜单项代码
- **功能更集中**:专注于核心功能
- **更易维护**:减少了菜单相关的维护工作
## 功能保留说明
### 自动功能
以下功能仍然自动运行,无需用户干预:
1. **日志优化**:自动减少日志打印频率
2. **锁文件管理**:自动清理无效锁文件
3. **缓存管理**:自动处理缓存冲突
4. **错误处理**:自动处理各种异常情况
### 手动功能
以下功能仍然可以通过托盘菜单使用:
1. **窗口控制**:显示/隐藏主窗口
2. **配置管理**:配置客户端地址
3. **悬浮窗口**:控制桌面悬浮窗口
4. **系统操作**:更新、缓存清理、退出等
## 注意事项
### 1. 日志管理
- 日志仍然正常记录和优化
- 如果需要调整日志级别,可以通过代码修改
- 日志文件仍然可以手动查看
### 2. 问题排查
- 如果遇到启动问题,应用会自动处理
- 如果遇到锁文件问题,应用会自动清理
- 如果遇到缓存问题,可以使用"清除缓存"功能
### 3. 开发调试
- 开发时可以通过代码设置日志级别
- 调试时可以直接查看日志文件
- 问题排查功能仍然完整
## 相关文件
- `src/main/tray.js` - 托盘菜单(已简化)
- `src/main/utils/logger.js` - 日志系统(功能保留)
- `src/main/utils/singleInstance.js` - 单实例管理(功能保留)
- `src/main/utils/cacheUtils.js` - 缓存管理(功能保留)

View File

@@ -3,28 +3,46 @@ import { electronApp, optimizer } from '@electron-toolkit/utils'
import Store from 'electron-store' import Store from 'electron-store'
import { createWindow, createDrageWindow, unregisterAllShortcuts } from './window.js' import { createWindow, createDrageWindow, unregisterAllShortcuts } from './window.js'
import { setupIPC } from './ipc.js' import { setupIPC } from './ipc.js'
import { createTray, destroyTray } from './tray.js' import { createTray, destroyTray,clearBrowserCache } from './tray.js'
import { getStoreValue } from './store.js'
import XEUtils from 'xe-utils' import XEUtils from 'xe-utils'
import fs from 'fs'
import path from 'path'
import dayjs from 'dayjs'
import logger from './utils/logger' import logger from './utils/logger'
import { clearAllSessionData } from './utils/cacheUtils.js'
import SingleInstanceManager from './utils/singleInstance.js'
import { setStoreValue, getStoreValue ,deleteStore} from './store.js'
import AutoLaunch from 'auto-launch' import AutoLaunch from 'auto-launch'
import WebSocketClient from './utils/WebSocketClient'; import WebSocketClient from './utils/WebSocketClient';
let wsClient=null let wsClient=null
wsClient=new WebSocketClient({ // 延迟创建WebSocket连接等待应用完全启动后再连接
autoReconnect: true, function createWebSocketClient() {
autoReconnectAttempts: 9999, try {
autoReconnectInterval: 5000, wsClient = new WebSocketClient({
timeout: 30000, autoReconnect: true,
}) autoReconnectAttempts: 9999,
autoReconnectInterval: 5000,
timeout: 30000,
});
wsClient.on('open', () => { wsClient.on('open', () => {
logger.info('WebSocket连接已打开') logger.info('WebSocket连接已打开')
}); });
wsClient.on('error', (error) => {
logger.error('WebSocket连接错误:', error)
});
wsClient.on('close', (data) => {
logger.info('WebSocket连接已关闭:', data)
});
} catch (error) {
logger.error('创建WebSocket客户端失败:', error)
}
}
var minecraftAutoLauncher = new AutoLaunch({ var minecraftAutoLauncher = new AutoLaunch({
@@ -53,40 +71,132 @@ if(!XEUtils.isEmpty(difySite)){
logger.info(`当前运行平台: ${process.platform}`) logger.info(`当前运行平台: ${process.platform}`)
if (process.platform === 'win32') { if (process.platform === 'win32') {
logger.info('Windows 系统,设置控制台编码为 UTF-8') logger.info('Windows 系统,设置控制台编码为 UTF-8')
require('child_process').execSync('chcp 65001', { stdio: 'ignore' }) try {
require('child_process').execSync('chcp 65001', { stdio: 'ignore' })
} catch (error) {
logger.error('设置控制台编码失败:', error)
}
} else { } else {
logger.info('非 Windows 系统,使用默认编码') logger.info('非 Windows 系统,使用默认编码')
} }
logger.info('%cRed text. %cGreen text', 'color: red', 'color: green') logger.info('%cRed text. %cGreen text', 'color: red', 'color: green')
const store = new Store() try {
const store = new Store()
logger.info('Store 初始化成功')
} catch (error) {
logger.error('Store 初始化失败:', error)
}
app.commandLine.appendSwitch('disable-features', 'OutOfBlinkCors') app.commandLine.appendSwitch('disable-features', 'OutOfBlinkCors')
app.commandLine.appendSwitch('ignore-certificate-errors') app.commandLine.appendSwitch('ignore-certificate-errors')
app.disableHardwareAcceleration() // 添加缓存相关配置,避免缓存错误
app.commandLine.appendSwitch('disable-http-cache')
app.commandLine.appendSwitch('disable-background-timer-throttling')
app.commandLine.appendSwitch('disable-renderer-backgrounding')
try {
app.disableHardwareAcceleration()
logger.info('硬件加速已禁用')
} catch (error) {
logger.error('禁用硬件加速失败:', error)
}
logger.info(app.getPath('userData')) logger.info(app.getPath('userData'))
logger.info('应用启动,日志文件路径:', logger.getLogPath())
// 添加更多启动信息
logger.info('开始检查单实例锁...')
// 创建单实例管理器
const singleInstanceManager = new SingleInstanceManager()
// 清理缓存目录
function cleanupCacheDirectories() {
try {
const userDataPath = app.getPath('userData')
const cachePaths = [
path.join(userDataPath, 'Cache'),
path.join(userDataPath, 'Code Cache'),
path.join(userDataPath, 'GPUCache'),
path.join(userDataPath, 'Service Worker'),
path.join(userDataPath, 'Session Storage')
]
for (const cachePath of cachePaths) {
if (fs.existsSync(cachePath)) {
logger.info(`清理缓存目录: ${cachePath}`)
try {
// 递归删除目录
const deleteRecursive = (dirPath) => {
if (fs.existsSync(dirPath)) {
const files = fs.readdirSync(dirPath)
for (const file of files) {
const curPath = path.join(dirPath, file)
if (fs.lstatSync(curPath).isDirectory()) {
deleteRecursive(curPath)
} else {
try {
fs.unlinkSync(curPath)
} catch (error) {
logger.warn(`无法删除缓存文件: ${curPath}`, error.message)
}
}
}
try {
fs.rmdirSync(dirPath)
} catch (error) {
logger.warn(`无法删除缓存目录: ${dirPath}`, error.message)
}
}
}
deleteRecursive(cachePath)
logger.info(`缓存目录清理完成: ${cachePath}`)
} catch (error) {
logger.error(`清理缓存目录失败: ${cachePath}`, error)
}
}
}
} catch (error) {
logger.error('清理缓存目录失败:', error)
}
}
// 检查是否为第一个实例 // 检查是否为第一个实例
const gotTheLock = app.requestSingleInstanceLock() const isFirstInstance = singleInstanceManager.checkSingleInstance()
if (!gotTheLock) { if (!isFirstInstance) {
// 如果不是第一个实例,尝试激活第一个实例的窗口 logger.info('检测到已有实例运行,尝试激活现有实例')
const windows = BrowserWindow.getAllWindows()
if (windows.length > 0) { // 尝试激活第一个实例的窗口
const mainWindow = windows[0] try {
if (mainWindow.isMinimized()) { const windows = BrowserWindow.getAllWindows()
mainWindow.restore() if (windows.length > 0) {
const mainWindow = windows[0]
if (mainWindow.isMinimized()) {
mainWindow.restore()
}
mainWindow.show()
mainWindow.focus()
logger.info('成功激活现有实例窗口')
app.quit() // 退出当前实例
} else {
logger.warn('未找到现有实例窗口,可能是锁未正确释放')
logger.info('尝试强制启动新实例...')
// 继续启动
} }
mainWindow.show() } catch (error) {
mainWindow.focus() logger.error('激活现有实例失败:', error)
logger.info('尝试强制启动新实例...')
// 继续启动
} }
app.quit() // 退出当前实例
} else { } else {
logger.info('这是第一个实例,继续启动')
// 这是第一个实例 // 这是第一个实例
// 监听第二个实例的启动 // 监听第二个实例的启动
@@ -104,6 +214,10 @@ if (!gotTheLock) {
}) })
app.whenReady().then(() => { app.whenReady().then(() => {
logger.info('应用已准备就绪,开始初始化...');
// 清理缓存目录
cleanupCacheDirectories()
// 获取默认会话并全局设置允许第三方 Cookie // 获取默认会话并全局设置允许第三方 Cookie
const defaultSession = session.defaultSession; const defaultSession = session.defaultSession;
@@ -134,6 +248,13 @@ if (!gotTheLock) {
createTray() createTray()
setupIPC() setupIPC()
// exe退出三秒之后即认为关闭过exe程序退出登录
checkIsKeepAlive()
// 延迟创建WebSocket连接
setTimeout(() => {
createWebSocketClient()
}, 2000); // 延迟2秒创建WebSocket连接
minecraftAutoLauncher.isEnabled() minecraftAutoLauncher.isEnabled()
.then(function(isEnabled){ .then(function(isEnabled){
@@ -151,26 +272,134 @@ if (!gotTheLock) {
app.on('activate', function () { app.on('activate', function () {
if (BrowserWindow.getAllWindows().length === 0) createWindow() if (BrowserWindow.getAllWindows().length === 0) createWindow()
}) })
logger.info('应用初始化完成');
}) })
// 修改窗口关闭行为 // 修改窗口关闭行为
app.on('window-all-closed', () => { app.on('window-all-closed', async (event) => {
if (process.platform !== 'darwin') { if (process.platform !== 'darwin') {
event.preventDefault();
if (!app.isQuiting) { if (!app.isQuiting) {
// 如果不是主动退出,则隐藏所有窗口 // 如果不是主动退出,则隐藏所有窗口
BrowserWindow.getAllWindows().forEach(window => { BrowserWindow.getAllWindows().forEach(window => {
window.hide() window.hide()
}) })
// 清除所有会话数据
try {
await clearAllSessionData('window-all-closed');
} catch (error) {
logger.error('窗口关闭时清理缓存失败:', error);
}
// 清理完成后退出应用
app.quit();
} else { } else {
// 如果是主动退出,则销毁托盘并退出应用 // 如果是主动退出,则销毁托盘并退出应用
destroyTray() destroyTray()
app.quit()
} }
} }
}) })
app.on('before-quit', async (event) => {
// 在应用程序即将退出时执行操作,例如保存数据
event.preventDefault();
// 清除所有会话数据
try {
await clearAllSessionData('before-quit');
} catch (error) {
logger.error('应用退出时清理缓存失败:', error);
}
// 清理完成后退出应用
app.quit();
});
// 在应用退出时注销所有快捷键 // 在应用退出时注销所有快捷键
app.on('will-quit', () => { app.on('will-quit', async (event) => {
event.preventDefault();
// 清除所有会话数据
try {
await clearAllSessionData('will-quit');
} catch (error) {
logger.error('应用退出时清理缓存失败:', error);
}
unregisterAllShortcuts() unregisterAllShortcuts()
// 清理完成后退出应用
app.quit();
}) })
// 监听进程退出信号,确保在系统强制关闭时也能清理缓存
process.on('SIGTERM', async () => {
logger.info('收到 SIGTERM 信号,开始清理缓存');
try {
await clearAllSessionData('SIGTERM');
} catch (error) {
logger.error('SIGTERM 时清理缓存失败:', error);
}
process.exit(0);
});
process.on('SIGINT', async () => {
logger.info('收到 SIGINT 信号,开始清理缓存');
try {
await clearAllSessionData('SIGINT');
} catch (error) {
logger.error('SIGINT 时清理缓存失败:', error);
}
process.exit(0);
});
// 监听未捕获的异常,确保应用崩溃时也能记录日志
process.on('uncaughtException', (error) => {
logger.error('未捕获的异常:', error);
// 不要立即退出,给应用一个恢复的机会
logger.error('应用遇到未捕获的异常,但将继续运行');
});
process.on('unhandledRejection', (reason, promise) => {
logger.error('未处理的 Promise 拒绝:', reason);
// 不要立即退出,给应用一个恢复的机会
logger.error('应用遇到未处理的 Promise 拒绝,但将继续运行');
});
}
export function checkIsKeepAlive(){
const checkIsKeepAliveTimer=setInterval(async () => {
const lastAliveTime = getStoreValue("lastAliveTime")||null
if (lastAliveTime!=null){
const nowTime=dayjs();
const lastTime=dayjs(lastAliveTime);
const diff=nowTime.diff(lastTime, 'second')
// 上次在线事件在三秒之前则认为关闭过exe程序
if (diff>5){
logger.info('上次在线事件在三秒之前则认为关闭过exe程序')
await clearAllSessionData('超时5秒');
deleteStore("lastAliveTime")
}else{
setStoreValue("lastAliveTime",dayjs())
}
}
}, 1000 * 2)
} }

View File

@@ -10,7 +10,7 @@ import path from 'path';
log.initialize(); log.initialize();
import {checkForUpdates} from "./utils/updateUtils" import {checkForUpdates} from "./utils/updateUtils"
import logger from './utils/logger' import logger from './utils/logger'
import dayjs from 'dayjs'
function isValidUrl(_url) { function isValidUrl(_url) {
const containsApp = _url.includes('/app/'); const containsApp = _url.includes('/app/');
@@ -34,9 +34,8 @@ export function setupIPC() {
setStoreValue("difySite", data.difySite); setStoreValue("difySite", data.difySite);
setStoreValue("test_12222222", "test_12222222");
setStoreValue("lastAliveTime",dayjs())
difyRetryRequestTimer() difyRetryRequestTimer()

View File

@@ -1,38 +1,22 @@
import { Tray, Menu, app, BrowserWindow } from 'electron' import { Tray, Menu, app, BrowserWindow, shell } from 'electron'
import { join } from 'path' import { join } from 'path'
import icon from '../../resources/icon.png?asset' import icon from '../../resources/icon.png?asset'
import { getMainWindow, createWindow, createApiConfigWindow, createConfigWindow ,getDragWindow} from './window.js' import { getMainWindow, createWindow, createApiConfigWindow, createConfigWindow ,getDragWindow} from './window.js'
import { getStoreValue, setStoreValue, clearStore } from './store.js' import { getStoreValue, setStoreValue, clearStore } from './store.js'
import { createDrageWindow, getDrageWindow } from './window.js' import { createDrageWindow, getDrageWindow } from './window.js'
import logger from './utils/logger' import logger from './utils/logger'
import { clearAllWindowsCache } from './utils/cacheUtils.js'
import fs from 'fs' import fs from 'fs'
import path from 'path'
let tray = null let tray = null
import {checkForUpdates} from "./utils/updateUtils" import {checkForUpdates} from "./utils/updateUtils"
async function clearBrowserCache() { export async function clearBrowserCache() {
try { try {
// 清除所有窗口的浏览器缓存 // 使用统一的缓存清理函数
const windows = BrowserWindow.getAllWindows() await clearAllWindowsCache('tray-clear-cache');
for (const window of windows) {
const session = window.webContents.session
await session.clearStorageData({
storages: [
'appcache',
'cookies',
'filesystem',
'indexdb',
'localstorage',
'shadercache',
'websql',
'serviceworkers',
'cachestorage'
]
})
}
logger.info('浏览器缓存清除成功')
} catch (error) { } catch (error) {
logger.error('清除浏览器缓存失败:', error) logger.error('清除浏览器缓存失败:', error)
} }
@@ -108,6 +92,7 @@ export function createTray() {
checkForUpdates(menuItem,true) checkForUpdates(menuItem,true)
} }
}, },
{ type: 'separator' }, { type: 'separator' },
{ {
label: '清除缓存', label: '清除缓存',

View File

@@ -6,16 +6,19 @@ import { Notification } from "electron";
class WebSocketClient { class WebSocketClient {
constructor(options = {}) { constructor(options = {}) {
this.reconnectInterval = 5000; // 重连间隔时间 this.reconnectInterval = options.reconnectInterval || 5000; // 重连间隔时间
this.lockReconnect = false; this.lockReconnect = false;
this.ws = null; this.ws = null;
this.pingTimeout = null; this.pingTimeout = null;
this.reconnectAttempts = 0; this.reconnectAttempts = 0;
this.messageHandlers = new Map(); this.messageHandlers = new Map();
this.isConnected = false; this.isConnected = false;
this.options = options;
// 初始化时立即创建连接 // 延迟创建连接,避免在应用启动时立即连接
this.createConnect(); setTimeout(() => {
this.createConnect();
}, 1000);
} }
createConnect() { createConnect() {
@@ -26,10 +29,10 @@ class WebSocketClient {
// 检查必要的连接信息是否存在 // 检查必要的连接信息是否存在
if (!apiUrl || !userInfo || !token) { if (!apiUrl || !userInfo || !token) {
logger.error("WebSocket连接信息不完整等待重试"); logger.info("WebSocket连接信息不完整等待重试");
logger.error("apiUrl:", apiUrl); logger.info("apiUrl:", apiUrl);
logger.error("userInfo:", userInfo); logger.info("userInfo:", userInfo);
logger.error("token:", token); logger.info("token:", token);
this.scheduleReconnect(); this.scheduleReconnect();
return; return;
} }
@@ -92,10 +95,8 @@ class WebSocketClient {
this.ws = new WebSocket(this.url, token, this.options); this.ws = new WebSocket(this.url, token, this.options);
this.ws.on('open', () => { this.ws.on('open', () => {
logger.info('😀😀😀😀 WebSocket connect create ok 😀😀😀😀'); logger.info('WebSocket 连接成功');
logger.info('😀😀😀😀 WebSocket connect create ok 😀😀😀😀'); logger.info('WebSocket protocol:', this.ws.protocol);
logger.info('😀😀😀😀 WebSocket connect create ok 😀😀😀😀');
logger.info('WebSocket protocol:', this.ws.protocol); // 添加协议日志
this.isConnected = true; this.isConnected = true;
this.reconnectAttempts = 0; this.reconnectAttempts = 0;
this.lockReconnect = false; this.lockReconnect = false;
@@ -105,8 +106,19 @@ class WebSocketClient {
this.ws.on('message', (data) => { this.ws.on('message', (data) => {
try { try {
logger.info('收到消息:', data.toString()); const messageStr = data.toString();
const message = JSON.parse(data); const message = JSON.parse(messageStr);
// 减少心跳消息的日志打印
if (message.cmd === 'heartcheck') {
// 心跳消息不打印日志,或者只在调试模式下打印
if (process.env.NODE_ENV === 'development') {
logger.debug('收到心跳消息');
}
} else {
logger.info('收到消息:', messageStr);
}
this.handleMessage(message); this.handleMessage(message);
} catch (error) { } catch (error) {
logger.error('消息解析错误:', error); logger.error('消息解析错误:', error);

View File

@@ -0,0 +1,93 @@
import { session } from 'electron'
import logger from './logger.js'
/**
* 清除所有会话数据的统一函数
* @param {string} context - 清理上下文,用于日志记录
* @returns {Promise<void>}
*/
export async function clearAllSessionData(context = 'unknown') {
logger.info(`[${context}] 开始清除所有会话数据`);
try {
await new Promise((resolve, reject) => {
session.defaultSession.clearStorageData({
storages: [
'appcache',
'cookies',
'filesystem',
'indexdb',
'localstorage',
'shadercache',
'websql',
'serviceworkers',
'cachestorage'
]
}, (error) => {
if (error) {
logger.error(`[${context}] 清除会话数据失败:`, error);
reject(error);
} else {
logger.info(`[${context}] 会话数据清除成功`);
resolve();
}
});
});
// 强制同步日志
logger.info(`[${context}] 缓存清理完成`);
// 等待一小段时间确保日志写入
await new Promise(resolve => setTimeout(resolve, 100));
} catch (error) {
logger.error(`[${context}] 清理缓存失败:`, error);
throw error;
}
}
/**
* 清除所有窗口的浏览器缓存
* @param {string} context - 清理上下文,用于日志记录
* @returns {Promise<void>}
*/
export async function clearAllWindowsCache(context = 'unknown') {
logger.info(`[${context}] 开始清除所有窗口的浏览器缓存`);
try {
const { BrowserWindow } = require('electron');
const windows = BrowserWindow.getAllWindows();
for (const window of windows) {
const session = window.webContents.session;
await new Promise((resolve, reject) => {
session.clearStorageData({
storages: [
'appcache',
'cookies',
'filesystem',
'indexdb',
'localstorage',
'shadercache',
'websql',
'serviceworkers',
'cachestorage'
]
}, (error) => {
if (error) {
logger.error(`[${context}] 清除窗口缓存失败:`, error);
reject(error);
} else {
resolve();
}
});
});
}
logger.info(`[${context}] 所有窗口缓存清除成功`);
} catch (error) {
logger.error(`[${context}] 清除窗口缓存失败:`, error);
throw error;
}
}

View File

@@ -1,14 +1,72 @@
import log from 'electron-log/main'; import log from 'electron-log/main';
import { app } from 'electron';
import path from 'path';
log.initialize(); log.initialize();
// 配置日志立即写入
log.transports.file.level = 'info';
log.transports.console.level = 'info';
// 确保日志立即写入文件
log.transports.file.sync = true;
// 获取日志文件路径
const logFilePath = log.transports.file.getFile().path;
log.info(`日志文件路径: ${logFilePath}`);
// 日志级别配置
const LOG_LEVELS = {
ERROR: 0,
WARN: 1,
INFO: 2,
DEBUG: 3
};
let currentLogLevel = LOG_LEVELS.INFO; // 默认日志级别
// 设置日志级别
export function setLogLevel(level) {
if (typeof level === 'string') {
currentLogLevel = LOG_LEVELS[level.toUpperCase()] || LOG_LEVELS.INFO;
} else {
currentLogLevel = level;
}
}
// 检查是否应该打印日志
function shouldLog(level) {
return level <= currentLogLevel;
}
export const logger = { export const logger = {
log: log.info, log: (message, ...args) => {
info: log.info, if (shouldLog(LOG_LEVELS.INFO)) {
error: log.error, log.info(message, ...args);
warn: log.warn, }
debug: log.debug },
info: (message, ...args) => {
if (shouldLog(LOG_LEVELS.INFO)) {
log.info(message, ...args);
}
},
error: (message, ...args) => {
if (shouldLog(LOG_LEVELS.ERROR)) {
log.error(message, ...args);
}
},
warn: (message, ...args) => {
if (shouldLog(LOG_LEVELS.WARN)) {
log.warn(message, ...args);
}
},
debug: (message, ...args) => {
if (shouldLog(LOG_LEVELS.DEBUG)) {
log.debug(message, ...args);
}
},
getLogPath: () => logFilePath,
setLogLevel: setLogLevel
} }
export default logger export default logger

View File

@@ -0,0 +1,205 @@
import { app } from 'electron'
import fs from 'fs'
import path from 'path'
import logger from './logger.js'
/**
* 单实例锁管理工具
*/
export class SingleInstanceManager {
constructor() {
this.lockFile = null
this.isLocked = false
}
/**
* 清理可能存在的无效单实例锁
*/
cleanupInvalidLock() {
try {
const userDataPath = app.getPath('userData')
const possibleLockFiles = [
path.join(userDataPath, 'single-instance-lock'),
path.join(userDataPath, 'lockfile'),
path.join(userDataPath, '.lock'),
path.join(process.cwd(), 'single-instance-lock'),
path.join(process.cwd(), 'lockfile'),
path.join(process.cwd(), '.lock')
]
for (const lockFile of possibleLockFiles) {
if (fs.existsSync(lockFile)) {
logger.info(`发现锁文件: ${lockFile}`)
// 检查锁文件是否有效(检查进程是否还在运行)
try {
const lockContent = fs.readFileSync(lockFile, 'utf8')
const pid = parseInt(lockContent.trim())
if (pid && this.isProcessRunning(pid)) {
logger.info(`进程 ${pid} 仍在运行,锁文件有效`)
} else {
logger.info(`进程 ${pid} 不存在,尝试删除无效锁文件`)
this.safeDeleteFile(lockFile)
}
} catch (error) {
logger.info(`锁文件无效,尝试删除: ${lockFile}`)
this.safeDeleteFile(lockFile)
}
}
}
} catch (error) {
logger.error('清理无效锁文件失败:', error)
}
}
/**
* 安全删除文件,处理文件被占用的情况
*/
safeDeleteFile(filePath) {
try {
// 首先尝试直接删除
fs.unlinkSync(filePath)
logger.info(`成功删除文件: ${filePath}`)
} catch (error) {
if (error.code === 'EBUSY' || error.code === 'EACCES') {
logger.warn(`文件被占用,无法删除: ${filePath}`)
logger.warn(`错误信息: ${error.message}`)
// 在 Windows 上尝试使用 del 命令
if (process.platform === 'win32') {
try {
const { execSync } = require('child_process')
execSync(`del /F /Q "${filePath}"`, { stdio: 'ignore' })
logger.info(`使用 del 命令删除文件: ${filePath}`)
} catch (delError) {
logger.error(`del 命令删除失败: ${filePath}`, delError)
}
}
} else {
logger.error(`删除文件失败: ${filePath}`, error)
}
}
}
/**
* 检查进程是否还在运行
*/
isProcessRunning(pid) {
try {
// 在 Windows 上使用 tasklist 命令检查进程
if (process.platform === 'win32') {
const { execSync } = require('child_process')
const result = execSync(`tasklist /FI "PID eq ${pid}" /FO CSV /NH`, { encoding: 'utf8' })
return result.includes(pid.toString())
} else {
// 在 Unix 系统上使用 kill -0 检查进程
const { execSync } = require('child_process')
execSync(`kill -0 ${pid}`, { stdio: 'ignore' })
return true
}
} catch (error) {
return false
}
}
/**
* 创建锁文件
*/
createLock() {
try {
const userDataPath = app.getPath('userData')
this.lockFile = path.join(userDataPath, 'single-instance-lock')
// 写入当前进程的 PID
fs.writeFileSync(this.lockFile, process.pid.toString())
this.isLocked = true
logger.info(`创建单实例锁文件: ${this.lockFile}, PID: ${process.pid}`)
// 监听应用退出事件,清理锁文件
app.on('before-quit', () => {
this.removeLock()
})
app.on('will-quit', () => {
this.removeLock()
})
// 监听进程退出信号
process.on('exit', () => {
this.removeLock()
})
process.on('SIGINT', () => {
this.removeLock()
process.exit(0)
})
process.on('SIGTERM', () => {
this.removeLock()
process.exit(0)
})
} catch (error) {
logger.error('创建锁文件失败:', error)
}
}
/**
* 移除锁文件
*/
removeLock() {
if (this.lockFile && this.isLocked) {
try {
if (fs.existsSync(this.lockFile)) {
fs.unlinkSync(this.lockFile)
logger.info(`移除锁文件: ${this.lockFile}`)
}
this.isLocked = false
} catch (error) {
logger.error('移除锁文件失败:', error)
}
}
}
/**
* 检查是否为第一个实例
*/
checkSingleInstance() {
// 首先清理可能存在的无效锁
this.cleanupInvalidLock()
// 尝试获取单实例锁
const gotTheLock = app.requestSingleInstanceLock()
logger.info(`单实例锁检查结果: ${gotTheLock}`)
if (gotTheLock) {
// 成功获取锁,创建锁文件
this.createLock()
return true
} else {
// 无法获取锁,检查是否真的有必要阻止启动
logger.info('检测到已有实例运行')
// 检查是否有实际的窗口存在
const windows = require('electron').BrowserWindow.getAllWindows()
if (windows.length === 0) {
logger.warn('未找到现有实例窗口,可能是锁文件问题,允许强制启动')
// 尝试强制获取锁
try {
this.createLock()
return true
} catch (error) {
logger.error('强制创建锁失败:', error)
return false
}
} else {
logger.info(`找到 ${windows.length} 个现有窗口,阻止新实例启动`)
return false
}
}
}
}
export default SingleInstanceManager

View File

@@ -11,13 +11,12 @@ import { setStoreValue, getStoreValue,deleteStore } from './store.js'
import {checkForUpdates} from "./utils/updateUtils" import {checkForUpdates} from "./utils/updateUtils"
import dayjs from 'dayjs' import dayjs from 'dayjs'
let mainWindow = null let mainWindow = null
let difyfullScreenWindow = null let difyfullScreenWindow = null
let drageWindow = null let drageWindow = null
let apiConfigWindow = null let apiConfigWindow = null
let configWindow = null let configWindow = null
import { clearAllSessionData } from './utils/cacheUtils.js'
// 权限授权 // 权限授权
async function checkMediaAccess(mediaType){ async function checkMediaAccess(mediaType){
const result = systemPreferences.getMediaAccessStatus(mediaType) const result = systemPreferences.getMediaAccessStatus(mediaType)
@@ -109,6 +108,9 @@ export async function createWindow() {
mainWindow.webContents.on('before-input-event', (event, input) => { mainWindow.webContents.on('before-input-event', (event, input) => {
if (input.key === 'F12') { if (input.key === 'F12') {
mainWindow.webContents.toggleDevTools() mainWindow.webContents.toggleDevTools()
} else if (input.key === 'F5') {
logger.info('主窗口 F5 快捷键触发')
mainWindow.reload()
} }
}) })
@@ -164,9 +166,11 @@ export async function createWindow() {
// 加载存储的 URL // 加载存储的 URL
mainWindow.loadURL(h5_client_url) mainWindow.loadURL(h5_client_url)
// 超过30分钟不活动则退出登录 // 接口超过30分钟不活动则退出登录
await tokenExpireTimer() await tokenExpireTimer()
setTimeout(()=>{ setTimeout(()=>{
try { try {
// 注册全局快捷键 // 注册全局快捷键
@@ -189,21 +193,44 @@ export async function createWindow() {
}); });
} }
// 添加一个专门的快捷键注册函数 // 添加一个专门的快捷键注册函数
function registerShortcuts(window) { function registerShortcuts(window=null) {
// 先注销可能存在的F5快捷键
globalShortcut.unregister('F5')
// 注册 F5 刷新快捷键 // 注册 F5 刷新快捷键
globalShortcut.register('F5', () => { const success = globalShortcut.register('F5', () => {
logger.info('F5 快捷键触发') logger.info('F5 快捷键触发')
if (window && !window.isDestroyed()) {
window.reload() try {
// 获取当前焦点窗口
const focusedWindow = BrowserWindow.getFocusedWindow()
logger.info('当前焦点窗口:', focusedWindow ? '存在' : '不存在')
if (focusedWindow && !focusedWindow.isDestroyed()) {
logger.info('刷新当前焦点窗口')
focusedWindow.reload()
} else if (mainWindow && !mainWindow.isDestroyed()) {
logger.info('没有焦点窗口,刷新主窗口')
mainWindow.reload()
} else if (difyfullScreenWindow && !difyfullScreenWindow.isDestroyed()) {
logger.info('主窗口不可用,刷新全屏窗口')
difyfullScreenWindow.reload()
} else {
logger.warn('没有可用的窗口进行刷新')
}
} catch (error) {
logger.error('F5快捷键执行出错:', error)
} }
}) })
logger.info(`F5快捷键注册${success ? '成功' : '失败'}`)
const isRegistered_F12 = globalShortcut.isRegistered('F12'); const isRegistered_F12 = globalShortcut.isRegistered('F12');
logger.info(`Is CommandOrControl+X registered: ${isRegistered_F12}`); logger.info(`Is F12 registered: ${isRegistered_F12}`);
// 桌面端快要退出的时候,注销快捷键 // 桌面端快要退出的时候,注销快捷键
app.on('will-quit', () => { app.on('will-quit', () => {
@@ -236,6 +263,8 @@ export async function createNewWindow(url, access_token, refresh_token,sandbox=f
difyfullScreenWindow.on('ready-to-show', () => { difyfullScreenWindow.on('ready-to-show', () => {
difyfullScreenWindow.show() difyfullScreenWindow.show()
// 确保快捷键在新窗口显示后也能工作
registerShortcuts(difyfullScreenWindow)
}) })
let code = `localStorage.setItem("IsHsAiApp","IsHsAiApp");localStorage.setItem("console_token","${access_token}");localStorage.setItem("refresh_token","12");` let code = `localStorage.setItem("IsHsAiApp","IsHsAiApp");localStorage.setItem("console_token","${access_token}");localStorage.setItem("refresh_token","12");`
@@ -298,6 +327,9 @@ export async function createNewWindow(url, access_token, refresh_token,sandbox=f
difyfullScreenWindow.webContents.on('before-input-event', (event, input) => { difyfullScreenWindow.webContents.on('before-input-event', (event, input) => {
if (input.key === 'F12') { if (input.key === 'F12') {
difyfullScreenWindow.webContents.toggleDevTools() difyfullScreenWindow.webContents.toggleDevTools()
} else if (input.key === 'F5') {
logger.info('全屏窗口 F5 快捷键触发')
difyfullScreenWindow.reload()
} }
}) })
@@ -467,18 +499,29 @@ export function createDrageWindow() {
drageWindow.setPosition(screenWidth - drageWindow.getSize()[0] -100, screenHeight - drageWindow.getSize()[1] - 100) drageWindow.setPosition(screenWidth - drageWindow.getSize()[0] -100, screenHeight - drageWindow.getSize()[1] - 100)
} }
export async function tokenExpireTimer(){ export async function tokenExpireTimer(){
let lastLogTime = 0; // 记录上次打印日志的时间
const LOG_INTERVAL = 60000; // 日志打印间隔60秒打印一次
const tokenExpireTimer = setInterval(async () => { const tokenExpireTimer = setInterval(async () => {
logger.info("tokenExpireTimer 触发") const currentTime = Date.now();
const lastActiveTime = getStoreValue("lastActiveTime")||null const lastActiveTime = getStoreValue("lastActiveTime")||null
if (lastActiveTime!=null){ if (lastActiveTime!=null){
logger.info("tokenExpireTimer 触发 对比时间戳")
try { try {
const nowTime=dayjs(); const nowTime=dayjs();
const lastTime=dayjs(lastActiveTime); const lastTime=dayjs(lastActiveTime);
const diff=nowTime.diff(lastTime, 'minute') const diff=nowTime.diff(lastTime, 'minute')
logger.info("tokenExpireTimer 触发 对比时间戳差距为:"+diff)
// 只在特定条件下打印日志,减少日志频率
if (currentTime - lastLogTime > LOG_INTERVAL) {
logger.info(`tokenExpireTimer 检查 - 时间差距: ${diff}分钟`)
lastLogTime = currentTime;
}
if ( diff> 30) { if ( diff> 30) {
logger.info(`用户超时退出登录 - 时间差距: ${diff}分钟`)
deleteStore("lastActiveTime") deleteStore("lastActiveTime")
try { try {
// 清除所有窗口的浏览器缓存 // 清除所有窗口的浏览器缓存
@@ -512,7 +555,7 @@ export async function tokenExpireTimer(){
} }
} catch (e) { } catch (e) {
logger.error('tokenExpireTimer 执行错误:', e)
} }
} }
}, 1000 * 10) }, 1000 * 10)
@@ -610,3 +653,32 @@ export function closeConfigWindow() {
configWindow = null configWindow = null
} }
} }
// 测试快捷键是否正常工作
export function testShortcuts() {
logger.info('测试快捷键功能...')
// 检查F5快捷键是否已注册
const isF5Registered = globalShortcut.isRegistered('F5')
logger.info(`F5快捷键是否已注册: ${isF5Registered}`)
// 获取所有已注册的快捷键
const allShortcuts = globalShortcut.isRegistered('F5') ? ['F5'] : []
logger.info(`已注册的快捷键: ${allShortcuts.join(', ')}`)
// 获取当前焦点窗口
const focusedWindow = BrowserWindow.getFocusedWindow()
logger.info(`当前焦点窗口: ${focusedWindow ? '存在' : '不存在'}`)
if (focusedWindow) {
logger.info(`焦点窗口标题: ${focusedWindow.getTitle()}`)
logger.info(`焦点窗口是否销毁: ${focusedWindow.isDestroyed()}`)
}
return {
f5Registered: isF5Registered,
focusedWindow: focusedWindow ? 'exists' : 'none',
mainWindow: mainWindow && !mainWindow.isDestroyed() ? 'exists' : 'none',
difyWindow: difyfullScreenWindow && !difyfullScreenWindow.isDestroyed() ? 'exists' : 'none'
}
}

44
test-cache-clear.js Normal file
View File

@@ -0,0 +1,44 @@
const { app, session } = require('electron');
// 模拟缓存清理测试
async function testCacheClear() {
console.log('开始测试缓存清理功能...');
try {
// 测试清除所有会话数据
await new Promise((resolve, reject) => {
session.defaultSession.clearStorageData({
storages: [
'appcache',
'cookies',
'filesystem',
'indexdb',
'localstorage',
'shadercache',
'websql',
'serviceworkers',
'cachestorage'
]
}, (error) => {
if (error) {
console.error('清除会话数据失败:', error);
reject(error);
} else {
console.log('会话数据清除成功');
resolve();
}
});
});
console.log('缓存清理测试完成');
} catch (error) {
console.error('缓存清理测试失败:', error);
}
}
// 如果直接运行此脚本
if (require.main === module) {
testCacheClear();
}
module.exports = { testCacheClear };

138
test-lock-cleanup.js Normal file
View File

@@ -0,0 +1,138 @@
const { app } = require('electron');
const fs = require('fs');
const path = require('path');
const { execSync } = require('child_process');
console.log('开始测试锁文件清理功能...');
// 测试安全删除文件功能
function testSafeDeleteFile(filePath) {
console.log(`测试删除文件: ${filePath}`);
try {
// 首先尝试直接删除
fs.unlinkSync(filePath);
console.log(`成功删除文件: ${filePath}`);
} catch (error) {
if (error.code === 'EBUSY' || error.code === 'EACCES') {
console.log(`文件被占用,无法删除: ${filePath}`);
console.log(`错误信息: ${error.message}`);
// 在 Windows 上尝试使用 del 命令
if (process.platform === 'win32') {
try {
execSync(`del /F /Q "${filePath}"`, { stdio: 'ignore' });
console.log(`使用 del 命令删除文件: ${filePath}`);
} catch (delError) {
console.error(`del 命令删除失败: ${filePath}`, delError);
}
}
} else {
console.error(`删除文件失败: ${filePath}`, error);
}
}
}
// 测试创建和删除锁文件
function testLockFileOperations() {
const userDataPath = app.getPath('userData');
const testLockFile = path.join(userDataPath, 'test-lockfile');
console.log('用户数据路径:', userDataPath);
console.log('测试锁文件路径:', testLockFile);
try {
// 创建测试锁文件
fs.writeFileSync(testLockFile, process.pid.toString());
console.log(`创建测试锁文件: ${testLockFile}, PID: ${process.pid}`);
// 等待一段时间
setTimeout(() => {
console.log('开始测试删除锁文件...');
testSafeDeleteFile(testLockFile);
// 检查文件是否还存在
if (fs.existsSync(testLockFile)) {
console.log('文件仍然存在,尝试强制删除...');
if (process.platform === 'win32') {
try {
execSync(`del /F /Q "${testLockFile}"`, { stdio: 'ignore' });
console.log('强制删除成功');
} catch (error) {
console.error('强制删除失败:', error);
}
}
} else {
console.log('文件已成功删除');
}
process.exit(0);
}, 2000);
} catch (error) {
console.error('创建测试锁文件失败:', error);
process.exit(1);
}
}
// 测试检查现有锁文件
function testExistingLockFiles() {
const userDataPath = app.getPath('userData');
const possibleLockFiles = [
path.join(userDataPath, 'single-instance-lock'),
path.join(userDataPath, 'lockfile'),
path.join(userDataPath, '.lock')
];
console.log('检查现有锁文件...');
for (const lockFile of possibleLockFiles) {
if (fs.existsSync(lockFile)) {
console.log(`发现锁文件: ${lockFile}`);
try {
const content = fs.readFileSync(lockFile, 'utf8');
console.log(`锁文件内容: ${content}`);
const pid = parseInt(content.trim());
if (pid) {
console.log(`锁文件中的 PID: ${pid}`);
// 检查进程是否还在运行
if (process.platform === 'win32') {
try {
const result = execSync(`tasklist /FI "PID eq ${pid}" /FO CSV /NH`, { encoding: 'utf8' });
if (result.includes(pid.toString())) {
console.log(`进程 ${pid} 仍在运行`);
} else {
console.log(`进程 ${pid} 不存在,可以删除锁文件`);
testSafeDeleteFile(lockFile);
}
} catch (error) {
console.log(`进程 ${pid} 不存在,删除锁文件`);
testSafeDeleteFile(lockFile);
}
}
}
} catch (error) {
console.error(`读取锁文件失败: ${lockFile}`, error);
testSafeDeleteFile(lockFile);
}
} else {
console.log(`没有找到锁文件: ${lockFile}`);
}
}
}
// 运行测试
console.log('当前进程 PID:', process.pid);
// 检查现有锁文件
testExistingLockFiles();
// 如果直接运行此脚本
if (require.main === module) {
// 测试锁文件操作
testLockFileOperations();
}
module.exports = { testSafeDeleteFile, testExistingLockFiles, testLockFileOperations };

87
test-single-instance.js Normal file
View File

@@ -0,0 +1,87 @@
const { app } = require('electron');
const fs = require('fs');
const path = require('path');
console.log('开始测试单实例锁功能...');
// 模拟单实例锁检查
function testSingleInstanceLock() {
const userDataPath = app.getPath('userData');
const lockFile = path.join(userDataPath, 'single-instance-lock');
console.log('用户数据路径:', userDataPath);
console.log('锁文件路径:', lockFile);
// 检查是否存在锁文件
if (fs.existsSync(lockFile)) {
console.log('发现锁文件,尝试读取内容...');
try {
const content = fs.readFileSync(lockFile, 'utf8');
console.log('锁文件内容:', content);
// 检查进程是否还在运行
const pid = parseInt(content.trim());
console.log('锁文件中的 PID:', pid);
// 在 Windows 上检查进程
if (process.platform === 'win32') {
const { execSync } = require('child_process');
try {
const result = execSync(`tasklist /FI "PID eq ${pid}" /FO CSV /NH`, { encoding: 'utf8' });
if (result.includes(pid.toString())) {
console.log(`进程 ${pid} 仍在运行`);
} else {
console.log(`进程 ${pid} 不存在,可以清理锁文件`);
fs.unlinkSync(lockFile);
console.log('锁文件已清理');
}
} catch (error) {
console.log(`进程 ${pid} 不存在,清理锁文件`);
fs.unlinkSync(lockFile);
console.log('锁文件已清理');
}
}
} catch (error) {
console.error('读取锁文件失败:', error);
}
} else {
console.log('没有找到锁文件');
}
}
// 测试创建锁文件
function testCreateLock() {
const userDataPath = app.getPath('userData');
const lockFile = path.join(userDataPath, 'single-instance-lock');
try {
fs.writeFileSync(lockFile, process.pid.toString());
console.log(`创建锁文件: ${lockFile}, PID: ${process.pid}`);
} catch (error) {
console.error('创建锁文件失败:', error);
}
}
// 运行测试
console.log('当前进程 PID:', process.pid);
testSingleInstanceLock();
// 如果直接运行此脚本
if (require.main === module) {
// 创建测试锁文件
testCreateLock();
// 等待一段时间后清理
setTimeout(() => {
console.log('清理测试锁文件...');
const userDataPath = app.getPath('userData');
const lockFile = path.join(userDataPath, 'single-instance-lock');
if (fs.existsSync(lockFile)) {
fs.unlinkSync(lockFile);
console.log('测试锁文件已清理');
}
process.exit(0);
}, 5000);
}
module.exports = { testSingleInstanceLock, testCreateLock };

42
test-startup.js Normal file
View File

@@ -0,0 +1,42 @@
const { app } = require('electron');
const path = require('path');
console.log('开始测试应用启动...');
// 模拟应用启动过程
app.on('ready', () => {
console.log('应用已准备就绪');
});
app.on('window-all-closed', () => {
console.log('所有窗口已关闭');
if (process.platform !== 'darwin') {
app.quit();
}
});
app.on('before-quit', () => {
console.log('应用即将退出');
});
app.on('will-quit', () => {
console.log('应用将要退出');
});
// 检查单实例锁
const gotTheLock = app.requestSingleInstanceLock();
console.log('单实例锁检查结果:', gotTheLock);
if (!gotTheLock) {
console.log('检测到已有实例运行,退出当前实例');
app.quit();
} else {
console.log('这是第一个实例,继续启动');
// 模拟异步操作
setTimeout(() => {
console.log('异步操作完成');
}, 1000);
}
console.log('启动脚本执行完成');