Add crash monitor, OOBE flow & factory-reset

Introduce crash monitoring and an OOBE onboarding flow. Adds a crash logger (koring-crash.log), crash-monitor handler/window, preload-crash bridge, crash UI (crash.html, src/crash.tsx, pages/crash) and a debug page. Main process now sets up crash listeners, startup checks (first-launch, .minecraft), and a config reset/factory-reset that removes Koring.yml, koring-auth.json and background cache and can relaunch the app. Exposes config preloading to renderer, adds OOBE pages/layout, confirm dialog store/UI, trims Silk renderer settings, bumps version to 1.0.1 and adds motion dependency, and registers crash.html in Vite. Various related type and config updates included.
This commit is contained in:
2026-07-12 01:24:31 +08:00
parent 2b19144352
commit 0a2b8bd018
50 changed files with 1867 additions and 296 deletions
+49 -1
View File
@@ -1,11 +1,45 @@
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.registerConfigHandlers = registerConfigHandlers;
const electron_1 = __importDefault(require("electron"));
const { ipcMain } = electron_1.default;
const { ipcMain, app } = electron_1.default;
const fs = __importStar(require("fs"));
const config_1 = require("../config");
function registerConfigHandlers() {
ipcMain.handle('config:get', () => {
@@ -26,4 +60,18 @@ function registerConfigHandlers() {
return { success: false, data: null, error: String(e) };
}
});
ipcMain.handle('config:reset', () => {
try {
const filePath = (0, config_1.configPath)();
if (fs.existsSync(filePath)) {
fs.unlinkSync(filePath);
}
app.relaunch();
app.exit(0);
return { success: true, data: null, error: null };
}
catch (e) {
return { success: false, data: null, error: String(e) };
}
});
}
+18 -2
View File
@@ -1,6 +1,8 @@
import electron from 'electron';
const { ipcMain } = electron;
import { loadConfig, saveConfig, type AppConfig } from '../config';
const { ipcMain, app } = electron;
import * as fs from 'fs';
import * as path from 'path';
import { loadConfig, saveConfig, type AppConfig, configPath } from '../config';
export function registerConfigHandlers() {
ipcMain.handle('config:get', () => {
@@ -20,4 +22,18 @@ export function registerConfigHandlers() {
return { success: false, data: null, error: String(e) };
}
});
ipcMain.handle('config:reset', () => {
try {
const filePath = configPath();
if (fs.existsSync(filePath)) {
fs.unlinkSync(filePath);
}
app.relaunch();
app.exit(0);
return { success: true, data: null, error: null };
} catch (e: unknown) {
return { success: false, data: null, error: String(e) };
}
});
}
+206
View File
@@ -0,0 +1,206 @@
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.setupCrashListeners = setupCrashListeners;
exports.registerCrashHandlers = registerCrashHandlers;
exports.testCrashDialog = testCrashDialog;
const electron_1 = __importDefault(require("electron"));
const path_1 = __importDefault(require("path"));
const fs_1 = __importDefault(require("fs"));
const crash_logger_1 = require("../core/crash-logger");
const { app, ipcMain, BrowserWindow } = electron_1.default;
const isDev = !app.isPackaged;
let crashWin = null;
function getIconPath() {
return path_1.default.join(__dirname, '../../build/icon.ico');
}
function createCrashWindow() {
const iconPath = getIconPath();
const win = new BrowserWindow({
width: 600,
height: 460,
minWidth: 500,
maxWidth: 700,
minHeight: 460,
maxHeight: 460,
frame: false,
transparent: false,
resizable: false,
show: false,
icon: iconPath,
webPreferences: {
preload: path_1.default.join(__dirname, '../preload-crash.js'),
nodeIntegration: false,
contextIsolation: true,
sandbox: false,
},
});
if (isDev) {
win.loadURL('http://localhost:1420/crash.html');
}
else {
win.loadFile(path_1.default.join(__dirname, '../../dist/crash.html'));
}
return win;
}
function sendToCrashWindow(data) {
if (!crashWin || crashWin.isDestroyed()) {
crashWin = createCrashWindow();
}
const send = () => {
crashWin?.show();
crashWin?.focus();
crashWin?.webContents.send('crash:show', {
type: data.type,
message: data.message,
timestamp: data.timestamp,
});
};
if (crashWin.webContents.isLoading()) {
crashWin.webContents.once('did-finish-load', send);
}
else {
send();
}
}
function setupCrashListeners(mainWindow) {
mainWindow.webContents.on('render-process-gone', (_event, details) => {
const entry = {
timestamp: new Date().toISOString(),
type: 'renderer-gone',
message: `渲染进程崩溃: ${details.reason} (退出码: ${details.exitCode})`,
details: details,
};
(0, crash_logger_1.writeCrashLog)(entry);
sendToCrashWindow(entry);
});
mainWindow.on('unresponsive', () => {
const entry = {
timestamp: new Date().toISOString(),
type: 'unresponsive',
message: '渲染进程无响应',
};
(0, crash_logger_1.writeCrashLog)(entry);
sendToCrashWindow(entry);
});
// Inject devtools crash tools when devtools opens
if (isDev) {
mainWindow.webContents.on('devtools-opened', () => {
const js = `
(function() {
if (window.__crashToolsLoaded) return;
window.__crashToolsLoaded = true;
console.log('%c[崩溃工具] 已加载', 'color: #f59e0b; font-weight: bold; font-size: 14px;');
console.log('%c可用命令:', 'color: #3b82f6; font-weight: bold;');
console.log('%c crash.simulate() %c— 模拟渲染进程崩溃', 'color: #ef4444; font-weight: bold;', 'color: inherit;');
console.log('%c crash.testDialog() %c— 测试崩溃弹窗', 'color: #ef4444; font-weight: bold;', 'color: inherit;');
console.log('%c crash.readLog() %c— 读取崩溃日志', 'color: #ef4444; font-weight: bold;', 'color: inherit;');
console.log('%c crash.factoryReset()%c— 强还原配置', 'color: #ef4444; font-weight: bold;', 'color: inherit;');
console.log('%c crash.restart() %c— 重启应用', 'color: #ef4444; font-weight: bold;', 'color: inherit;');
console.log('');
window.crash = {
simulate: function() { window.electronAPI?.simulateCrash(); },
testDialog: function() { window.electronAPI?.testCrashDialog(); },
readLog: function() { return window.electronAPI?.invoke('crash:readLog'); },
factoryReset: function() { return window.electronAPI?.invoke('crash:factoryReset'); },
restart: function() { return window.electronAPI?.invoke('crash:restart'); },
};
})();
`;
mainWindow.webContents.executeJavaScript(js);
});
}
}
function registerCrashHandlers() {
// Close crash window
ipcMain.handle('crash:closeWindow', () => {
if (crashWin && !crashWin.isDestroyed()) {
crashWin.close();
crashWin = null;
}
});
// Read crash log
ipcMain.handle('crash:readLog', () => {
return (0, crash_logger_1.readCrashLog)();
});
// Simulate crash — forcefully crash the renderer (devtools only)
ipcMain.handle('crash:simulate', (_event) => {
const senderWebContents = electron_1.default.webContents.getAllWebContents().find((wc) => wc.id === _event.sender.id);
if (senderWebContents) {
senderWebContents.forcefullyCrashRenderer();
}
});
// Test crash dialog — show crash window without actually crashing
ipcMain.handle('crash:testDialog', () => {
testCrashDialog();
});
// Factory reset
ipcMain.handle('crash:factoryReset', () => {
const dataPath = app.isPackaged
? path_1.default.dirname(app.getPath('exe'))
: path_1.default.join(__dirname, '../..');
// Delete config
try {
const configPath = path_1.default.join(dataPath, 'Koring.yml');
if (fs_1.default.existsSync(configPath))
fs_1.default.unlinkSync(configPath);
}
catch { }
// Delete auth
try {
const authPath = path_1.default.join(dataPath, 'koring-auth.json');
if (fs_1.default.existsSync(authPath))
fs_1.default.unlinkSync(authPath);
}
catch { }
// Delete background cache in userData
try {
const bgPath = path_1.default.join(app.getPath('userData'), 'background.png');
if (fs_1.default.existsSync(bgPath))
fs_1.default.unlinkSync(bgPath);
}
catch { }
// Delete crash log
(0, crash_logger_1.clearCrashLog)();
return { success: true };
});
// Restart app
ipcMain.handle('crash:restart', () => {
app.relaunch();
app.exit(0);
});
// Main process error handlers
process.on('uncaughtException', (error) => {
const entry = {
timestamp: new Date().toISOString(),
type: 'uncaught-exception',
message: error.message,
stack: error.stack,
};
(0, crash_logger_1.writeCrashLog)(entry);
sendToCrashWindow(entry);
});
process.on('unhandledRejection', (reason) => {
const entry = {
timestamp: new Date().toISOString(),
type: 'unhandled-rejection',
message: String(reason),
stack: reason instanceof Error ? reason.stack : undefined,
};
(0, crash_logger_1.writeCrashLog)(entry);
sendToCrashWindow(entry);
});
}
// Called from devtools to test crash dialog
function testCrashDialog() {
const entry = {
timestamp: new Date().toISOString(),
type: 'test',
message: '这是一个测试崩溃弹窗',
};
sendToCrashWindow(entry);
}
+220
View File
@@ -0,0 +1,220 @@
import electron from 'electron';
import path from 'path';
import fs from 'fs';
import { writeCrashLog, readCrashLog, clearCrashLog, type CrashEntry } from '../core/crash-logger';
const { app, ipcMain, BrowserWindow } = electron;
const isDev = !app.isPackaged;
let crashWin: electron.BrowserWindow | null = null;
function getIconPath(): string {
return path.join(__dirname, '../../build/icon.ico');
}
function createCrashWindow(): electron.BrowserWindow {
const iconPath = getIconPath();
const win = new BrowserWindow({
width: 600,
height: 460,
minWidth: 500,
maxWidth: 700,
minHeight: 460,
maxHeight: 460,
frame: false,
transparent: false,
resizable: false,
show: false,
icon: iconPath,
webPreferences: {
preload: path.join(__dirname, '../preload-crash.js'),
nodeIntegration: false,
contextIsolation: true,
sandbox: false,
},
});
if (isDev) {
win.loadURL('http://localhost:1420/crash.html');
} else {
win.loadFile(path.join(__dirname, '../../dist/crash.html'));
}
return win;
}
function sendToCrashWindow(data: CrashEntry) {
if (!crashWin || crashWin.isDestroyed()) {
crashWin = createCrashWindow();
}
const send = () => {
crashWin?.show();
crashWin?.focus();
crashWin?.webContents.send('crash:show', {
type: data.type,
message: data.message,
timestamp: data.timestamp,
});
};
if (crashWin.webContents.isLoading()) {
crashWin.webContents.once('did-finish-load', send);
} else {
send();
}
}
export function setupCrashListeners(mainWindow: electron.BrowserWindow) {
mainWindow.webContents.on('render-process-gone', (_event: any, details: any) => {
const entry: CrashEntry = {
timestamp: new Date().toISOString(),
type: 'renderer-gone',
message: `渲染进程崩溃: ${details.reason} (退出码: ${details.exitCode})`,
details: details as unknown as Record<string, unknown>,
};
writeCrashLog(entry);
sendToCrashWindow(entry);
});
mainWindow.on('unresponsive', () => {
const entry: CrashEntry = {
timestamp: new Date().toISOString(),
type: 'unresponsive',
message: '渲染进程无响应',
};
writeCrashLog(entry);
sendToCrashWindow(entry);
});
// Inject devtools crash tools when devtools opens
if (isDev) {
mainWindow.webContents.on('devtools-opened', () => {
const js = `
(function() {
if (window.__crashToolsLoaded) return;
window.__crashToolsLoaded = true;
console.log('%c[崩溃工具] 已加载', 'color: #f59e0b; font-weight: bold; font-size: 14px;');
console.log('%c可用命令:', 'color: #3b82f6; font-weight: bold;');
console.log('%c crash.simulate() %c— 模拟渲染进程崩溃', 'color: #ef4444; font-weight: bold;', 'color: inherit;');
console.log('%c crash.testDialog() %c— 测试崩溃弹窗', 'color: #ef4444; font-weight: bold;', 'color: inherit;');
console.log('%c crash.readLog() %c— 读取崩溃日志', 'color: #ef4444; font-weight: bold;', 'color: inherit;');
console.log('%c crash.factoryReset()%c— 强还原配置', 'color: #ef4444; font-weight: bold;', 'color: inherit;');
console.log('%c crash.restart() %c— 重启应用', 'color: #ef4444; font-weight: bold;', 'color: inherit;');
console.log('');
window.crash = {
simulate: function() { window.electronAPI?.simulateCrash(); },
testDialog: function() { window.electronAPI?.testCrashDialog(); },
readLog: function() { return window.electronAPI?.invoke('crash:readLog'); },
factoryReset: function() { return window.electronAPI?.invoke('crash:factoryReset'); },
restart: function() { return window.electronAPI?.invoke('crash:restart'); },
};
})();
`;
mainWindow.webContents.executeJavaScript(js);
});
}
}
export function registerCrashHandlers() {
// Close crash window
ipcMain.handle('crash:closeWindow', () => {
if (crashWin && !crashWin.isDestroyed()) {
crashWin.close();
crashWin = null;
}
});
// Read crash log
ipcMain.handle('crash:readLog', () => {
return readCrashLog();
});
// Simulate crash — forcefully crash the renderer (devtools only)
ipcMain.handle('crash:simulate', (_event) => {
const senderWebContents = electron.webContents.getAllWebContents().find(
(wc) => wc.id === _event.sender.id
);
if (senderWebContents) {
senderWebContents.forcefullyCrashRenderer();
}
});
// Test crash dialog — show crash window without actually crashing
ipcMain.handle('crash:testDialog', () => {
testCrashDialog();
});
// Factory reset
ipcMain.handle('crash:factoryReset', () => {
const dataPath = app.isPackaged
? path.dirname(app.getPath('exe'))
: path.join(__dirname, '../..');
// Delete config
try {
const configPath = path.join(dataPath, 'Koring.yml');
if (fs.existsSync(configPath)) fs.unlinkSync(configPath);
} catch {}
// Delete auth
try {
const authPath = path.join(dataPath, 'koring-auth.json');
if (fs.existsSync(authPath)) fs.unlinkSync(authPath);
} catch {}
// Delete background cache in userData
try {
const bgPath = path.join(app.getPath('userData'), 'background.png');
if (fs.existsSync(bgPath)) fs.unlinkSync(bgPath);
} catch {}
// Delete crash log
clearCrashLog();
return { success: true };
});
// Restart app
ipcMain.handle('crash:restart', () => {
app.relaunch();
app.exit(0);
});
// Main process error handlers
process.on('uncaughtException', (error) => {
const entry: CrashEntry = {
timestamp: new Date().toISOString(),
type: 'uncaught-exception',
message: error.message,
stack: error.stack,
};
writeCrashLog(entry);
sendToCrashWindow(entry);
});
process.on('unhandledRejection', (reason) => {
const entry: CrashEntry = {
timestamp: new Date().toISOString(),
type: 'unhandled-rejection',
message: String(reason),
stack: reason instanceof Error ? reason.stack : undefined,
};
writeCrashLog(entry);
sendToCrashWindow(entry);
});
}
// Called from devtools to test crash dialog
export function testCrashDialog() {
const entry: CrashEntry = {
timestamp: new Date().toISOString(),
type: 'test',
message: '这是一个测试崩溃弹窗',
};
sendToCrashWindow(entry);
}