first-update

This commit is contained in:
2026-03-17 14:36:31 +08:00
parent 72f08aee7c
commit 4eddf05e79
516 changed files with 115270 additions and 1 deletions

View File

@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.cs.allow-jit</key>
<true/>
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
<true/>
<key>com.apple.security.cs.allow-dyld-environment-variables</key>
<true/>
<key>com.apple.security.network.client</key>
<true/>
<key>com.apple.security.files.user-selected.read-write</key>
<true/>
</dict>
</plist>

View File

@@ -0,0 +1,124 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Easy Dataset Loading...</title>
<style>
body {
font-family:
-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans',
'Helvetica Neue', sans-serif;
background-color: #f5f5f5;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
height: 100vh;
margin: 0;
padding: 0;
color: #333;
overflow: hidden;
}
.container {
text-align: center;
max-width: 500px;
padding: 20px;
}
.logo {
width: 120px;
height: 120px;
margin-bottom: 20px;
animation: pulse 2s infinite;
}
.title {
font-size: 24px;
font-weight: 600;
margin-bottom: 10px;
}
.subtitle {
font-size: 16px;
color: #666;
margin-bottom: 30px;
}
.loading {
display: flex;
justify-content: center;
margin-top: 20px;
}
.loading-dot {
width: 10px;
height: 10px;
margin: 0 5px;
background-color: #1976d2;
border-radius: 50%;
animation: loading 1.4s infinite ease-in-out both;
}
.loading-dot:nth-child(1) {
animation-delay: -0.32s;
}
.loading-dot:nth-child(2) {
animation-delay: -0.16s;
}
@keyframes loading {
0%,
80%,
100% {
transform: scale(0);
}
40% {
transform: scale(1);
}
}
@keyframes pulse {
0% {
transform: scale(1);
}
50% {
transform: scale(1.05);
}
100% {
transform: scale(1);
}
}
.version {
position: fixed;
bottom: 20px;
font-size: 12px;
color: #999;
}
</style>
</head>
<body>
<div class="container">
<img
src="../public/imgs/logo.png"
alt="Easy Dataset Logo"
class="logo"
onerror="
this.src = '../public/favicon.ico';
this.style.width = '80px';
this.style.height = '80px';
"
/>
<h1 class="title">Easy Dataset</h1>
<p class="subtitle">The first startup may take a bit longer to load. Please be patient. ...</p>
<div class="loading">
<div class="loading-dot"></div>
<div class="loading-dot"></div>
<div class="loading-dot"></div>
</div>
</div>
<div class="version" id="version"></div>
<script>
// 获取版本信息
window.addEventListener('DOMContentLoaded', () => {
if (window.electronAPI) {
const version = window.electronAPI.getAppVersion();
document.getElementById('version').textContent = `版本: v${version}`;
}
});
</script>
</body>
</html>

View File

@@ -0,0 +1,83 @@
const { app, dialog, ipcMain } = require('electron');
const { setupLogging, setupIpcLogging } = require('./modules/logger');
const { createWindow, loadAppUrl, openDevTools, getMainWindow } = require('./modules/window-manager');
const { createMenu } = require('./modules/menu');
const { startNextServer } = require('./modules/server');
const { setupAutoUpdater } = require('./modules/updater');
const { initializeDatabase } = require('./modules/database');
const { clearCache } = require('./modules/cache');
const { setupIpcHandlers } = require('./modules/ipc-handlers');
// 是否是开发环境
const isDev = process.env.NODE_ENV === 'development';
const port = 1717;
let mainWindow;
// 当 Electron 完成初始化时创建窗口
app.whenReady().then(async () => {
try {
// 设置日志系统
setupLogging(app);
// 设置 IPC 处理程序
setupIpcHandlers(app, isDev);
setupIpcLogging(ipcMain, app, isDev);
// 初始化数据库
await initializeDatabase(app);
// 创建主窗口
mainWindow = createWindow(isDev, port);
// 创建菜单
createMenu(mainWindow, () => clearCache(app));
// 在开发环境中加载 localhost URL
if (isDev) {
loadAppUrl(`http://localhost:${port}`);
openDevTools();
} else {
// 在生产环境中启动 Next.js 服务
const appUrl = await startNextServer(port, app);
loadAppUrl(appUrl);
}
// 设置自动更新
setupAutoUpdater(mainWindow);
// 应用启动完成后的一段时间后自动检查更新
setTimeout(() => {
if (!isDev) {
const { autoUpdater } = require('electron-updater');
autoUpdater.checkForUpdates().catch(err => {
console.error('Automatic update check failed:', err);
});
}
}, 10000); // Check for updates after 10 seconds
} catch (error) {
console.error('An error occurred during application initialization:', error);
dialog.showErrorBox(
'Application Initialization Error',
`An error occurred during startup, which may affect application functionality.
Error details: ${error.message}`
);
}
});
// 当所有窗口关闭时退出应用
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
app.quit();
}
});
app.on('activate', () => {
if (BrowserWindow.getAllWindows().length === 0) {
mainWindow = createWindow(isDev, port);
}
});
// 应用退出前清理
app.on('before-quit', () => {
console.log('应用正在退出...');
});

View File

@@ -0,0 +1,21 @@
const { clearLogs } = require('./logger');
const { clearDatabaseCache } = require('./database');
/**
* 清除缓存函数 - 清理logs和local-db目录
* @param {Object} app Electron app 对象
* @returns {Promise<boolean>} 操作是否成功
*/
async function clearCache(app) {
// 清理日志目录
await clearLogs(app);
// 清理数据库缓存
await clearDatabaseCache(app);
return true;
}
module.exports = {
clearCache
};

View File

@@ -0,0 +1,147 @@
const fs = require('fs');
const path = require('path');
const { dialog } = require('electron');
const { updateDatabase } = require('./db-updater');
/**
* 清除数据库缓存
* @param {Object} app Electron app 对象
* @returns {Promise<boolean>} 操作是否成功
*/
async function clearDatabaseCache(app) {
// 清理local-db目录保留db.sqlite文件
const localDbDir = path.join(app.getPath('userData'), 'local-db');
if (fs.existsSync(localDbDir)) {
// 读取目录下所有文件
const files = await fs.promises.readdir(localDbDir);
// 删除除了db.sqlite之外的所有文件
for (const file of files) {
if (file !== 'db.sqlite') {
const filePath = path.join(localDbDir, file);
const stat = await fs.promises.stat(filePath);
if (stat.isFile()) {
await fs.promises.unlink(filePath);
global.appLog(`已删除数据库缓存文件: ${filePath}`);
} else if (stat.isDirectory()) {
// 如果是目录,可能需要递归删除,根据需求决定
global.appLog(`跳过目录: ${filePath}`);
}
}
}
}
return true;
}
/**
* 初始化数据库
* @param {Object} app Electron app 对象
* @returns {Promise<Object>} 数据库配置信息
*/
async function initializeDatabase(app) {
try {
// 设置数据库路径
const userDataPath = app.getPath('userData');
const dataDir = path.join(userDataPath, 'local-db');
const dbFilePath = path.join(dataDir, 'db.sqlite');
const dbJSONPath = path.join(dataDir, 'db.json');
fs.writeFileSync(path.join(process.resourcesPath, 'root-path.txt'), dataDir);
// 确保数据目录存在
if (!fs.existsSync(dataDir)) {
fs.mkdirSync(dataDir, { recursive: true });
console.log(`数据目录已创建: ${dataDir}`);
}
// 设置数据库连接字符串 (Prisma 格式)
const dbConnectionString = `file:${dbFilePath}`;
process.env.DATABASE_URL = dbConnectionString;
// 仅在开发环境记录日志
const logs = {
userDataPath,
dataDir,
dbFilePath,
dbConnectionString,
dbExists: fs.existsSync(dbFilePath)
};
global.appLog(`数据库配置: ${JSON.stringify(logs)}`);
if (!fs.existsSync(dbFilePath)) {
global.appLog('数据库文件不存在,正在初始化...');
try {
const resourcePath =
process.env.NODE_ENV === 'development'
? path.join(__dirname, '../..', 'prisma', 'template.sqlite')
: path.join(process.resourcesPath, 'prisma', 'template.sqlite');
const resourceJSONPath =
process.env.NODE_ENV === 'development'
? path.join(__dirname, '../..', 'prisma', 'sql.json')
: path.join(process.resourcesPath, 'prisma', 'sql.json');
global.appLog(`resourcePath: ${resourcePath}`);
if (fs.existsSync(resourcePath)) {
fs.copyFileSync(resourcePath, dbFilePath);
global.appLog(`数据库已从模板初始化: ${dbFilePath}`);
}
if (fs.existsSync(resourceJSONPath)) {
fs.copyFileSync(resourceJSONPath, dbJSONPath);
global.appLog(`数据库SQL配置已初始化: ${dbJSONPath}`);
}
} catch (error) {
console.error('数据库初始化失败:', error);
dialog.showErrorBox('数据库初始化失败', `应用无法初始化数据库,可能需要重新安装。\n错误详情: ${error.message}`);
throw error;
}
} else {
// 数据库文件存在,检查是否需要更新
global.appLog('检查数据库是否需要更新...');
try {
const resourcesPath =
process.env.NODE_ENV === 'development' ? path.join(__dirname, '../..') : process.resourcesPath;
const isDev = process.env.NODE_ENV === 'development';
// 更新数据库
const result = await updateDatabase(userDataPath, resourcesPath, isDev, global.appLog);
if (result.updated) {
global.appLog(`数据库更新成功: ${result.message}`);
global.appLog(`执行的版本: ${result.executedVersions.join(', ')}`);
} else {
global.appLog(`数据库无需更新: ${result.message}`);
}
} catch (error) {
console.error('数据库更新失败:', error);
global.appLog(`数据库更新失败: ${error.message}`, 'error');
// 非致命错误,只提示但不阻止应用启动
dialog.showMessageBox({
type: 'warning',
title: '数据库更新警告',
message: '数据库更新过程中出现错误,部分功能可能受影响。',
detail: `错误详情: ${error.message}\n\n您可以继续使用应用,但如果遇到问题,请重新安装应用。`,
buttons: ['继续']
});
}
}
return {
userDataPath,
dataDir,
dbFilePath,
dbConnectionString
};
} catch (error) {
console.error('初始化数据库时发生错误:', error);
throw error;
}
}
module.exports = {
clearDatabaseCache,
initializeDatabase
};

View File

@@ -0,0 +1,179 @@
const fs = require('fs');
const path = require('path');
const { PrismaClient } = require('@prisma/client');
/**
* 执行SQL命令
* @param {string} dbUrl 数据库连接 URL
* @param {string} sql SQL命令
* @returns {Promise<void>}
*/
async function executeSql(dbUrl, sql) {
// 允许多条SQL语句分开执行支持分号和空行分隔
const statements = sql
.split(';')
.map(stmt => stmt.trim())
.filter(stmt => stmt.length > 0);
if (statements.length === 0) {
return;
}
// 设置环境变量
process.env.DATABASE_URL = dbUrl;
// 创建Prisma实例
const prisma = new PrismaClient();
try {
// 执行每条SQL语句
for (const statement of statements) {
await prisma.$executeRawUnsafe(statement);
}
} finally {
// 关闭连接
await prisma.$disconnect();
}
}
/**
* 获取本地和应用的SQL配置文件
* @param {string} userDataPath 用户数据目录
* @param {string} resourcesPath 应用资源目录
* @param {boolean} isDev 是否开发环境
* @returns {Promise<{userSqlConfig: Array, appSqlConfig: Array}>}
*/
async function getSqlConfigs(userDataPath, resourcesPath, isDev, logger = console.log) {
// 用户SQL配置文件路径
const userSqlPath = path.join(userDataPath, 'sql.json');
// 应用SQL配置文件路径
const appSqlPath = isDev
? path.join(__dirname, '..', 'prisma', 'sql.json')
: path.join(resourcesPath, 'prisma', 'sql.json');
let userSqlConfig = [];
let appSqlConfig = [];
// 读取应用SQL配置
try {
if (fs.existsSync(appSqlPath)) {
const appSqlContent = fs.readFileSync(appSqlPath, 'utf8');
appSqlConfig = JSON.parse(appSqlContent);
}
} catch (error) {
throw new Error(`读取应用SQL配置文件失败: ${error.message}`);
}
// 读取用户SQL配置如果存在
try {
if (fs.existsSync(userSqlPath)) {
const userSqlContent = fs.readFileSync(userSqlPath, 'utf8');
userSqlConfig = JSON.parse(userSqlContent);
}
} catch (error) {
// 如果用户SQL配置不存在或无法解析使用空数组
userSqlConfig = [];
}
logger(appSqlPath);
// logger(JSON.stringify(appSqlConfig, null, 2));
logger(userSqlPath);
// logger(JSON.stringify(userSqlConfig, null, 2));
return { userSqlConfig, appSqlConfig };
}
/**
* 更新用户SQL配置文件
* @param {string} userDataPath 用户数据目录
* @param {Array} sqlConfig 新的SQL配置
*/
function updateUserSqlConfig(userDataPath, sqlConfig) {
const userSqlPath = path.join(userDataPath, 'sql.json');
fs.writeFileSync(userSqlPath, JSON.stringify(sqlConfig, null, 4), 'utf8');
}
// 不再需要版本比较功能
/**
* 获取需要执行的SQL命令
* @param {Array} userSqlConfig 用户SQL配置
* @param {Array} appSqlConfig 应用SQL配置
* @returns {Array} 需要执行的SQL命令
*/
function getSqlsToExecute(userSqlConfig, appSqlConfig) {
// 创建用户已执行的SQL集合 (使用 version + sql 的组合作为唯一标识)
const userExecutedSqlSet = new Set();
userSqlConfig.forEach(item => {
const key = `${item.version}:${item.sql}`;
userExecutedSqlSet.add(key);
});
// 过滤出用户需要执行的SQL (即应用SQL配置中存在但用户尚未执行的SQL)
return appSqlConfig.filter(item => {
const key = `${item.version}:${item.sql}`;
return !userExecutedSqlSet.has(key);
});
}
/**
* 更新数据库
* @param {string} userDataPath 用户数据目录
* @param {string} resourcesPath 应用资源目录
* @param {boolean} isDev 是否开发环境
* @param {function} logger 日志函数
*/
async function updateDatabase(userDataPath, resourcesPath, isDev, logger = console.log) {
const dbPath = path.join(userDataPath, 'local-db', 'db.sqlite');
try {
// 获取SQL配置
const { userSqlConfig, appSqlConfig } = await getSqlConfigs(userDataPath, resourcesPath, isDev, logger);
// 获取需要执行的SQL
const sqlsToExecute = getSqlsToExecute(userSqlConfig, appSqlConfig);
if (sqlsToExecute.length === 0) {
logger('数据库已是最新版本,无需更新');
return { updated: false, message: '数据库已是最新版本' };
}
// 设置数据库URL
const dbUrl = `file:${dbPath}`;
// 执行SQL更新
logger(`发现 ${sqlsToExecute.length} 个数据库更新,开始执行...`);
for (const item of sqlsToExecute) {
try {
logger(`执行版本 ${item.version} 的SQL更新: ${item.sql.substring(0, 100)}...`);
await executeSql(dbUrl, item.sql);
// 添加到用户SQL配置
userSqlConfig.push(item);
} catch (error) {
logger(`执行版本 ${item.version} 的SQL更新失败: ${error.message}`);
}
}
// 更新用户SQL配置文件
updateUserSqlConfig(userDataPath, userSqlConfig);
logger('数据库更新完成');
return {
updated: true,
message: `成功执行了 ${sqlsToExecute.length} 个数据库更新`,
executedVersions: sqlsToExecute.map(item => item.version)
};
} catch (error) {
logger(`数据库更新失败: ${error.message}`);
return { updated: false, error: error.message };
}
}
module.exports = {
updateDatabase,
executeSql,
getSqlConfigs,
updateUserSqlConfig,
getSqlsToExecute
};

View File

@@ -0,0 +1,33 @@
const { ipcMain } = require('electron');
const { checkUpdate, downloadUpdate, installUpdate } = require('./updater');
/**
* 设置 IPC 处理程序
* @param {Object} app Electron app 对象
* @param {boolean} isDev 是否为开发环境
*/
function setupIpcHandlers(app, isDev) {
// 获取用户数据路径
ipcMain.on('get-user-data-path', event => {
event.returnValue = app.getPath('userData');
});
// 检查更新
ipcMain.handle('check-update', async () => {
return await checkUpdate(isDev);
});
// 下载更新
ipcMain.handle('download-update', async () => {
return await downloadUpdate();
});
// 安装更新
ipcMain.handle('install-update', () => {
return installUpdate();
});
}
module.exports = {
setupIpcHandlers
};

View File

@@ -0,0 +1,84 @@
const fs = require('fs');
const path = require('path');
/**
* 设置应用日志系统
* @param {Object} app Electron app 对象
* @returns {string} 日志文件路径
*/
function setupLogging(app) {
const logDir = path.join(app.getPath('userData'), 'logs');
if (!fs.existsSync(logDir)) {
fs.mkdirSync(logDir, { recursive: true });
}
const logFilePath = path.join(logDir, `app-${new Date().toISOString().slice(0, 10)}.log`);
// 创建自定义日志函数
global.appLog = (message, level = 'info') => {
const timestamp = new Date().toISOString();
const logEntry = `[${timestamp}] [${level.toUpperCase()}] ${message}\n`;
// 同时输出到控制台和日志文件
console.log(message);
fs.appendFileSync(logFilePath, logEntry);
};
// 捕获全局未处理异常并记录
process.on('uncaughtException', error => {
global.appLog(`未捕获的异常: ${error.stack || error}`, 'error');
});
return logFilePath;
}
/**
* 设置 IPC 日志处理程序
* @param {Object} ipcMain IPC 主进程对象
* @param {Object} app Electron app 对象
* @param {boolean} isDev 是否为开发环境
*/
function setupIpcLogging(ipcMain, app, isDev) {
ipcMain.on('log', (event, { level, message }) => {
const timestamp = new Date().toISOString();
const logEntry = `[${timestamp}] [${level.toUpperCase()}] ${message}\n`;
// 只在客户端环境下写入文件
if (!isDev || true) {
const logsDir = path.join(app.getPath('userData'), 'logs');
if (!fs.existsSync(logsDir)) {
fs.mkdirSync(logsDir, { recursive: true });
}
const logFile = path.join(logsDir, `${new Date().toISOString().split('T')[0]}.log`);
fs.appendFileSync(logFile, logEntry);
}
// 同时输出到控制台
console[level](message);
});
}
/**
* 清理日志文件
* @param {Object} app Electron app 对象
* @returns {Promise<void>}
*/
async function clearLogs(app) {
const logsDir = path.join(app.getPath('userData'), 'logs');
if (fs.existsSync(logsDir)) {
// 读取目录下所有文件
const files = await fs.promises.readdir(logsDir);
// 删除所有文件
for (const file of files) {
const filePath = path.join(logsDir, file);
await fs.promises.unlink(filePath);
global.appLog(`已删除日志文件: ${filePath}`);
}
}
}
module.exports = {
setupLogging,
setupIpcLogging,
clearLogs
};

View File

@@ -0,0 +1,136 @@
const { Menu, dialog, shell, app } = require('electron');
const path = require('path');
const fs = require('fs');
const os = require('os');
const { getAppVersion } = require('../util');
/**
* 创建应用菜单
* @param {BrowserWindow} mainWindow 主窗口
* @param {Function} clearCache 清除缓存函数
*/
function createMenu(mainWindow, clearCache) {
const template = [
{
label: 'File',
submenu: [{ role: 'quit', label: 'Quit' }]
},
{
label: 'Edit',
submenu: [
{ role: 'undo', label: 'Undo' },
{ role: 'redo', label: 'Redo' },
{ type: 'separator' },
{ role: 'cut', label: 'Cut' },
{ role: 'copy', label: 'Copy' },
{ role: 'paste', label: 'Paste' }
]
},
{
label: 'View',
submenu: [
{ role: 'reload', label: 'Refresh' },
{ type: 'separator' },
{ role: 'resetzoom', label: 'Reset Zoom' },
{ role: 'zoomin', label: 'Zoom In' },
{ role: 'zoomout', label: 'Zoom Out' },
{ type: 'separator' },
{ role: 'togglefullscreen', label: 'Fullscreen' }
]
},
{
label: 'Help',
submenu: [
{
label: 'About',
click: () => {
dialog.showMessageBox(mainWindow, {
title: 'About Easy Dataset',
message: `Easy Dataset v${getAppVersion()}`,
detail: 'An application for creating fine-tuning datasets for large models.',
buttons: ['OK']
});
}
},
{
label: 'Visit GitHub',
click: () => {
shell.openExternal('https://github.com/ConardLi/easy-dataset');
}
}
]
},
{
label: 'More',
submenu: [
{ role: 'toggledevtools', label: 'Developer Tools' },
{
label: 'Open Logs Directory',
click: () => {
const logsDir = path.join(app.getPath('userData'), 'logs');
if (!fs.existsSync(logsDir)) {
fs.mkdirSync(logsDir, { recursive: true });
}
shell.openPath(logsDir);
}
},
{
label: 'Open Data Directory',
click: () => {
const dataDir = path.join(app.getPath('userData'), 'local-db');
if (!fs.existsSync(dataDir)) {
fs.mkdirSync(dataDir, { recursive: true });
}
shell.openPath(dataDir);
}
},
{
label: 'Open Data Directory (History)',
click: () => {
const dataDir = path.join(os.homedir(), '.easy-dataset-db');
if (!fs.existsSync(dataDir)) {
fs.mkdirSync(dataDir, { recursive: true });
}
shell.openPath(dataDir);
}
},
{
label: 'Clear Cache',
click: async () => {
try {
const response = await dialog.showMessageBox(mainWindow, {
type: 'question',
buttons: ['Cancel', 'Confirm'],
defaultId: 1,
title: 'Clear Cache',
message: 'Are you sure you want to clear the cache?',
detail:
'This will delete all files in the logs directory and local database cache files (excluding main database files).'
});
if (response.response === 1) {
// User clicked confirm
await clearCache();
dialog.showMessageBox(mainWindow, {
type: 'info',
title: 'Cleared Successfully',
message: 'Cache has been cleared successfully'
});
}
} catch (error) {
global.appLog(`Failed to clear cache: ${error.message}`, 'error');
dialog.showErrorBox('Failed to clear cache', error.message);
}
}
}
]
}
];
const menu = Menu.buildFromTemplate(template);
Menu.setApplicationMenu(menu);
}
module.exports = {
createMenu
};

View File

@@ -0,0 +1,118 @@
const http = require('http');
const path = require('path');
const fs = require('fs');
const { dialog } = require('electron');
/**
* 检查端口是否被占用
* @param {number} port 端口号
* @returns {Promise<boolean>} 端口是否被占用
*/
function checkPort(port) {
return new Promise(resolve => {
const server = http.createServer();
server.once('error', () => {
resolve(true); // 端口被占用
});
server.once('listening', () => {
server.close();
resolve(false); // 端口未被占用
});
server.listen(port);
});
}
/**
* 启动 Next.js 服务
* @param {number} port 端口号
* @param {Object} app Electron app 对象
* @returns {Promise<string>} 服务URL
*/
async function startNextServer(port, app) {
console.log(`Easy Dataset 客户端启动中,当前版本: ${require('../util').getAppVersion()}`);
// 设置日志文件路径
const logDir = path.join(app.getPath('userData'), 'logs');
if (!fs.existsSync(logDir)) {
fs.mkdirSync(logDir, { recursive: true });
}
const logFile = path.join(logDir, `nextjs-${new Date().toISOString().replace(/:/g, '-')}.log`);
const logStream = fs.createWriteStream(logFile, { flags: 'a' });
// 重定向 console.log 和 console.error
const originalConsoleLog = console.log;
const originalConsoleError = console.error;
console.log = function () {
const args = Array.from(arguments);
const logMessage = args.map(arg => (typeof arg === 'object' ? JSON.stringify(arg, null, 2) : arg)).join(' ');
logStream.write(`[${new Date().toISOString()}] [LOG] ${logMessage}\n`);
originalConsoleLog.apply(console, args);
};
console.error = function () {
const args = Array.from(arguments);
const logMessage = args.map(arg => (typeof arg === 'object' ? JSON.stringify(arg, null, 2) : arg)).join(' ');
logStream.write(`[${new Date().toISOString()}] [ERROR] ${logMessage}\n`);
originalConsoleError.apply(console, args);
};
// 检查端口是否被占用
const isPortBusy = await checkPort(port);
if (isPortBusy) {
console.log(`端口 ${port} 已被占用,尝试直接连接...`);
return `http://localhost:${port}`;
}
console.log(`启动 Next.js 服务,端口: ${port}`);
try {
// 动态导入 Next.js
const next = require('next');
const nextApp = next({
dev: false,
dir: path.join(__dirname, '../..'),
conf: {
// 配置 Next.js 的日志输出
onInfo: info => {
console.log(`[Next.js Info] ${info}`);
},
onError: error => {
console.error(`[Next.js Error] ${error}`);
},
onWarn: warn => {
console.log(`[Next.js Warning] ${warn}`);
}
}
});
const handle = nextApp.getRequestHandler();
await nextApp.prepare();
const server = http.createServer((req, res) => {
// 记录请求日志
console.log(`[Request] ${req.method} ${req.url}`);
handle(req, res);
});
return new Promise(resolve => {
server.listen(port, err => {
if (err) throw err;
console.log(`服务已启动,正在打开应用...`);
resolve(`http://localhost:${port}`);
});
});
} catch (error) {
console.error('启动服务失败:', error);
dialog.showErrorBox('启动失败', `无法启动 Next.js 服务: ${error.message}`);
app.quit();
return '';
}
}
module.exports = {
checkPort,
startNextServer
};

View File

@@ -0,0 +1,116 @@
const { autoUpdater } = require('electron-updater');
const { getAppVersion } = require('../util');
/**
* 设置自动更新
* @param {BrowserWindow} mainWindow 主窗口
*/
function setupAutoUpdater(mainWindow) {
autoUpdater.autoDownload = false;
autoUpdater.allowDowngrade = false;
// 检查更新时出错
autoUpdater.on('error', error => {
if (mainWindow) {
mainWindow.webContents.send('update-error', error.message);
}
});
// 检查到更新时
autoUpdater.on('update-available', info => {
if (mainWindow) {
mainWindow.webContents.send('update-available', {
version: info.version,
releaseDate: info.releaseDate,
releaseNotes: info.releaseNotes
});
}
});
// 没有可用更新
autoUpdater.on('update-not-available', () => {
if (mainWindow) {
mainWindow.webContents.send('update-not-available');
}
});
// 下载进度
autoUpdater.on('download-progress', progressObj => {
if (mainWindow) {
mainWindow.webContents.send('download-progress', progressObj);
}
});
// 下载完成
autoUpdater.on('update-downloaded', info => {
if (mainWindow) {
mainWindow.webContents.send('update-downloaded', {
version: info.version,
releaseDate: info.releaseDate,
releaseNotes: info.releaseNotes
});
}
});
}
/**
* 检查更新
* @param {boolean} isDev 是否为开发环境
* @returns {Promise<Object>} 更新信息
*/
async function checkUpdate(isDev) {
try {
if (isDev) {
// 开发环境下模拟更新检查
return {
hasUpdate: false,
currentVersion: getAppVersion(),
message: '开发环境下不检查更新'
};
}
// 返回当前版本信息,并开始检查更新
const result = await autoUpdater.checkForUpdates();
return {
checking: true,
currentVersion: getAppVersion()
};
} catch (error) {
console.error('检查更新失败:', error);
return {
hasUpdate: false,
currentVersion: getAppVersion(),
error: error.message
};
}
}
/**
* 下载更新
* @returns {Promise<Object>} 下载状态
*/
async function downloadUpdate() {
try {
autoUpdater.downloadUpdate();
return { downloading: true };
} catch (error) {
console.error('下载更新失败:', error);
return { error: error.message };
}
}
/**
* 安装更新
* @returns {Object} 安装状态
*/
function installUpdate() {
autoUpdater.quitAndInstall(false, true);
return { installing: true };
}
module.exports = {
setupAutoUpdater,
checkUpdate,
downloadUpdate,
installUpdate
};

View File

@@ -0,0 +1,113 @@
const { BrowserWindow, shell } = require('electron');
const path = require('path');
const url = require('url');
const { getAppVersion } = require('../util');
let mainWindow;
/**
* 创建主窗口
* @param {boolean} isDev 是否为开发环境
* @param {number} port 服务端口
* @returns {BrowserWindow} 创建的主窗口
*/
function createWindow(isDev, port) {
mainWindow = new BrowserWindow({
width: 1200,
height: 800,
show: false,
frame: true,
webPreferences: {
nodeIntegration: false,
contextIsolation: true,
preload: path.join(__dirname, '..', 'preload.js')
},
icon: path.join(__dirname, '../../public/imgs/logo.ico')
});
// 设置窗口标题
mainWindow.setTitle(`Easy Dataset v${getAppVersion()}`);
const loadingPath = url.format({
pathname: path.join(__dirname, '..', 'loading.html'),
protocol: 'file:',
slashes: true
});
// 加载 loading 页面时使用专门的 preload 脚本
mainWindow.webContents.on('did-finish-load', () => {
mainWindow.show();
});
mainWindow.loadURL(loadingPath);
// 处理窗口导航事件,将外部链接在浏览器中打开
mainWindow.webContents.on('will-navigate', (event, navigationUrl) => {
// 解析当前 URL 和导航 URL
const parsedUrl = new URL(navigationUrl);
const currentHostname = isDev ? 'localhost' : 'localhost';
const currentPort = port.toString();
// 检查是否是外部链接
if (parsedUrl.hostname !== currentHostname || (parsedUrl.port !== currentPort && parsedUrl.port !== '')) {
event.preventDefault();
shell.openExternal(navigationUrl);
}
});
// 处理新窗口打开请求,将外部链接在浏览器中打开
mainWindow.webContents.setWindowOpenHandler(({ url: navigationUrl }) => {
// 解析导航 URL
const parsedUrl = new URL(navigationUrl);
const currentHostname = isDev ? 'localhost' : 'localhost';
const currentPort = port.toString();
// 检查是否是外部链接
if (parsedUrl.hostname !== currentHostname || (parsedUrl.port !== currentPort && parsedUrl.port !== '')) {
shell.openExternal(navigationUrl);
return { action: 'deny' };
}
return { action: 'allow' };
});
mainWindow.on('closed', () => {
mainWindow = null;
});
mainWindow.maximize();
return mainWindow;
}
/**
* 加载应用URL
* @param {string} appUrl 应用URL
*/
function loadAppUrl(appUrl) {
if (mainWindow) {
mainWindow.loadURL(appUrl);
}
}
/**
* 在开发环境中打开开发者工具
*/
function openDevTools() {
if (mainWindow) {
mainWindow.webContents.openDevTools();
}
}
/**
* 获取主窗口
* @returns {BrowserWindow} 主窗口
*/
function getMainWindow() {
return mainWindow;
}
module.exports = {
createWindow,
loadAppUrl,
openDevTools,
getMainWindow
};

View File

@@ -0,0 +1,73 @@
const { contextBridge, ipcRenderer } = require('electron');
// 在渲染进程中暴露安全的 API
contextBridge.exposeInMainWorld('electron', {
// 获取应用版本
getAppVersion: () => ipcRenderer.invoke('get-app-version'),
// 获取当前语言
getLanguage: () => {
// 尝试从本地存储获取语言设置
const storedLang = localStorage.getItem('i18nextLng');
// 如果存在则返回,否则返回系统语言或默认为中文
return storedLang || navigator.language.startsWith('zh') ? 'zh' : 'en';
},
// 获取用户数据目录
getUserDataPath: () => {
try {
return ipcRenderer.sendSync('get-user-data-path');
} catch (error) {
console.error('获取用户数据目录失败:', error);
return null;
}
},
// 更新相关 API
updater: {
// 检查更新
checkForUpdates: () => ipcRenderer.invoke('check-update'),
// 下载更新
downloadUpdate: () => ipcRenderer.invoke('download-update'),
// 安装更新
installUpdate: () => ipcRenderer.invoke('install-update'),
// 监听更新事件
onUpdateAvailable: callback => {
const handler = (_, info) => callback(info);
ipcRenderer.on('update-available', handler);
return () => ipcRenderer.removeListener('update-available', handler);
},
onUpdateNotAvailable: callback => {
const handler = () => callback();
ipcRenderer.on('update-not-available', handler);
return () => ipcRenderer.removeListener('update-not-available', handler);
},
onUpdateError: callback => {
const handler = (_, error) => callback(error);
ipcRenderer.on('update-error', handler);
return () => ipcRenderer.removeListener('update-error', handler);
},
onDownloadProgress: callback => {
const handler = (_, progress) => callback(progress);
ipcRenderer.on('download-progress', handler);
return () => ipcRenderer.removeListener('download-progress', handler);
},
onUpdateDownloaded: callback => {
const handler = (_, info) => callback(info);
ipcRenderer.on('update-downloaded', handler);
return () => ipcRenderer.removeListener('update-downloaded', handler);
}
}
});
// 通知渲染进程 preload 脚本已加载完成
window.addEventListener('DOMContentLoaded', () => {
console.log('Electron preload script loaded');
});

View File

@@ -0,0 +1,19 @@
const path = require('path');
const fs = require('fs');
// 获取应用版本
const getAppVersion = () => {
try {
const packageJsonPath = path.join(__dirname, '../package.json');
if (fs.existsSync(packageJsonPath)) {
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
return packageJson.version;
}
return '1.0.0';
} catch (error) {
console.error('读取版本信息失败:', error);
return '1.0.0';
}
};
module.exports = { getAppVersion };