Compare commits
23 Commits
Author | SHA1 | Date |
---|---|---|
|
f5c6bf2432 | 2 weeks ago |
|
f4505621d5 | 2 weeks ago |
|
6692e16720 | 3 weeks ago |
|
de5f511d6a | 3 weeks ago |
|
d4be8b22b9 | 4 weeks ago |
|
c142937af9 | 4 weeks ago |
|
950bfe9060 | 4 weeks ago |
|
7035429775 | 4 weeks ago |
|
05f83e2a08 | 4 weeks ago |
|
fa6ef80493 | 4 weeks ago |
|
0f093b2ef9 | 4 weeks ago |
|
80cc4fe0fe | 4 weeks ago |
|
28eea56a3d | 4 weeks ago |
|
b6964f5fbe | 4 weeks ago |
|
7246ab2d9a | 4 weeks ago |
|
bd9ac214c6 | 4 weeks ago |
|
dcdc4aa857 | 1 month ago |
|
2d5a57853d | 1 month ago |
|
3c434df31c | 1 month ago |
|
b4b975174d | 2 months ago |
|
91f06eb4a1 | 2 months ago |
|
248716be69 | 2 months ago |
|
ca363ceac9 | 2 months ago |
1502 changed files with 11386 additions and 3607 deletions
@ -0,0 +1 @@ |
|||
src/renderer/public/icons/*.svg filter=lfs diff=lfs merge=lfs -text |
@ -1,3 +1,3 @@ |
|||
{ |
|||
"recommendations": ["dbaeumer.vscode-eslint"] |
|||
"recommendations": ["dbaeumer.vscode-eslint", "lokalise.i18n-ally"] |
|||
} |
|||
|
@ -0,0 +1,41 @@ |
|||
if (import.meta.env.DEV) { |
|||
// 引入之后可以热更新
|
|||
import("./languages/zh.json") |
|||
import("./languages/en.json") |
|||
} |
|||
|
|||
const datetimeFormats = { |
|||
en: { |
|||
short: { |
|||
year: "numeric", |
|||
month: "short", |
|||
day: "numeric", |
|||
}, |
|||
long: { |
|||
year: "numeric", |
|||
month: "short", |
|||
day: "numeric", |
|||
weekday: "short", |
|||
hour: "numeric", |
|||
minute: "numeric", |
|||
}, |
|||
}, |
|||
zh: { |
|||
short: { |
|||
year: "numeric", |
|||
month: "short", |
|||
day: "numeric", |
|||
}, |
|||
long: { |
|||
year: "numeric", |
|||
month: "short", |
|||
day: "numeric", |
|||
weekday: "short", |
|||
hour: "numeric", |
|||
minute: "numeric", |
|||
hour12: true, |
|||
}, |
|||
}, |
|||
} |
|||
|
|||
export { datetimeFormats } |
@ -0,0 +1,20 @@ |
|||
{ |
|||
"update": { |
|||
"ready": { |
|||
"hot": { |
|||
"desc": "The new version v{version} is ready for update and will be automatically updated the next time you launch the program.", |
|||
"title": "Prompt" |
|||
} |
|||
} |
|||
}, |
|||
"browser": { |
|||
"navbar": { |
|||
"menu": { |
|||
"fullscreen": "Full screen", |
|||
"quit-fullscreen": "Exit Full Screen", |
|||
"toggleDevTools": "Developer panel", |
|||
"label": "Menu" |
|||
} |
|||
} |
|||
} |
|||
} |
@ -0,0 +1,20 @@ |
|||
{ |
|||
"update": { |
|||
"ready": { |
|||
"hot": { |
|||
"title": "提示", |
|||
"desc": "新版本 v{version} 已经准备好更新, 下次启动程序即可自动更新" |
|||
} |
|||
} |
|||
}, |
|||
"browser": { |
|||
"navbar": { |
|||
"menu": { |
|||
"label": "菜单", |
|||
"fullscreen": "全屏", |
|||
"quit-fullscreen": "取消全屏", |
|||
"toggleDevTools": "开发者面板" |
|||
} |
|||
} |
|||
} |
|||
} |
@ -0,0 +1,53 @@ |
|||
import { app } from "electron" |
|||
import { get } from "lodash-es" |
|||
|
|||
import zh from "./languages/zh.json" |
|||
import en from "./languages/en.json" |
|||
|
|||
type FlattenObject<T, Prefix extends string = ""> = T extends object |
|||
? { |
|||
[K in keyof T & (string | number)]: FlattenObject<T[K], Prefix extends "" ? `${K}` : `${Prefix}.${K}`> |
|||
}[keyof T & (string | number)] |
|||
: Prefix |
|||
|
|||
type FlattenKeys<T> = FlattenObject<T> |
|||
|
|||
type TranslationKey = FlattenKeys<typeof zh> |
|||
|
|||
class Locale { |
|||
locale: string = "zh" |
|||
|
|||
constructor() { |
|||
try { |
|||
this.locale = app.getLocale() |
|||
} catch (e) { |
|||
console.log(e) |
|||
} |
|||
} |
|||
|
|||
isCN(): boolean { |
|||
return this.locale.startsWith("zh") |
|||
} |
|||
|
|||
t(key: TranslationKey, replacements?: Record<string, string>): string { |
|||
let text: string = this.isCN() ? get(zh, key) : get(en, key) |
|||
if (!text) { |
|||
text = get(zh, key) |
|||
if (!text) { |
|||
return key |
|||
} |
|||
} |
|||
if (replacements) { |
|||
// 替换所有形如 {key} 的占位符
|
|||
Object.entries(replacements).forEach(([key, value]) => { |
|||
console.log(text) |
|||
text = text.replace(new RegExp(`{${key}}`, "g"), value) |
|||
}) |
|||
} |
|||
return text |
|||
} |
|||
} |
|||
|
|||
const Locales = new Locale() |
|||
export default Locales |
|||
export { Locales } |
@ -0,0 +1,7 @@ |
|||
{ |
|||
"name": "locales", |
|||
"version": "1.0.0", |
|||
"keywords": [], |
|||
"author": "", |
|||
"license": "ISC" |
|||
} |
@ -0,0 +1,31 @@ |
|||
// 日志级别定义
|
|||
export enum LogLevel { |
|||
TRACE = 0, |
|||
DEBUG = 1, |
|||
INFO = 2, |
|||
WARN = 3, |
|||
ERROR = 4, |
|||
FATAL = 5, |
|||
OFF = 6, |
|||
} |
|||
// 日志级别名称映射
|
|||
export const LogLevelName: Record<LogLevel, string> = { |
|||
[LogLevel.TRACE]: "TRACE", |
|||
[LogLevel.DEBUG]: "DEBUG", |
|||
[LogLevel.INFO]: "INFO", |
|||
[LogLevel.WARN]: "WARN", |
|||
[LogLevel.ERROR]: "ERROR", |
|||
[LogLevel.FATAL]: "FATAL", |
|||
[LogLevel.OFF]: "OFF", |
|||
} |
|||
|
|||
// 日志颜色映射(控制台输出用)
|
|||
export const LogLevelColor: Record<LogLevel, string> = { |
|||
[LogLevel.TRACE]: "\x1b[90m", // 灰色
|
|||
[LogLevel.DEBUG]: "\x1b[36m", // 青色
|
|||
[LogLevel.INFO]: "\x1b[32m", // 绿色
|
|||
[LogLevel.WARN]: "\x1b[33m", // 黄色
|
|||
[LogLevel.ERROR]: "\x1b[31m", // 红色
|
|||
[LogLevel.FATAL]: "\x1b[35m", // 紫色
|
|||
[LogLevel.OFF]: "", // 无色
|
|||
} |
@ -0,0 +1,486 @@ |
|||
import { app, dialog } from "electron" |
|||
import fs from "fs" |
|||
import path from "path" |
|||
import os from "os" |
|||
import logger from "./main" |
|||
import errorHandler, { ErrorDetail } from "./main-error" |
|||
|
|||
/** |
|||
* 崩溃报告接口 |
|||
*/ |
|||
export interface CrashReport { |
|||
timestamp: string |
|||
error: ErrorDetail |
|||
systemInfo: { |
|||
platform: string |
|||
release: string |
|||
arch: string |
|||
totalMemory: number |
|||
freeMemory: number |
|||
uptime: number |
|||
} |
|||
appInfo: { |
|||
version: string |
|||
name: string |
|||
path: string |
|||
argv: string[] |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 崩溃处理配置 |
|||
*/ |
|||
export interface CrashHandlerOptions { |
|||
crashReportDir?: string |
|||
maxReports?: number |
|||
showDialog?: boolean |
|||
} |
|||
|
|||
/** |
|||
* 默认崩溃处理配置 |
|||
*/ |
|||
const DEFAULT_OPTIONS: CrashHandlerOptions = { |
|||
maxReports: 10, |
|||
showDialog: true, |
|||
} |
|||
|
|||
/** |
|||
* 崩溃处理类 |
|||
*/ |
|||
export class CrashHandler { |
|||
private static instance: CrashHandler |
|||
private options: CrashHandlerOptions |
|||
private crashReportDir: string |
|||
private initialized: boolean = false |
|||
private startTime: number = Date.now() |
|||
// private normalShutdown: boolean = false
|
|||
|
|||
/** |
|||
* 获取单例实例 |
|||
*/ |
|||
public static getInstance(): CrashHandler { |
|||
if (!CrashHandler.instance) { |
|||
CrashHandler.instance = new CrashHandler() |
|||
} |
|||
return CrashHandler.instance |
|||
} |
|||
|
|||
/** |
|||
* 构造函数 |
|||
*/ |
|||
private constructor() { |
|||
this.options = { ...DEFAULT_OPTIONS } |
|||
this.crashReportDir = path.join(app.getPath("userData"), "crash-reports") |
|||
} |
|||
|
|||
/** |
|||
* 初始化崩溃处理器 |
|||
*/ |
|||
public init(options?: CrashHandlerOptions): void { |
|||
if (this.initialized) { |
|||
return |
|||
} |
|||
|
|||
this.options = { ...this.options, ...options } |
|||
|
|||
if (options?.crashReportDir) { |
|||
this.crashReportDir = options.crashReportDir |
|||
} |
|||
|
|||
// 确保崩溃报告目录存在
|
|||
if (!fs.existsSync(this.crashReportDir)) { |
|||
fs.mkdirSync(this.crashReportDir, { recursive: true }) |
|||
} |
|||
|
|||
// 检查上次是否崩溃
|
|||
this.checkPreviousCrash() |
|||
|
|||
// 记录应用启动时间
|
|||
this.startTime = Date.now() |
|||
this.saveStartupMarker() |
|||
|
|||
// 设置全局未捕获异常处理器
|
|||
this.setupGlobalHandlers() |
|||
|
|||
// 设置应用退出处理
|
|||
app.on("before-quit", () => { |
|||
// this.normalShutdown = true
|
|||
this.clearStartupMarker() |
|||
}) |
|||
|
|||
this.initialized = true |
|||
logger.info("crash-handler", "Crash handler initialized") |
|||
} |
|||
|
|||
/** |
|||
* 设置全局未捕获异常处理器 |
|||
*/ |
|||
private setupGlobalHandlers(): void { |
|||
// 增强现有的错误处理器
|
|||
const originalCaptureError = errorHandler.captureError.bind(errorHandler) |
|||
errorHandler.captureError = (error: any, componentInfo?: string, additionalInfo?: Record<string, any>) => { |
|||
// 调用原始方法记录错误
|
|||
originalCaptureError(error, componentInfo, additionalInfo) |
|||
|
|||
// 对于严重错误,生成崩溃报告
|
|||
if (error instanceof Error && error.stack) { |
|||
this.generateCrashReport(error, componentInfo, additionalInfo) |
|||
} |
|||
} |
|||
|
|||
// 捕获未处理的Promise异常
|
|||
process.on("unhandledRejection", (reason, promise) => { |
|||
logger.error("crash-handler", `Unhandled Promise Rejection: ${reason}`) |
|||
this.generateCrashReport(reason, "unhandledRejection", { promise: String(promise) }) |
|||
}) |
|||
|
|||
// 捕获未捕获的异常
|
|||
process.on("uncaughtException", error => { |
|||
logger.error("crash-handler", `Uncaught Exception: ${error.message}`) |
|||
this.generateCrashReport(error, "uncaughtException") |
|||
|
|||
// 显示错误对话框
|
|||
if (this.options.showDialog) { |
|||
this.showCrashDialog(error) |
|||
} |
|||
}) |
|||
} |
|||
|
|||
/** |
|||
* 生成崩溃报告 |
|||
*/ |
|||
private generateCrashReport(error: any, componentInfo?: string, additionalInfo?: Record<string, any>): void { |
|||
try { |
|||
// 格式化错误信息
|
|||
const errorDetail = this.formatError(error, componentInfo, additionalInfo) |
|||
|
|||
// 创建崩溃报告
|
|||
const crashReport: CrashReport = { |
|||
timestamp: new Date().toISOString(), |
|||
error: errorDetail, |
|||
systemInfo: { |
|||
platform: os.platform(), |
|||
release: os.release(), |
|||
arch: os.arch(), |
|||
totalMemory: os.totalmem(), |
|||
freeMemory: os.freemem(), |
|||
uptime: os.uptime(), |
|||
}, |
|||
appInfo: { |
|||
version: app.getVersion(), |
|||
name: app.getName(), |
|||
path: app.getAppPath(), |
|||
argv: process.argv, |
|||
}, |
|||
} |
|||
|
|||
// 保存崩溃报告
|
|||
this.saveCrashReport(crashReport) |
|||
} catch (e) { |
|||
logger.error("crash-handler", `Failed to generate crash report: ${e}`) |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 格式化错误信息 |
|||
*/ |
|||
private formatError(error: any, componentInfo?: string, additionalInfo?: Record<string, any>): ErrorDetail { |
|||
// 基本错误信息
|
|||
const errorDetail: ErrorDetail = { |
|||
message: "", |
|||
timestamp: new Date().toISOString(), |
|||
type: "Unknown", |
|||
} |
|||
|
|||
// 处理不同类型的错误
|
|||
if (error instanceof Error) { |
|||
errorDetail.message = error.message |
|||
errorDetail.type = error.name || error.constructor.name |
|||
errorDetail.stack = error.stack |
|||
} else if (typeof error === "string") { |
|||
errorDetail.message = error |
|||
errorDetail.type = "String" |
|||
} else if (error === null) { |
|||
errorDetail.message = "Null error received" |
|||
errorDetail.type = "Null" |
|||
} else if (error === undefined) { |
|||
errorDetail.message = "Undefined error received" |
|||
errorDetail.type = "Undefined" |
|||
} else if (typeof error === "object") { |
|||
try { |
|||
errorDetail.message = error.message || JSON.stringify(error) |
|||
errorDetail.type = "Object" |
|||
errorDetail.additionalInfo = { ...error } |
|||
} catch (e) { |
|||
errorDetail.message = "Unserializable error object" |
|||
errorDetail.type = "Unserializable" |
|||
} |
|||
} else { |
|||
try { |
|||
errorDetail.message = String(error) |
|||
errorDetail.type = typeof error |
|||
} catch (e) { |
|||
errorDetail.message = "Error converting to string" |
|||
errorDetail.type = "Unknown" |
|||
} |
|||
} |
|||
|
|||
// 添加组件信息
|
|||
if (componentInfo) { |
|||
errorDetail.componentInfo = componentInfo |
|||
} |
|||
|
|||
// 添加额外信息
|
|||
if (additionalInfo) { |
|||
errorDetail.additionalInfo = { |
|||
...errorDetail.additionalInfo, |
|||
...additionalInfo, |
|||
} |
|||
} |
|||
|
|||
return errorDetail |
|||
} |
|||
|
|||
/** |
|||
* 保存崩溃报告 |
|||
*/ |
|||
private saveCrashReport(report: CrashReport): void { |
|||
try { |
|||
// 生成唯一的崩溃报告文件名
|
|||
const timestamp = new Date().toISOString().replace(/[:.]/g, "-") |
|||
const filename = `crash-${timestamp}.json` |
|||
const filepath = path.join(this.crashReportDir, filename) |
|||
|
|||
// 写入崩溃报告
|
|||
fs.writeFileSync(filepath, JSON.stringify(report, null, 2)) |
|||
logger.info("crash-handler", `Crash report saved: ${filepath}`) |
|||
|
|||
// 清理旧的崩溃报告
|
|||
this.cleanupOldReports() |
|||
} catch (e) { |
|||
logger.error("crash-handler", `Failed to save crash report: ${e}`) |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 清理旧的崩溃报告 |
|||
*/ |
|||
private cleanupOldReports(): void { |
|||
try { |
|||
// 获取所有崩溃报告文件
|
|||
const files = fs |
|||
.readdirSync(this.crashReportDir) |
|||
.filter(file => file.startsWith("crash-") && file.endsWith(".json")) |
|||
.map(file => ({ |
|||
name: file, |
|||
path: path.join(this.crashReportDir, file), |
|||
time: fs.statSync(path.join(this.crashReportDir, file)).mtime.getTime(), |
|||
})) |
|||
.sort((a, b) => b.time - a.time) // 按时间降序排序
|
|||
|
|||
// 删除超出最大数量的旧报告
|
|||
if (files.length > this.options.maxReports!) { |
|||
const filesToDelete = files.slice(this.options.maxReports!) |
|||
filesToDelete.forEach(file => { |
|||
fs.unlinkSync(file.path) |
|||
logger.debug("crash-handler", `Deleted old crash report: ${file.name}`) |
|||
}) |
|||
} |
|||
} catch (e) { |
|||
logger.error("crash-handler", `Failed to cleanup old reports: ${e}`) |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 保存启动标记 |
|||
*/ |
|||
private saveStartupMarker(): void { |
|||
try { |
|||
const markerPath = path.join(this.crashReportDir, "startup-marker.json") |
|||
const marker = { |
|||
startTime: this.startTime, |
|||
pid: process.pid, |
|||
} |
|||
fs.writeFileSync(markerPath, JSON.stringify(marker)) |
|||
} catch (e) { |
|||
logger.error("crash-handler", `Failed to save startup marker: ${e}`) |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 清除启动标记 |
|||
*/ |
|||
private clearStartupMarker(): void { |
|||
try { |
|||
const markerPath = path.join(this.crashReportDir, "startup-marker.json") |
|||
if (fs.existsSync(markerPath)) { |
|||
fs.unlinkSync(markerPath) |
|||
} |
|||
} catch (e) { |
|||
logger.error("crash-handler", `Failed to clear startup marker: ${e}`) |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 检查上次是否崩溃 |
|||
*/ |
|||
private checkPreviousCrash(): boolean { |
|||
try { |
|||
const markerPath = path.join(this.crashReportDir, "startup-marker.json") |
|||
// 如果存在启动标记,说明上次可能崩溃了
|
|||
if (fs.existsSync(markerPath)) { |
|||
const markerData = JSON.parse(fs.readFileSync(markerPath, "utf8")) |
|||
const lastStartTime = markerData.startTime |
|||
const lastPid = markerData.pid |
|||
|
|||
logger.warn( |
|||
"crash-handler", |
|||
`Found previous startup marker. App may have crashed. Last PID: ${lastPid}, Last start time: ${new Date(lastStartTime).toISOString()}`, |
|||
) |
|||
|
|||
// 查找最近的崩溃报告
|
|||
const recentCrash = this.getRecentCrashReport() |
|||
|
|||
// 显示崩溃恢复对话框
|
|||
if (recentCrash && this.options.showDialog) { |
|||
app.whenReady().then(() => { |
|||
this.showRecoveryDialog(recentCrash) |
|||
}) |
|||
} |
|||
|
|||
// 清除旧的启动标记
|
|||
fs.unlinkSync(markerPath) |
|||
return true |
|||
} |
|||
} catch (e) { |
|||
logger.error("crash-handler", `Failed to check previous crash: ${e}`) |
|||
} |
|||
|
|||
return false |
|||
} |
|||
|
|||
/** |
|||
* 获取最近的崩溃报告 |
|||
*/ |
|||
private getRecentCrashReport(): CrashReport | null { |
|||
try { |
|||
// 获取所有崩溃报告文件
|
|||
const files = fs |
|||
.readdirSync(this.crashReportDir) |
|||
.filter(file => file.startsWith("crash-") && file.endsWith(".json")) |
|||
.map(file => ({ |
|||
name: file, |
|||
path: path.join(this.crashReportDir, file), |
|||
time: fs.statSync(path.join(this.crashReportDir, file)).mtime.getTime(), |
|||
})) |
|||
.sort((a, b) => b.time - a.time) // 按时间降序排序
|
|||
|
|||
// 读取最近的崩溃报告
|
|||
if (files.length > 0) { |
|||
const recentFile = files[0] |
|||
const reportData = fs.readFileSync(recentFile.path, "utf8") |
|||
return JSON.parse(reportData) as CrashReport |
|||
} |
|||
} catch (e) { |
|||
logger.error("crash-handler", `Failed to get recent crash report: ${e}`) |
|||
} |
|||
|
|||
return null |
|||
} |
|||
|
|||
/** |
|||
* 显示崩溃对话框 |
|||
*/ |
|||
private showCrashDialog(error: Error): void { |
|||
try { |
|||
const options = { |
|||
type: "error" as const, |
|||
title: "应用崩溃", |
|||
message: "应用遇到了一个严重错误,即将关闭", |
|||
detail: `错误信息: ${error.message}\n\n堆栈信息: ${error.stack}\n\n崩溃报告已保存,应用将在您点击确定后关闭。`, |
|||
buttons: ["确定"], |
|||
defaultId: 0, |
|||
} |
|||
|
|||
dialog.showMessageBoxSync(options) |
|||
|
|||
// 强制退出应用
|
|||
setTimeout(() => { |
|||
app.exit(1) |
|||
}, 1000) |
|||
} catch (e) { |
|||
logger.error("crash-handler", `Failed to show crash dialog: ${e}`) |
|||
app.exit(1) |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 显示恢复对话框 |
|||
*/ |
|||
private showRecoveryDialog(crashReport: CrashReport): void { |
|||
try { |
|||
const crashTime = new Date(crashReport.timestamp).toLocaleString() |
|||
const errorMessage = crashReport.error.message |
|||
const errorType = crashReport.error.type |
|||
|
|||
const options = { |
|||
type: "warning" as const, |
|||
title: "应用恢复", |
|||
message: "应用上次异常退出", |
|||
detail: `应用在 ${crashTime} 因 ${errorType} 错误崩溃: ${errorMessage}\n\n崩溃报告已保存,您可以继续使用应用或联系开发者报告此问题。`, |
|||
buttons: ["继续", "查看详情"], |
|||
defaultId: 0, |
|||
} |
|||
|
|||
const response = dialog.showMessageBoxSync(options) |
|||
|
|||
// 如果用户选择查看详情
|
|||
if (response === 1) { |
|||
// 显示详细的崩溃报告
|
|||
this.showDetailedCrashInfo(crashReport) |
|||
} |
|||
} catch (e) { |
|||
logger.error("crash-handler", `Failed to show recovery dialog: ${e}`) |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 显示详细的崩溃信息 |
|||
*/ |
|||
private showDetailedCrashInfo(crashReport: CrashReport): void { |
|||
try { |
|||
const options = { |
|||
type: "info" as const, |
|||
title: "崩溃详情", |
|||
message: "应用崩溃详细信息", |
|||
detail: JSON.stringify(crashReport, null, 2), |
|||
buttons: ["关闭"], |
|||
defaultId: 0, |
|||
} |
|||
|
|||
dialog.showMessageBoxSync(options) |
|||
} catch (e) { |
|||
logger.error("crash-handler", `Failed to show detailed crash info: ${e}`) |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 设置崩溃处理选项 |
|||
*/ |
|||
public setOptions(options: Partial<CrashHandlerOptions>): void { |
|||
this.options = { ...this.options, ...options } |
|||
} |
|||
|
|||
/** |
|||
* 获取当前选项 |
|||
*/ |
|||
public getOptions(): CrashHandlerOptions { |
|||
return { ...this.options } |
|||
} |
|||
} |
|||
|
|||
// 创建默认实例
|
|||
const crashHandler = CrashHandler.getInstance() |
|||
|
|||
export default crashHandler |
|||
export { crashHandler } |
@ -0,0 +1,177 @@ |
|||
import { LogLevel, LogLevelName } from "./common" |
|||
import logger from "./main" |
|||
|
|||
/** |
|||
* 错误详情接口 |
|||
*/ |
|||
export interface ErrorDetail { |
|||
message: string |
|||
stack?: string |
|||
componentInfo?: string |
|||
additionalInfo?: Record<string, any> |
|||
timestamp: string |
|||
type: string |
|||
} |
|||
|
|||
/** |
|||
* 错误处理配置 |
|||
*/ |
|||
export interface ErrorHandlerOptions { |
|||
namespace?: string |
|||
level?: LogLevel |
|||
includeStack?: boolean |
|||
includeComponentInfo?: boolean |
|||
formatError?: (error: any) => ErrorDetail |
|||
} |
|||
|
|||
/** |
|||
* 默认错误处理配置 |
|||
*/ |
|||
const DEFAULT_OPTIONS: ErrorHandlerOptions = { |
|||
namespace: "error", |
|||
level: LogLevel.ERROR, |
|||
includeStack: true, |
|||
includeComponentInfo: true, |
|||
} |
|||
|
|||
/** |
|||
* 格式化错误信息 |
|||
*/ |
|||
const formatError = (error: any, options: ErrorHandlerOptions): ErrorDetail => { |
|||
// 如果已经是ErrorDetail格式,直接返回
|
|||
if (error && typeof error === "object" && error.type && error.message && error.timestamp) { |
|||
return error as ErrorDetail |
|||
} |
|||
|
|||
// 基本错误信息
|
|||
const errorDetail: ErrorDetail = { |
|||
message: "", |
|||
timestamp: new Date().toISOString(), |
|||
type: "Unknown", |
|||
} |
|||
|
|||
// 处理不同类型的错误
|
|||
if (error instanceof Error) { |
|||
errorDetail.message = error.message |
|||
errorDetail.type = error.name || error.constructor.name |
|||
if (options.includeStack) { |
|||
errorDetail.stack = error.stack |
|||
} |
|||
} else if (typeof error === "string") { |
|||
errorDetail.message = error |
|||
errorDetail.type = "String" |
|||
} else if (error === null) { |
|||
errorDetail.message = "Null error received" |
|||
errorDetail.type = "Null" |
|||
} else if (error === undefined) { |
|||
errorDetail.message = "Undefined error received" |
|||
errorDetail.type = "Undefined" |
|||
} else if (typeof error === "object") { |
|||
try { |
|||
errorDetail.message = error.message || JSON.stringify(error) |
|||
errorDetail.type = "Object" |
|||
errorDetail.additionalInfo = { ...error } |
|||
} catch (e) { |
|||
errorDetail.message = "Unserializable error object" |
|||
errorDetail.type = "Unserializable" |
|||
} |
|||
} else { |
|||
try { |
|||
errorDetail.message = String(error) |
|||
errorDetail.type = typeof error |
|||
} catch (e) { |
|||
errorDetail.message = "Error converting to string" |
|||
errorDetail.type = "Unknown" |
|||
} |
|||
} |
|||
|
|||
return errorDetail |
|||
} |
|||
|
|||
/** |
|||
* 错误处理类 |
|||
*/ |
|||
export class ErrorHandler { |
|||
private options: ErrorHandlerOptions |
|||
|
|||
constructor(options?: Partial<ErrorHandlerOptions>) { |
|||
this.options = { ...DEFAULT_OPTIONS, ...options } |
|||
} |
|||
|
|||
/** |
|||
* 记录错误 |
|||
*/ |
|||
public captureError(error: any, componentInfo?: string, additionalInfo?: Record<string, any>): void { |
|||
const errorDetail = formatError(error, this.options) |
|||
|
|||
// 添加组件信息
|
|||
if (this.options.includeComponentInfo && componentInfo) { |
|||
errorDetail.componentInfo = componentInfo |
|||
} |
|||
|
|||
// 添加额外信息
|
|||
if (additionalInfo) { |
|||
errorDetail.additionalInfo = { |
|||
...errorDetail.additionalInfo, |
|||
...additionalInfo, |
|||
} |
|||
} |
|||
|
|||
// 使用logger记录错误
|
|||
const namespace = this.options.namespace || "error" |
|||
const level = LogLevelName[this.options.level || LogLevel.ERROR].toLowerCase() |
|||
|
|||
// 构建错误消息
|
|||
let errorMessage = `${errorDetail.type}: ${errorDetail.message}` |
|||
if (errorDetail.componentInfo) { |
|||
errorMessage += ` | Component: ${errorDetail.componentInfo}` |
|||
} |
|||
|
|||
// 记录错误
|
|||
logger[level](namespace, errorMessage) |
|||
|
|||
// 如果有堆栈信息,单独记录
|
|||
if (errorDetail.stack) { |
|||
logger[level](namespace, `Stack: ${errorDetail.stack}`) |
|||
} |
|||
|
|||
// 如果有额外信息,单独记录
|
|||
if (errorDetail.additionalInfo) { |
|||
try { |
|||
const additionalInfoStr = JSON.stringify(errorDetail.additionalInfo, null, 2) |
|||
logger[level](namespace, `Additional Info: ${additionalInfoStr}`) |
|||
} catch (e) { |
|||
logger[level](namespace, "Additional Info: [Unserializable]") |
|||
} |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 设置错误处理选项 |
|||
*/ |
|||
public setOptions(options: Partial<ErrorHandlerOptions>): void { |
|||
this.options = { ...this.options, ...options } |
|||
} |
|||
|
|||
/** |
|||
* 获取当前选项 |
|||
*/ |
|||
public getOptions(): ErrorHandlerOptions { |
|||
return { ...this.options } |
|||
} |
|||
} |
|||
|
|||
// 创建默认实例
|
|||
const errorHandler = new ErrorHandler() |
|||
|
|||
// 捕获未处理的Promise异常
|
|||
process.on("unhandledRejection", reason => { |
|||
errorHandler.captureError(reason) |
|||
}) |
|||
|
|||
// 捕获未捕获的异常
|
|||
process.on("uncaughtException", error => { |
|||
errorHandler.captureError(error) |
|||
}) |
|||
|
|||
export default errorHandler |
@ -0,0 +1,275 @@ |
|||
import { app, ipcMain } from "electron" |
|||
import fs from "fs" |
|||
import path from "path" |
|||
import setting from "setting/main" |
|||
import * as rfs from "rotating-file-stream" |
|||
import { LogLevel, LogLevelColor, LogLevelName } from "./common" |
|||
|
|||
|
|||
// 重置颜色的ANSI代码
|
|||
const RESET_COLOR = "\x1b[0m" |
|||
|
|||
// 日志配置接口
|
|||
export interface LoggerOptions { |
|||
level?: LogLevel // 日志级别
|
|||
namespace?: string // 日志命名空间
|
|||
console?: boolean // 是否输出到控制台
|
|||
file?: boolean // 是否输出到文件
|
|||
maxSize?: string // 单个日志文件最大大小
|
|||
maxFiles?: number // 保留的最大日志文件数量
|
|||
} |
|||
|
|||
// 默认配置
|
|||
const DEFAULT_OPTIONS: LoggerOptions = { |
|||
level: setting.values("debug"), |
|||
namespace: "app", |
|||
console: true, |
|||
file: true, |
|||
maxSize: "10M", |
|||
maxFiles: 10, |
|||
} |
|||
|
|||
let logDir |
|||
const isElectronApp = !!process.versions.electron |
|||
if (isElectronApp && app) { |
|||
logDir = path.join(app.getPath("logs")) |
|||
} else { |
|||
// 非Electron环境下使用当前目录下的logs文件夹
|
|||
logDir = path.join(process.cwd(), "logs") |
|||
} |
|||
|
|||
/** |
|||
* 日志管理类 |
|||
*/ |
|||
export class Logger { |
|||
private static instance: Logger |
|||
private options: LoggerOptions = DEFAULT_OPTIONS |
|||
private logStream: rfs.RotatingFileStream | null = null |
|||
private logDir: string = logDir |
|||
private currentLogFile: string = "" |
|||
private isElectronApp: boolean = !!process.versions.electron |
|||
private callInitialize: boolean = false |
|||
|
|||
/** |
|||
* 获取单例实例 |
|||
*/ |
|||
public static getInstance(): Logger { |
|||
if (!Logger.instance) { |
|||
Logger.instance = new Logger() |
|||
} |
|||
if (Logger.instance.callInitialize) { |
|||
return Logger.instance |
|||
} else { |
|||
// 创建代理对象,确保只有在初始化后才能访问除init之外的方法
|
|||
const handler = { |
|||
get: function (target: any, prop: string) { |
|||
if (prop === "init") { |
|||
return target[prop] |
|||
} |
|||
if (!target.callInitialize) { |
|||
throw new Error(`Logger未初始化,不能调用${prop}方法,请先调用init()方法`) |
|||
} |
|||
return target[prop] |
|||
}, |
|||
} |
|||
Logger.instance = new Proxy(new Logger(), handler) |
|||
} |
|||
return Logger.instance |
|||
} |
|||
|
|||
/** |
|||
* 构造函数 |
|||
*/ |
|||
private constructor() {} |
|||
|
|||
public init(options?: LoggerOptions): void { |
|||
this.callInitialize = true |
|||
this.options = { ...this.options, ...options } |
|||
|
|||
// 确保日志目录存在
|
|||
if (!fs.existsSync(this.logDir)) { |
|||
fs.mkdirSync(this.logDir, { recursive: true }) |
|||
} |
|||
|
|||
// 初始化日志文件
|
|||
this.initLogFile() |
|||
|
|||
// 如果在主进程中,设置IPC监听器接收渲染进程的日志
|
|||
if (this.isElectronApp && process.type === "browser") { |
|||
this.setupIPC() |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 初始化日志文件 |
|||
*/ |
|||
private initLogFile(): void { |
|||
if (!this.options.file) return |
|||
|
|||
// 生成日志文件名
|
|||
const now = new Date() |
|||
const timestamp = now.toISOString().replace(/[:.]/g, "-") |
|||
this.currentLogFile = `app-logger-${timestamp}.log` |
|||
|
|||
// 创建日志流
|
|||
this.logStream = rfs.createStream(this.currentLogFile, { |
|||
path: this.logDir, |
|||
size: this.options.maxSize, |
|||
rotate: this.options.maxFiles, |
|||
}) |
|||
} |
|||
|
|||
/** |
|||
* 设置IPC通信,接收渲染进程日志 |
|||
*/ |
|||
private setupIPC(): void { |
|||
if (!ipcMain) return |
|||
ipcMain.on("logger:log", (_, level: LogLevel, namespace: string, ...messages: any[]) => { |
|||
this.logWithLevel(level, namespace, ...messages) |
|||
}) |
|||
|
|||
// 处理日志级别设置请求
|
|||
ipcMain.on("logger:setLevel", (_, level: LogLevel) => { |
|||
this.setLevel(level) |
|||
}) |
|||
} |
|||
|
|||
/** |
|||
* 关闭日志流 |
|||
*/ |
|||
public close(): void { |
|||
if (this.logStream) { |
|||
this.logStream.end() |
|||
this.logStream.destroy() |
|||
this.logStream = null |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 设置日志级别 |
|||
*/ |
|||
public setLevel(level: LogLevel): void { |
|||
this.options.level = level |
|||
} |
|||
|
|||
/** |
|||
* 获取当前日志级别 |
|||
*/ |
|||
public getLevel(): LogLevel { |
|||
return this.options.level ?? LogLevel.INFO |
|||
} |
|||
|
|||
/** |
|||
* 根据级别记录日志 |
|||
*/ |
|||
private logWithLevel(level: LogLevel, namespace: string, ...messages: any[]): void { |
|||
// 检查日志级别
|
|||
if (level < this.getLevel() || level === LogLevel.OFF) return |
|||
|
|||
const timestamp = new Date().toISOString() |
|||
const levelName = LogLevelName[level] |
|||
const prefix = `[${timestamp}] [${namespace}] [${levelName}]` |
|||
|
|||
// 格式化消息
|
|||
const formattedMessages = messages.map(msg => { |
|||
if (typeof msg === "object") { |
|||
try { |
|||
return JSON.stringify(msg) |
|||
} catch (e) { |
|||
return String(msg) |
|||
} |
|||
} |
|||
return String(msg) |
|||
}) |
|||
|
|||
const message = formattedMessages.join(" ") |
|||
|
|||
// 输出到控制台
|
|||
if (this.options.console) { |
|||
const color = LogLevelColor[level] |
|||
console.log(`${color}${prefix} ${message}${RESET_COLOR}`) |
|||
} |
|||
|
|||
// 写入日志文件
|
|||
if (this.options.file && this.logStream) { |
|||
this.logStream.write(`${prefix} ${message}\n`) |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 记录跟踪级别日志 |
|||
*/ |
|||
public trace(namespace: string, ...messages: any[]): void { |
|||
this.logWithLevel(LogLevel.TRACE, namespace, ...messages) |
|||
} |
|||
|
|||
/** |
|||
* 记录调试级别日志 |
|||
*/ |
|||
public debug(namespace: string, ...messages: any[]): void { |
|||
this.logWithLevel(LogLevel.DEBUG, namespace, ...messages) |
|||
} |
|||
|
|||
/** |
|||
* 记录信息级别日志 |
|||
*/ |
|||
public info(namespace: string, ...messages: any[]): void { |
|||
this.logWithLevel(LogLevel.INFO, namespace, ...messages) |
|||
} |
|||
|
|||
/** |
|||
* 记录警告级别日志 |
|||
*/ |
|||
public warn(namespace: string, ...messages: any[]): void { |
|||
this.logWithLevel(LogLevel.WARN, namespace, ...messages) |
|||
} |
|||
|
|||
/** |
|||
* 记录错误级别日志 |
|||
*/ |
|||
public error(namespace: string, ...messages: any[]): void { |
|||
this.logWithLevel(LogLevel.ERROR, namespace, ...messages) |
|||
} |
|||
|
|||
/** |
|||
* 记录致命错误级别日志 |
|||
*/ |
|||
public fatal(namespace: string, ...messages: any[]): void { |
|||
this.logWithLevel(LogLevel.FATAL, namespace, ...messages) |
|||
} |
|||
|
|||
/** |
|||
* 创建一个固定命名空间的日志记录器 |
|||
* @param namespace 命名空间 |
|||
* @returns 带有固定命名空间的日志记录器 |
|||
*/ |
|||
public createNamespace(namespace: string) { |
|||
return { |
|||
trace: (...messages: any[]) => this.trace(namespace, ...messages), |
|||
debug: (...messages: any[]) => this.debug(namespace, ...messages), |
|||
info: (...messages: any[]) => this.info(namespace, ...messages), |
|||
warn: (...messages: any[]) => this.warn(namespace, ...messages), |
|||
error: (...messages: any[]) => this.error(namespace, ...messages), |
|||
fatal: (...messages: any[]) => this.fatal(namespace, ...messages), |
|||
setLevel: (level: LogLevel) => this.setLevel(level), |
|||
getLevel: () => this.getLevel(), |
|||
} |
|||
} |
|||
} |
|||
|
|||
// 默认实例
|
|||
const logger = Logger.getInstance() |
|||
logger.init() |
|||
setting.onChange("debug", function(n){ |
|||
logger.setLevel(n.debug) |
|||
}) |
|||
|
|||
// 应用退出时关闭日志流
|
|||
if (process.type === "browser" && app) { |
|||
app.on("before-quit", () => { |
|||
logger.info("app", "应用关闭") |
|||
logger.close() |
|||
}) |
|||
} |
|||
|
|||
export default logger |
@ -0,0 +1,7 @@ |
|||
{ |
|||
"name": "logger", |
|||
"version": "1.0.0", |
|||
"keywords": [], |
|||
"author": "", |
|||
"license": "ISC" |
|||
} |
@ -0,0 +1,195 @@ |
|||
import { contextBridge, ipcRenderer } from "electron" |
|||
import { LogLevel, LogLevelName } from "./common" |
|||
import logger from "./preload" |
|||
|
|||
/** |
|||
* 错误详情接口 |
|||
*/ |
|||
interface ErrorDetail { |
|||
message: string |
|||
stack?: string |
|||
componentInfo?: string |
|||
additionalInfo?: Record<string, any> |
|||
timestamp: string |
|||
type: string |
|||
} |
|||
|
|||
/** |
|||
* 错误处理配置 |
|||
*/ |
|||
interface ErrorHandlerOptions { |
|||
namespace?: string |
|||
level?: LogLevel |
|||
includeStack?: boolean |
|||
includeComponentInfo?: boolean |
|||
} |
|||
|
|||
/** |
|||
* 渲染进程错误处理接口 |
|||
*/ |
|||
interface IRendererErrorHandler { |
|||
/** |
|||
* 捕获错误 |
|||
*/ |
|||
captureError(error: any, componentInfo?: string, additionalInfo?: Record<string, any>): void |
|||
|
|||
/** |
|||
* 设置错误处理选项 |
|||
*/ |
|||
setOptions(options: Partial<ErrorHandlerOptions>): void |
|||
|
|||
/** |
|||
* 获取当前选项 |
|||
*/ |
|||
getOptions(): ErrorHandlerOptions |
|||
|
|||
/** |
|||
* 安装全局错误处理器 |
|||
*/ |
|||
installGlobalHandlers(): void |
|||
} |
|||
|
|||
/** |
|||
* 默认错误处理配置 |
|||
*/ |
|||
const DEFAULT_OPTIONS: ErrorHandlerOptions = { |
|||
namespace: "error", |
|||
level: LogLevel.ERROR, |
|||
includeStack: true, |
|||
includeComponentInfo: true, |
|||
} |
|||
|
|||
/** |
|||
* 格式化错误信息 |
|||
*/ |
|||
const formatError = (error: any, options: ErrorHandlerOptions): ErrorDetail => { |
|||
// 基本错误信息
|
|||
const errorDetail: ErrorDetail = { |
|||
message: "", |
|||
timestamp: new Date().toISOString(), |
|||
type: "Unknown", |
|||
} |
|||
// 处理不同类型的错误
|
|||
if (error instanceof Error) { |
|||
errorDetail.message = error.message |
|||
errorDetail.type = error.name || error.constructor.name |
|||
if (options.includeStack) { |
|||
errorDetail.stack = error.stack |
|||
} |
|||
} else if (typeof error === "string") { |
|||
errorDetail.message = error |
|||
errorDetail.type = "String" |
|||
} else if (error === null) { |
|||
errorDetail.message = "Null error received" |
|||
errorDetail.type = "Null" |
|||
} else if (error === undefined) { |
|||
errorDetail.message = "Undefined error received" |
|||
errorDetail.type = "Undefined" |
|||
} else if (typeof error === "object") { |
|||
try { |
|||
errorDetail.message = error.message || JSON.stringify(error) |
|||
errorDetail.type = "Object" |
|||
errorDetail.additionalInfo = { ...error } |
|||
} catch (e) { |
|||
errorDetail.message = "Unserializable error object" |
|||
errorDetail.type = "Unserializable" |
|||
} |
|||
} else { |
|||
try { |
|||
errorDetail.message = String(error) |
|||
errorDetail.type = typeof error |
|||
} catch (e) { |
|||
errorDetail.message = "Error converting to string" |
|||
errorDetail.type = "Unknown" |
|||
} |
|||
} |
|||
|
|||
return errorDetail |
|||
} |
|||
|
|||
/** |
|||
* 创建渲染进程错误处理器 |
|||
*/ |
|||
const createRendererErrorHandler = (): IRendererErrorHandler => { |
|||
// 当前错误处理选项
|
|||
let options: ErrorHandlerOptions = { ...DEFAULT_OPTIONS } |
|||
|
|||
/** |
|||
* 处理并转发错误到主进程 |
|||
*/ |
|||
const handleError = (error: any, componentInfo?: string, additionalInfo?: Record<string, any>) => { |
|||
// 如果已经是ErrorDetail格式,直接使用
|
|||
let errorDetail: ErrorDetail |
|||
if (error && typeof error === "object" && error.type && error.message && error.timestamp) { |
|||
errorDetail = error as ErrorDetail |
|||
} else { |
|||
// 否则格式化错误
|
|||
errorDetail = formatError(error, options) |
|||
} |
|||
|
|||
// 添加组件信息
|
|||
if (options.includeComponentInfo && componentInfo) { |
|||
errorDetail.componentInfo = componentInfo |
|||
} |
|||
|
|||
// 使用logger记录错误
|
|||
const namespace = options.namespace || "error" |
|||
const level = LogLevelName[options.level || LogLevel.ERROR].toLowerCase() |
|||
|
|||
// 添加额外信息
|
|||
if (additionalInfo) { |
|||
errorDetail.additionalInfo = { |
|||
...errorDetail.additionalInfo, |
|||
...additionalInfo, |
|||
} |
|||
} |
|||
|
|||
// 记录完整的错误信息
|
|||
logger[level](namespace, JSON.stringify(errorDetail)) |
|||
|
|||
// 同时在控制台输出错误信息
|
|||
logger[level](namespace, `${errorDetail.type}: ${errorDetail.message}`) |
|||
if (errorDetail.stack) { |
|||
logger[level](namespace, `Stack: ${errorDetail.stack}`) |
|||
} |
|||
|
|||
// 如果有额外信息,单独记录
|
|||
if (errorDetail.additionalInfo) { |
|||
try { |
|||
const additionalInfoStr = JSON.stringify(errorDetail.additionalInfo, null, 2) |
|||
logger[level](namespace, `Additional Info: ${additionalInfoStr}`) |
|||
} catch (e) { |
|||
logger[level](namespace, "Additional Info: [Unserializable]") |
|||
} |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 空的安装全局错误处理器方法 |
|||
* 实际的全局错误处理由renderer-error.ts负责 |
|||
*/ |
|||
const installGlobalHandlers = () => { |
|||
// 不再在preload层安装全局错误处理器
|
|||
// 仅记录日志表明该方法被调用
|
|||
logger.info("[ErrorHandler] Global error handlers should be installed in renderer process") |
|||
} |
|||
|
|||
return { |
|||
captureError: handleError, |
|||
setOptions: (newOptions: Partial<ErrorHandlerOptions>) => { |
|||
options = { ...options, ...newOptions } |
|||
// 同步选项到主进程
|
|||
ipcRenderer.send("logger:errorOptions", options) |
|||
}, |
|||
getOptions: () => ({ ...options }), |
|||
installGlobalHandlers, |
|||
} |
|||
} |
|||
|
|||
const errorHandler = createRendererErrorHandler() |
|||
|
|||
// 暴露错误处理器到渲染进程全局
|
|||
contextBridge.exposeInMainWorld("preloadErrorHandler", errorHandler) |
|||
|
|||
// 导出类型定义,方便在渲染进程中使用
|
|||
export type { IRendererErrorHandler, ErrorDetail, ErrorHandlerOptions } |
@ -0,0 +1,153 @@ |
|||
import { contextBridge, ipcRenderer } from "electron" |
|||
import { LogLevel } from "./common" |
|||
|
|||
/** |
|||
* 渲染进程日志接口 |
|||
*/ |
|||
interface IRendererLogger { |
|||
trace(namespace: string, ...messages: any[]): void |
|||
debug(namespace: string, ...messages: any[]): void |
|||
info(namespace: string, ...messages: any[]): void |
|||
warn(namespace: string, ...messages: any[]): void |
|||
error(namespace: string, ...messages: any[]): void |
|||
fatal(namespace: string, ...messages: any[]): void |
|||
setLevel(level: LogLevel): void |
|||
createNamespace(namespace: string): INamespacedLogger |
|||
} |
|||
|
|||
/** |
|||
* 命名空间作用域日志接口 |
|||
*/ |
|||
interface INamespacedLogger { |
|||
trace(...messages: any[]): void |
|||
debug(...messages: any[]): void |
|||
info(...messages: any[]): void |
|||
warn(...messages: any[]): void |
|||
error(...messages: any[]): void |
|||
fatal(...messages: any[]): void |
|||
setLevel(level: LogLevel): void |
|||
} |
|||
|
|||
// 日志级别名称映射
|
|||
const LogLevelName: Record<LogLevel, string> = { |
|||
[LogLevel.TRACE]: "TRACE", |
|||
[LogLevel.DEBUG]: "DEBUG", |
|||
[LogLevel.INFO]: "INFO", |
|||
[LogLevel.WARN]: "WARN", |
|||
[LogLevel.ERROR]: "ERROR", |
|||
[LogLevel.FATAL]: "FATAL", |
|||
[LogLevel.OFF]: "OFF", |
|||
} |
|||
|
|||
// 日志颜色映射(控制台输出用)
|
|||
const LogLevelColor: Record<LogLevel, string> = { |
|||
[LogLevel.TRACE]: "\x1b[90m", // 灰色
|
|||
[LogLevel.DEBUG]: "\x1b[36m", // 青色
|
|||
[LogLevel.INFO]: "\x1b[32m", // 绿色
|
|||
[LogLevel.WARN]: "\x1b[33m", // 黄色
|
|||
[LogLevel.ERROR]: "\x1b[31m", // 红色
|
|||
[LogLevel.FATAL]: "\x1b[35m", // 紫色
|
|||
[LogLevel.OFF]: "", // 无色
|
|||
} |
|||
|
|||
// 重置颜色的ANSI代码
|
|||
const RESET_COLOR = "\x1b[0m" |
|||
|
|||
/** |
|||
* 创建渲染进程日志对象 |
|||
*/ |
|||
const createRendererLogger = (): IRendererLogger => { |
|||
// 当前日志级别
|
|||
let currentLevel: LogLevel = LogLevel.INFO |
|||
|
|||
// 格式化消息
|
|||
const formatMessages = (messages: any[]): string => { |
|||
return messages |
|||
.map(msg => { |
|||
if (typeof msg === "object") { |
|||
try { |
|||
return JSON.stringify(msg) |
|||
} catch (e) { |
|||
return String(msg) |
|||
} |
|||
} |
|||
return String(msg) |
|||
}) |
|||
.join(" ") |
|||
} |
|||
|
|||
// 本地打印日志
|
|||
const printLog = (level: LogLevel, namespace: string, ...messages: any[]): void => { |
|||
// 检查日志级别
|
|||
if (level < currentLevel || level === LogLevel.OFF) return |
|||
|
|||
const timestamp = new Date().toISOString() |
|||
const levelName = LogLevelName[level] |
|||
const prefix = `[${timestamp}] [${namespace}] [${levelName}]` |
|||
const message = formatMessages(messages) |
|||
|
|||
// 输出到控制台
|
|||
const color = LogLevelColor[level] |
|||
console.log(`${color}${prefix} ${message}${RESET_COLOR}`) |
|||
} |
|||
|
|||
// 通过IPC发送日志到主进程
|
|||
const sendLog = (level: LogLevel, namespace: string, ...messages: any[]) => { |
|||
// 本地打印
|
|||
printLog(level, namespace, ...messages) |
|||
|
|||
// 发送到主进程
|
|||
ipcRenderer.send("logger:log", level, namespace, ...messages) |
|||
} |
|||
|
|||
return { |
|||
trace(namespace: string, ...messages: any[]): void { |
|||
sendLog(LogLevel.TRACE, namespace, ...messages) |
|||
}, |
|||
debug(namespace: string, ...messages: any[]): void { |
|||
sendLog(LogLevel.DEBUG, namespace, ...messages) |
|||
}, |
|||
info(namespace: string, ...messages: any[]): void { |
|||
sendLog(LogLevel.INFO, namespace, ...messages) |
|||
}, |
|||
warn(namespace: string, ...messages: any[]): void { |
|||
sendLog(LogLevel.WARN, namespace, ...messages) |
|||
}, |
|||
error(namespace: string, ...messages: any[]): void { |
|||
sendLog(LogLevel.ERROR, namespace, ...messages) |
|||
}, |
|||
fatal(namespace: string, ...messages: any[]): void { |
|||
sendLog(LogLevel.FATAL, namespace, ...messages) |
|||
}, |
|||
setLevel(level: LogLevel): void { |
|||
// 更新本地日志级别
|
|||
currentLevel = level |
|||
// 设置日志级别(可选,如果需要在渲染进程中动态调整日志级别)
|
|||
ipcRenderer.send("logger:setLevel", level) |
|||
}, |
|||
createNamespace(namespace: string): INamespacedLogger { |
|||
return { |
|||
trace: (...messages: any[]) => sendLog(LogLevel.TRACE, namespace, ...messages), |
|||
debug: (...messages: any[]) => sendLog(LogLevel.DEBUG, namespace, ...messages), |
|||
info: (...messages: any[]) => sendLog(LogLevel.INFO, namespace, ...messages), |
|||
warn: (...messages: any[]) => sendLog(LogLevel.WARN, namespace, ...messages), |
|||
error: (...messages: any[]) => sendLog(LogLevel.ERROR, namespace, ...messages), |
|||
fatal: (...messages: any[]) => sendLog(LogLevel.FATAL, namespace, ...messages), |
|||
setLevel: (level: LogLevel) => { |
|||
currentLevel = level |
|||
ipcRenderer.send("logger:setLevel", level) |
|||
}, |
|||
} |
|||
}, |
|||
} |
|||
} |
|||
|
|||
const logger = createRendererLogger() |
|||
|
|||
// 暴露logger对象到渲染进程全局
|
|||
contextBridge.exposeInMainWorld("logger", logger) |
|||
|
|||
export { logger } |
|||
export default logger |
|||
// 导出类型定义,方便在渲染进程中使用
|
|||
export type { IRendererLogger } |
@ -0,0 +1,243 @@ |
|||
import { LogLevel } from "./common" |
|||
|
|||
/** |
|||
* 错误详情接口 |
|||
*/ |
|||
interface ErrorDetail { |
|||
message: string |
|||
stack?: string |
|||
componentInfo?: string |
|||
additionalInfo?: Record<string, any> |
|||
timestamp: string |
|||
type: string |
|||
} |
|||
|
|||
/** |
|||
* 错误处理配置 |
|||
*/ |
|||
interface ErrorHandlerOptions { |
|||
namespace?: string |
|||
level?: LogLevel |
|||
includeStack?: boolean |
|||
includeComponentInfo?: boolean |
|||
} |
|||
|
|||
/** |
|||
* 渲染进程错误处理接口 |
|||
*/ |
|||
export interface IRendererErrorHandler { |
|||
/** |
|||
* 捕获错误 |
|||
*/ |
|||
captureError(error: any, componentInfo?: string, additionalInfo?: Record<string, any>): void |
|||
|
|||
/** |
|||
* 设置错误处理选项 |
|||
*/ |
|||
setOptions(options: Partial<ErrorHandlerOptions>): void |
|||
|
|||
/** |
|||
* 获取当前选项 |
|||
*/ |
|||
getOptions(): ErrorHandlerOptions |
|||
|
|||
/** |
|||
* 安装全局错误处理器 |
|||
*/ |
|||
installGlobalHandlers(): void |
|||
} |
|||
|
|||
/** |
|||
* 默认错误处理配置 |
|||
*/ |
|||
const DEFAULT_OPTIONS: ErrorHandlerOptions = { |
|||
namespace: "error", |
|||
level: LogLevel.ERROR, |
|||
includeStack: true, |
|||
includeComponentInfo: true, |
|||
} |
|||
|
|||
/** |
|||
* 格式化错误信息 |
|||
*/ |
|||
const formatError = (error: any, options: ErrorHandlerOptions): ErrorDetail => { |
|||
// 基本错误信息
|
|||
const errorDetail: ErrorDetail = { |
|||
message: "", |
|||
timestamp: new Date().toISOString(), |
|||
type: "Unknown", |
|||
} |
|||
console.log(error) |
|||
|
|||
// 处理不同类型的错误
|
|||
if (error instanceof Error) { |
|||
errorDetail.message = error.message |
|||
errorDetail.type = error.name || error.constructor.name |
|||
if (options.includeStack) { |
|||
errorDetail.stack = error.stack |
|||
} |
|||
} else if (typeof error === "string") { |
|||
errorDetail.message = error |
|||
errorDetail.type = "String" |
|||
} else if (error === null) { |
|||
errorDetail.message = "Null error received" |
|||
errorDetail.type = "Null" |
|||
} else if (error === undefined) { |
|||
errorDetail.message = "Undefined error received" |
|||
errorDetail.type = "Undefined" |
|||
} else if (typeof error === "object") { |
|||
try { |
|||
errorDetail.message = error.message || JSON.stringify(error) |
|||
errorDetail.type = "Object" |
|||
errorDetail.additionalInfo = { ...error } |
|||
} catch (e) { |
|||
errorDetail.message = "Unserializable error object" |
|||
errorDetail.type = "Unserializable" |
|||
} |
|||
} else { |
|||
try { |
|||
errorDetail.message = String(error) |
|||
errorDetail.type = typeof error |
|||
} catch (e) { |
|||
errorDetail.message = "Error converting to string" |
|||
errorDetail.type = "Unknown" |
|||
} |
|||
} |
|||
|
|||
return errorDetail |
|||
} |
|||
|
|||
// @ts-ignore
|
|||
const preloadErrorHandler = window.preloadErrorHandler |
|||
|
|||
/** |
|||
* 创建渲染进程错误处理器 |
|||
*/ |
|||
export const createRendererErrorHandler = (): IRendererErrorHandler => { |
|||
// 当前错误处理选项
|
|||
let options: ErrorHandlerOptions = { ...DEFAULT_OPTIONS } |
|||
|
|||
/** |
|||
* 处理错误并序列化 |
|||
*/ |
|||
const processError = (error: any, componentInfo?: string, additionalInfo?: Record<string, any>): ErrorDetail => { |
|||
const errorDetail = formatError(error, options) |
|||
|
|||
// 添加组件信息
|
|||
if (options.includeComponentInfo && componentInfo) { |
|||
errorDetail.componentInfo = componentInfo |
|||
} |
|||
|
|||
// 添加额外信息
|
|||
if (additionalInfo) { |
|||
errorDetail.additionalInfo = { |
|||
...errorDetail.additionalInfo, |
|||
...additionalInfo, |
|||
} |
|||
} |
|||
|
|||
return errorDetail |
|||
} |
|||
|
|||
/** |
|||
* 发送错误到preload层 |
|||
*/ |
|||
const sendError = (error: any, componentInfo?: string, additionalInfo?: Record<string, any>) => { |
|||
// 处理并序列化错误
|
|||
const errorDetail = processError(error, componentInfo, additionalInfo) |
|||
|
|||
// 调用window.errorHandler.captureError发送错误
|
|||
// 这里假设preload层已经暴露了errorHandler对象
|
|||
if (preloadErrorHandler && typeof preloadErrorHandler.captureError === "function") { |
|||
preloadErrorHandler.captureError(errorDetail) |
|||
} else { |
|||
// 如果errorHandler不可用,则降级到控制台输出
|
|||
console.error("[ErrorHandler]", errorDetail) |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 安装全局错误处理器 |
|||
*/ |
|||
const installGlobalHandlers = () => { |
|||
// 捕获未处理的异常
|
|||
window.addEventListener("error", event => { |
|||
event.preventDefault() |
|||
sendError(event.error || event.message, "window.onerror", { |
|||
filename: event.filename, |
|||
lineno: event.lineno, |
|||
colno: event.colno, |
|||
}) |
|||
return true |
|||
}) |
|||
|
|||
// 捕获未处理的Promise拒绝
|
|||
window.addEventListener("unhandledrejection", event => { |
|||
event.preventDefault() |
|||
sendError(event.reason, "unhandledrejection", { |
|||
promise: "[Promise]", // 不能直接序列化Promise对象
|
|||
}) |
|||
return true |
|||
}) |
|||
|
|||
// 捕获资源加载错误
|
|||
document.addEventListener( |
|||
"error", |
|||
event => { |
|||
// 只处理资源加载错误
|
|||
if (event.target && (event.target as HTMLElement).tagName) { |
|||
const target = event.target as HTMLElement |
|||
sendError(`Resource load failed: ${(target as any).src || (target as any).href}`, "resource.error", { |
|||
tagName: target.tagName, |
|||
src: (target as any).src, |
|||
href: (target as any).href, |
|||
}) |
|||
} |
|||
}, |
|||
true, |
|||
) // 使用捕获阶段
|
|||
|
|||
console.info("[ErrorHandler] Global error handlers installed") |
|||
} |
|||
|
|||
return { |
|||
captureError: sendError, |
|||
setOptions: (newOptions: Partial<ErrorHandlerOptions>) => { |
|||
options = { ...options, ...newOptions } |
|||
// 同步选项到preload层
|
|||
if (preloadErrorHandler && typeof preloadErrorHandler.setOptions === "function") { |
|||
preloadErrorHandler.setOptions(options) |
|||
} |
|||
}, |
|||
getOptions: () => ({ ...options }), |
|||
installGlobalHandlers, |
|||
} |
|||
} |
|||
|
|||
// 导出类型定义,方便在渲染进程中使用
|
|||
export type { ErrorDetail, ErrorHandlerOptions } |
|||
|
|||
// 创建渲染进程错误处理器
|
|||
const errorHandler = createRendererErrorHandler() |
|||
|
|||
// 安装全局错误处理器
|
|||
errorHandler.installGlobalHandlers() |
|||
|
|||
window.errorHandler = errorHandler |
|||
|
|||
/** |
|||
* 使用示例: |
|||
* |
|||
* // 捕获特定错误
|
|||
* try { |
|||
* // 可能出错的代码
|
|||
* } catch (error) { |
|||
* errorHandler.captureError(error, 'ComponentName', { additionalInfo: 'value' }) |
|||
* } |
|||
* |
|||
* // 设置错误处理选项
|
|||
* errorHandler.setOptions({ |
|||
* namespace: 'custom-error', |
|||
* includeComponentInfo: true |
|||
* }) |
|||
*/ |
@ -0,0 +1,230 @@ |
|||
import fs from "fs-extra" |
|||
import { app } from "electron" |
|||
import path from "path" |
|||
import { cloneDeep } from "lodash" |
|||
import Config from "config" |
|||
import type { IDefaultConfig } from "config" |
|||
import _debug from "debug" |
|||
|
|||
const debug = _debug("app:setting") |
|||
|
|||
type IConfig = IDefaultConfig |
|||
|
|||
type IOnFunc = (n: IConfig, c: IConfig, keys?: (keyof IConfig)[]) => void |
|||
type IT = (keyof IConfig)[] | keyof IConfig | "_" |
|||
|
|||
let storagePath = path.join(app.getPath("documents"), Config.app_title) |
|||
const storagePathDev = path.join(app.getPath("documents"), Config.app_title + "-dev") |
|||
|
|||
if (process.env.NODE_ENV === "development") { |
|||
storagePath = storagePathDev |
|||
} |
|||
|
|||
const _tempConfig = cloneDeep(Config.default_config as IConfig) |
|||
Object.keys(_tempConfig).forEach(key => { |
|||
if (typeof _tempConfig[key] === "string" && _tempConfig[key].includes("$storagePath$")) { |
|||
_tempConfig[key] = _tempConfig[key].replace(/\$storagePath\$/g, storagePath) |
|||
if (_tempConfig[key] && path.isAbsolute(_tempConfig[key])) { |
|||
_tempConfig[key] = path.normalize(_tempConfig[key]) |
|||
} |
|||
} |
|||
}) |
|||
|
|||
function isPath(str) { |
|||
// 使用正则表达式检查字符串是否以斜杠或盘符开头
|
|||
return /^(?:\/|[a-zA-Z]:\\)/.test(str) |
|||
} |
|||
|
|||
function init(config: IConfig) { |
|||
// 在配置初始化后执行
|
|||
Object.keys(config).forEach(key => { |
|||
if (config[key] && isPath(config[key]) && path.isAbsolute(config[key])) { |
|||
fs.ensureDirSync(config[key]) |
|||
} |
|||
}) |
|||
// 在配置初始化后执行
|
|||
// fs.ensureDirSync(config["snippet.storagePath"])
|
|||
// fs.ensureDirSync(config["bookmark.storagePath"])
|
|||
} |
|||
|
|||
// 判断是否是空文件夹
|
|||
function isEmptyDir(fPath: string) { |
|||
const pa = fs.readdirSync(fPath) |
|||
if (pa.length === 0) { |
|||
return true |
|||
} else { |
|||
return false |
|||
} |
|||
} |
|||
|
|||
class SettingClass { |
|||
constructor() { |
|||
debug(`Setting inited`) |
|||
this.init() |
|||
} |
|||
|
|||
#cb: [IT, IOnFunc][] = [] |
|||
|
|||
onChange(fn: IOnFunc, that?: any) |
|||
onChange(key: IT, fn: IOnFunc, that?: any) |
|||
onChange(fnOrType: IT | IOnFunc, fnOrThat: IOnFunc | any = null, that: any = null) { |
|||
if (typeof fnOrType === "function") { |
|||
this.#cb.push(["_", fnOrType.bind(fnOrThat)]) |
|||
} else { |
|||
this.#cb.push([fnOrType, fnOrThat.bind(that)]) |
|||
} |
|||
} |
|||
|
|||
#runCB(n: IConfig, c: IConfig, keys: (keyof IConfig)[]) { |
|||
for (let i = 0; i < this.#cb.length; i++) { |
|||
const temp = this.#cb[i] |
|||
const k = temp[0] |
|||
const fn = temp[1] |
|||
if (k === "_") { |
|||
fn(n, c, keys) |
|||
} |
|||
if (typeof k === "string" && keys.includes(k as keyof IConfig)) { |
|||
fn(n, c) |
|||
} |
|||
if (Array.isArray(k) && k.filter(v => keys.indexOf(v) !== -1).length) { |
|||
fn(n, c) |
|||
} |
|||
} |
|||
} |
|||
|
|||
#pathFile: string = |
|||
process.env.NODE_ENV === "development" |
|||
? path.resolve(app.getPath("userData"), "./config_path-dev") |
|||
: path.resolve(app.getPath("userData"), "./config_path") |
|||
#config: IConfig = cloneDeep(_tempConfig) |
|||
#configPath(storagePath?: string): string { |
|||
return path.join(storagePath || this.#config.storagePath, "./config.json") |
|||
} |
|||
/** |
|||
* 读取配置文件变量同步 |
|||
* @param confingPath 配置文件路径 |
|||
*/ |
|||
#syncVar(confingPath?: string) { |
|||
const configFile = this.#configPath(confingPath) |
|||
if (!fs.pathExistsSync(configFile)) { |
|||
fs.ensureFileSync(configFile) |
|||
fs.writeJSONSync(configFile, {}) |
|||
} |
|||
const config = fs.readJSONSync(configFile) as IConfig |
|||
confingPath && (config.storagePath = confingPath) |
|||
// 优先取本地的值
|
|||
for (const key in config) { |
|||
// if (Object.prototype.hasOwnProperty.call(this.#config, key)) {
|
|||
// this.#config[key] = config[key] || this.#config[key]
|
|||
// }
|
|||
// 删除配置时本地的配置不会改变,想一下哪种方式更好
|
|||
this.#config[key] = config[key] ?? this.#config[key] |
|||
} |
|||
} |
|||
init() { |
|||
debug(`位置:${this.#pathFile}`) |
|||
|
|||
if (fs.pathExistsSync(this.#pathFile)) { |
|||
const confingPath = fs.readFileSync(this.#pathFile, { encoding: "utf8" }) |
|||
if (confingPath && fs.pathExistsSync(this.#configPath(confingPath))) { |
|||
this.#syncVar(confingPath) |
|||
// 防止增加了配置本地却没变的情况
|
|||
this.#sync(confingPath) |
|||
} else { |
|||
this.#syncVar(confingPath) |
|||
this.#sync(confingPath) |
|||
} |
|||
} else { |
|||
this.#syncVar() |
|||
this.#sync() |
|||
} |
|||
init.call(this, this.#config) |
|||
} |
|||
config() { |
|||
return this.#config |
|||
} |
|||
#sync(c?: string) { |
|||
const config = cloneDeep(this.#config) |
|||
delete config.storagePath |
|||
const p = this.#configPath(c) |
|||
fs.ensureFileSync(p) |
|||
fs.writeJSONSync(this.#configPath(c), config) |
|||
} |
|||
#change(p: string) { |
|||
const storagePath = this.#config.storagePath |
|||
if (fs.existsSync(storagePath) && !fs.existsSync(p)) { |
|||
fs.moveSync(storagePath, p) |
|||
} |
|||
if (fs.existsSync(p) && fs.existsSync(storagePath) && isEmptyDir(p)) { |
|||
fs.moveSync(storagePath, p, { overwrite: true }) |
|||
} |
|||
fs.writeFileSync(this.#pathFile, p, { encoding: "utf8" }) |
|||
} |
|||
reset(key: keyof IConfig) { |
|||
this.set(key, cloneDeep(_tempConfig[key])) |
|||
} |
|||
set(key: keyof IConfig | Partial<IConfig>, value?: any) { |
|||
const oldMainConfig = Object.assign({}, this.#config) |
|||
let isChange = false |
|||
const changeKeys: (keyof IConfig)[] = [] |
|||
const canChangeStorage = (targetPath: string) => { |
|||
if (fs.existsSync(oldMainConfig.storagePath) && fs.existsSync(targetPath) && !isEmptyDir(targetPath)) { |
|||
if (fs.existsSync(path.join(targetPath, "./config.json"))) { |
|||
return true |
|||
} |
|||
return false |
|||
} |
|||
return true |
|||
} |
|||
if (typeof key === "string") { |
|||
if (value != undefined && value !== this.#config[key]) { |
|||
if (key === "storagePath") { |
|||
if (!canChangeStorage(value)) { |
|||
throw "无法改变存储地址" |
|||
return |
|||
} |
|||
this.#change(value) |
|||
changeKeys.push("storagePath") |
|||
this.#config["storagePath"] = value |
|||
} else { |
|||
changeKeys.push(key) |
|||
this.#config[key as string] = value |
|||
} |
|||
isChange = true |
|||
} |
|||
} else { |
|||
if (key["storagePath"] !== undefined && key["storagePath"] !== this.#config["storagePath"]) { |
|||
if (!canChangeStorage(key["storagePath"])) { |
|||
throw "无法改变存储地址" |
|||
return |
|||
} |
|||
this.#change(key["storagePath"]) |
|||
this.#config["storagePath"] = key["storagePath"] |
|||
changeKeys.push("storagePath") |
|||
isChange = true |
|||
} |
|||
for (const _ in key) { |
|||
if (Object.prototype.hasOwnProperty.call(key, _)) { |
|||
const v = key[_] |
|||
if (v != undefined && _ !== "storagePath" && v !== this.#config[_]) { |
|||
this.#config[_] = v |
|||
changeKeys.push(_ as keyof IConfig) |
|||
isChange = true |
|||
} |
|||
} |
|||
} |
|||
} |
|||
if (isChange) { |
|||
this.#sync() |
|||
this.#runCB(this.#config, oldMainConfig, changeKeys) |
|||
} |
|||
} |
|||
values<T extends keyof IConfig>(key: T): IConfig[T] { |
|||
return this.#config[key] |
|||
} |
|||
} |
|||
|
|||
const Setting = new SettingClass() |
|||
|
|||
export default Setting |
|||
export { Setting } |
@ -0,0 +1,7 @@ |
|||
{ |
|||
"name": "setting", |
|||
"version": "1.0.0", |
|||
"keywords": [], |
|||
"author": "", |
|||
"license": "ISC" |
|||
} |
@ -1,11 +1,12 @@ |
|||
<!DOCTYPE html> |
|||
<!doctype html> |
|||
<html lang="en"> |
|||
<head> |
|||
<meta charset="UTF-8"> |
|||
<meta name="viewport" content="width=device-width, initial-scale=1.0"> |
|||
<head> |
|||
<meta charset="UTF-8" /> |
|||
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> |
|||
<title>Document</title> |
|||
</head> |
|||
<body> |
|||
前往 <a href="https://baidu.com" target="_blank">百度</a> |
|||
</body> |
|||
</head> |
|||
<body> |
|||
前往 |
|||
<a href="https://baidu.com" target="_blank">百度</a> |
|||
</body> |
|||
</html> |
|||
|
@ -0,0 +1,17 @@ |
|||
import { Container, ContainerModule } from "inversify" |
|||
import UpdateCommand from "common/event/Update/main/command" |
|||
import PlatFormCommand from "common/event/PlatForm/main/command" |
|||
import TabsCommand from "common/event/Tabs/main/command" |
|||
|
|||
const modules = new ContainerModule(bind => { |
|||
bind("TabsCommand").to(TabsCommand).inSingletonScope() |
|||
bind("PlatFormCommand").to(PlatFormCommand).inSingletonScope() |
|||
bind("UpdateCommand").to(UpdateCommand).inSingletonScope() |
|||
}) |
|||
|
|||
async function destroyAllCommand(ioc: Container) { |
|||
await ioc.unloadAsync(modules) |
|||
} |
|||
|
|||
export { modules, destroyAllCommand } |
|||
export default modules |
@ -0,0 +1,5 @@ |
|||
import { PlatForm } from "." |
|||
|
|||
export function usePlatForm() { |
|||
return PlatForm.getInstance<PlatForm>() |
|||
} |
@ -0,0 +1,48 @@ |
|||
import { _Base } from "common/lib/_Base" |
|||
import { ApiFactory } from "common/lib/abstract" |
|||
import { LogLevel } from "packages/logger/common" |
|||
|
|||
class PlatForm extends _Base { |
|||
constructor() { |
|||
super() |
|||
} |
|||
|
|||
private get api() { |
|||
return ApiFactory.getApiClient() |
|||
} |
|||
|
|||
async logSetLevel(level: LogLevel) { |
|||
return this.api.call("PlatFormCommand.logSetLevel", level) |
|||
} |
|||
|
|||
async logGetLevel() { |
|||
return this.api.call("PlatFormCommand.logGetLevel") |
|||
} |
|||
|
|||
async showAbout() { |
|||
// return this.api.call("BasicService.showAbout")
|
|||
return this.api.call("PlatFormCommand.showAbout") |
|||
} |
|||
|
|||
async crash() { |
|||
return this.api.call("PlatFormCommand.crash") |
|||
} |
|||
|
|||
async isFullScreen() { |
|||
return this.api.call("PlatFormCommand.isFullscreen") |
|||
} |
|||
|
|||
async toggleFullScreen() { |
|||
return this.api.call("PlatFormCommand.fullscreen") |
|||
} |
|||
|
|||
async reload() { |
|||
return this.api.call("PlatFormCommand.reload") |
|||
} |
|||
|
|||
async toggleDevTools() { |
|||
return this.api.call("PlatFormCommand.toggleDevTools") |
|||
} |
|||
} |
|||
|
|||
export { PlatForm } |
@ -0,0 +1,120 @@ |
|||
import { app, dialog, nativeTheme, TitleBarOverlayOptions } from "electron" |
|||
import { inject } from "inversify" |
|||
import errorHandler from "logger/main-error" |
|||
import Tabs from "main/modules/tabs" |
|||
import WindowManager from "main/modules/window-manager" |
|||
import { getFileUrl } from "main/utils" |
|||
import icon from "@res/icon.png?asset" |
|||
import setting from "setting/main" |
|||
import { LogLevel } from "logger/common" |
|||
|
|||
export default class PlatFormCommand { |
|||
constructor( |
|||
@inject(WindowManager) private _WindowManager: WindowManager, |
|||
@inject(Tabs) private _Tabs: Tabs, |
|||
) {} |
|||
|
|||
setTheme(theme: typeof nativeTheme.themeSource) { |
|||
nativeTheme.themeSource = theme |
|||
} |
|||
|
|||
logSetLevel(level: LogLevel) { |
|||
return setting.set("debug", level) |
|||
} |
|||
|
|||
logGetLevel() { |
|||
return setting.values("debug") |
|||
} |
|||
|
|||
setTitlBar(options: TitleBarOverlayOptions) { |
|||
const mainWindow = this._WindowManager.getMainWindow() |
|||
if (mainWindow) { |
|||
mainWindow.setTitleBarOverlay(options) |
|||
} |
|||
} |
|||
|
|||
showAbout() { |
|||
this._WindowManager.createWindow("about", { |
|||
url: getFileUrl("about.html"), |
|||
overideWindowOpts: true, |
|||
confrimWindowClose: false, |
|||
type: "info", |
|||
windowOpts: { |
|||
width: 600, |
|||
height: 400, |
|||
minimizable: false, |
|||
darkTheme: true, |
|||
modal: true, |
|||
show: false, |
|||
resizable: false, |
|||
icon: icon, |
|||
webPreferences: { |
|||
devTools: false, |
|||
sandbox: false, |
|||
nodeIntegration: false, |
|||
contextIsolation: true, |
|||
}, |
|||
}, |
|||
}) |
|||
} |
|||
|
|||
toggleDevTools() { |
|||
const focusedWindow = this._WindowManager.getFocusWindow() |
|||
if (focusedWindow) { |
|||
// @ts-ignore ...
|
|||
focusedWindow.toggleDevTools() |
|||
} |
|||
} |
|||
fullscreen() { |
|||
const focusedWindow = this._WindowManager.getFocusWindow() |
|||
if (focusedWindow) { |
|||
const isFullScreen = focusedWindow!.isFullScreen() |
|||
focusedWindow!.setFullScreen(!isFullScreen) |
|||
} |
|||
} |
|||
|
|||
crash() { |
|||
errorHandler.captureError(new Error("手动触发的崩溃")) |
|||
process.crash() |
|||
} |
|||
|
|||
isFullscreen() { |
|||
const focusedWindow = this._WindowManager.getFocusWindow() |
|||
if (focusedWindow) { |
|||
return focusedWindow!.isFullScreen() |
|||
} |
|||
return false |
|||
} |
|||
|
|||
relunch() { |
|||
app.relaunch() |
|||
app.exit() |
|||
} |
|||
|
|||
reload() { |
|||
const focusedWindow = this._WindowManager.getFocusWindow() |
|||
// 重载之后, 刷新并关闭所有的次要窗体
|
|||
if (this._WindowManager.length() > 1 && focusedWindow && focusedWindow.$$opts!.name === this._WindowManager.mainInfo.name) { |
|||
const choice = dialog.showMessageBoxSync(focusedWindow, { |
|||
type: "question", |
|||
buttons: ["取消", "是的,继续", "不,算了"], |
|||
title: "警告", |
|||
defaultId: 2, |
|||
cancelId: 0, |
|||
message: "警告", |
|||
detail: "重载主窗口将关闭所有子窗口,是否继续", |
|||
}) |
|||
if (choice == 1) { |
|||
this._WindowManager.getWndows().forEach(win => { |
|||
if (win.$$opts!.name !== this._WindowManager.mainInfo.name) { |
|||
win.close() |
|||
} |
|||
}) |
|||
} else { |
|||
return |
|||
} |
|||
} |
|||
this._Tabs.closeAll() |
|||
focusedWindow!.reload() |
|||
} |
|||
} |
@ -0,0 +1,54 @@ |
|||
import { _Base } from "../../lib/_Base" |
|||
|
|||
export class Tabs extends _Base { |
|||
constructor() { |
|||
super() |
|||
} |
|||
|
|||
private isListen: boolean = false |
|||
|
|||
private execUpdate = (...args) => { |
|||
this.#fnList.forEach(v => v(...args)) |
|||
} |
|||
|
|||
#fnList: ((...args) => void)[] = [] |
|||
listenUpdate(cb: (...args) => void) { |
|||
if (!this.isListen) { |
|||
api.on("main:TabsCommand.update", this.execUpdate) |
|||
this.isListen = true |
|||
} |
|||
this.#fnList.push(cb) |
|||
} |
|||
|
|||
unListenUpdate(fn: (...args) => void) { |
|||
this.#fnList = this.#fnList.filter(v => { |
|||
return v !== fn |
|||
}) |
|||
if (!this.#fnList.length) { |
|||
api.off("main:TabsCommand.update", this.execUpdate) |
|||
this.isListen = false |
|||
} |
|||
} |
|||
|
|||
bindPosition(data) { |
|||
api.call("TabsCommand.bindElement", data) |
|||
} |
|||
|
|||
closeAll() { |
|||
api.call("TabsCommand.closeAll") |
|||
} |
|||
|
|||
sync() { |
|||
api.call("TabsCommand.sync") |
|||
} |
|||
|
|||
unListenerAll() { |
|||
this.#fnList = [] |
|||
api.offAll("main:TabsCommand.update") |
|||
} |
|||
|
|||
async getAllTabs() { |
|||
const res = await api.call("TabsCommand.getAllTabs") |
|||
return res |
|||
} |
|||
} |
@ -0,0 +1,65 @@ |
|||
import { inject } from "inversify" |
|||
import Tabs from "main/modules/tabs" |
|||
import WindowManager from "main/modules/window-manager" |
|||
import { broadcast } from "main/utils" |
|||
|
|||
class TabsCommand { |
|||
constructor( |
|||
@inject(Tabs) private _Tabs: Tabs, |
|||
@inject(WindowManager) private _WindowManager: WindowManager, |
|||
) { |
|||
this._Tabs.events.on("update", this.listenerTabActive) |
|||
} |
|||
|
|||
listenerTabActive = () => { |
|||
broadcast("main:TabsCommand.update", this.getAllTabs()) |
|||
} |
|||
|
|||
bindElement(rect) { |
|||
this._Tabs.updateRect(rect) |
|||
} |
|||
|
|||
reload() { |
|||
this._WindowManager.getMainWindow()?.reload() |
|||
} |
|||
|
|||
sync() { |
|||
this.listenerTabActive() |
|||
if (!this.getAllTabs().length) { |
|||
this.add("about:blank") |
|||
} |
|||
} |
|||
|
|||
add(url) { |
|||
this._Tabs.add(url, true, this._WindowManager.getMainWindow()!) |
|||
} |
|||
|
|||
nagivate(index: number, url: string) { |
|||
this._Tabs.navigate(+index, url) |
|||
} |
|||
|
|||
closeAll() { |
|||
this._Tabs.closeAll() |
|||
} |
|||
|
|||
setActive(index) { |
|||
this._Tabs.changeActive(index) |
|||
} |
|||
|
|||
closeTab(e) { |
|||
this._Tabs.remove(e.body.active) |
|||
} |
|||
|
|||
getAllTabs() { |
|||
return this._Tabs._tabs.map(v => ({ |
|||
url: v.url, |
|||
showUrl: v.showUrl, |
|||
title: v.title, |
|||
favicons: v.favicons, |
|||
isActive: v.isActive, |
|||
})) |
|||
} |
|||
} |
|||
|
|||
export { TabsCommand } |
|||
export default TabsCommand |
@ -0,0 +1,5 @@ |
|||
const keys = ["hot-update-ready"] as const |
|||
|
|||
type AllKeys = (typeof keys)[number] |
|||
|
|||
export type { AllKeys } |
@ -0,0 +1,14 @@ |
|||
// import type { AllKeys } from "../common"
|
|||
|
|||
const curProgress = ref(0) |
|||
// api.on<AllKeys>("progress", () => {
|
|||
// curProgress.value = 10
|
|||
// })
|
|||
|
|||
function useUpdate() { |
|||
return { |
|||
curProgress, |
|||
} |
|||
} |
|||
|
|||
export { useUpdate } |
@ -0,0 +1,10 @@ |
|||
import { inject } from "inversify" |
|||
import Updater from "main/modules/updater" |
|||
|
|||
export default class PlatFormCommand { |
|||
constructor(@inject(Updater) private _Updater: Updater) {} |
|||
|
|||
async triggerHotUpdate() { |
|||
await this._Updater.triggerHotUpdate() |
|||
} |
|||
} |
@ -0,0 +1,8 @@ |
|||
import { broadcast } from "main/utils" |
|||
import { AllKeys } from "common/event/common" |
|||
|
|||
function emitHotUpdateReady(...argu) { |
|||
broadcast<AllKeys>("hot-update-ready", ...argu) |
|||
} |
|||
|
|||
export { emitHotUpdateReady } |
@ -0,0 +1,12 @@ |
|||
export abstract class _Base { |
|||
static instance |
|||
|
|||
static getInstance<T>(): T { |
|||
if (!this.instance) { |
|||
// 如果实例不存在,则创建一个新的实例
|
|||
// @ts-ignore ...
|
|||
this.instance = new this() |
|||
} |
|||
return this.instance |
|||
} |
|||
} |
@ -0,0 +1,53 @@ |
|||
import { ElectronApiClient } from "common/lib/electron" |
|||
import { BrowserApiClient } from "common/lib/browser" |
|||
|
|||
// 定义抽象 API 接口
|
|||
export interface IApiClient { |
|||
call<T = any>(command: string, ...args: any[]): Promise<T> |
|||
on<K extends string>(channel: K, callback: (...args: any[]) => void): void |
|||
off<K extends string>(channel: K, callback: (...args: any[]) => void): void |
|||
offAll<K extends string>(channel: K): void |
|||
} |
|||
|
|||
class NullApiClient implements IApiClient { |
|||
async call<T = any>(command: string, ...args: any[]): Promise<T> { |
|||
args |
|||
console.warn(`API call to ${command} failed: API client not initialized`) |
|||
return undefined as any |
|||
} |
|||
|
|||
on<K extends string>(channel: K, callback: (...args: any[]) => void): void { |
|||
callback |
|||
console.warn(`Failed to register listener for ${channel}: API client not initialized`) |
|||
} |
|||
|
|||
off<K extends string>(channel: K, callback: (...args: any[]) => void): void { |
|||
callback |
|||
console.warn(`Failed to unregister listener for ${channel}: API client not initialized`) |
|||
} |
|||
|
|||
offAll<K extends string>(channel: K): void { |
|||
console.warn(`Failed to unregister all listeners for ${channel}: API client not initialized`) |
|||
} |
|||
} |
|||
|
|||
// 创建 API 工厂
|
|||
export class ApiFactory { |
|||
private static instance: IApiClient = new NullApiClient() // 默认使用空实现
|
|||
|
|||
static setApiClient(client: IApiClient) { |
|||
this.instance = client |
|||
} |
|||
|
|||
static getApiClient(): IApiClient { |
|||
if (this.instance instanceof NullApiClient) { |
|||
// 根据环境选择合适的 API 客户端
|
|||
if (window.api && window.electron) { |
|||
this.instance = new ElectronApiClient() |
|||
} else { |
|||
this.instance = new BrowserApiClient() |
|||
} |
|||
} |
|||
return this.instance |
|||
} |
|||
} |
@ -0,0 +1,29 @@ |
|||
import { IApiClient } from "./abstract" |
|||
|
|||
export class BrowserApiClient implements IApiClient { |
|||
call<T = any>(command: string, ...args: any[]): Promise<T> { |
|||
// 浏览器特定实现,可能使用 fetch 或其他方式
|
|||
const [service, method] = command.split(".") |
|||
return fetch(`/api/${service}/${method}`, { |
|||
method: "POST", |
|||
body: JSON.stringify(args), |
|||
headers: { "Content-Type": "application/json" }, |
|||
}).then(res => res.json()) |
|||
} |
|||
|
|||
// 实现其他方法...
|
|||
on<K extends string>(channel: K, callback: (...args: any[]) => void): void { |
|||
// 浏览器中可能使用 WebSocket 或其他方式
|
|||
console.log("不支持 on 方法", channel, callback) |
|||
} |
|||
|
|||
off<K extends string>(channel: K, callback: (...args: any[]) => void): void { |
|||
// 相应的解绑实现
|
|||
console.log("不支持 on 方法", channel, callback) |
|||
} |
|||
|
|||
offAll<K extends string>(channel: K): void { |
|||
// 相应的全部解绑实现
|
|||
console.log("不支持 on 方法", channel) |
|||
} |
|||
} |
@ -0,0 +1,20 @@ |
|||
import { IApiClient } from "./abstract" |
|||
|
|||
export class ElectronApiClient implements IApiClient { |
|||
call<T = any>(command: string, ...args: any[]): Promise<T> { |
|||
// Electron 特定实现
|
|||
return window.api.call(command, ...args) |
|||
} |
|||
|
|||
on<K extends string>(channel: K, callback: (...args: any[]) => void): void { |
|||
window.api.on(channel, callback) |
|||
} |
|||
|
|||
off<K extends string>(channel: K, callback: (...args: any[]) => void): void { |
|||
window.api.off(channel, callback) |
|||
} |
|||
|
|||
offAll<K extends string>(channel: K): void { |
|||
window.api.offAll(channel) |
|||
} |
|||
} |
@ -1,63 +0,0 @@ |
|||
import { app, dialog } from "electron" |
|||
import { inject } from "inversify" |
|||
import Tabs from "main/modules/tabs" |
|||
import WindowManager from "main/modules/window-manager" |
|||
|
|||
export default class BasicCommand { |
|||
constructor( |
|||
@inject(WindowManager) private _WindowManager: WindowManager, |
|||
@inject(Tabs) private _Tabs: Tabs, |
|||
) { |
|||
//
|
|||
} |
|||
|
|||
toggleDevTools() { |
|||
const focusedWindow = this._WindowManager.getFocusWindow() |
|||
if (focusedWindow) { |
|||
// @ts-ignore ...
|
|||
focusedWindow.toggleDevTools() |
|||
} |
|||
} |
|||
fullscreen() { |
|||
const focusedWindow = this._WindowManager.getFocusWindow() |
|||
const isFullScreen = focusedWindow!.isFullScreen() |
|||
focusedWindow!.setFullScreen(!isFullScreen) |
|||
return !isFullScreen |
|||
} |
|||
isFullscreen() { |
|||
const focusedWindow = this._WindowManager.getFocusWindow() |
|||
return focusedWindow!.isFullScreen() |
|||
} |
|||
|
|||
relunch() { |
|||
app.relaunch() |
|||
app.exit() |
|||
} |
|||
|
|||
reload() { |
|||
const focusedWindow = this._WindowManager.getFocusWindow() |
|||
// 重载之后, 刷新并关闭所有的次要窗体
|
|||
if (this._WindowManager.length() > 1 && focusedWindow && focusedWindow.$$opts!.name === this._WindowManager.mainInfo.name) { |
|||
const choice = dialog.showMessageBoxSync(focusedWindow, { |
|||
type: "question", |
|||
buttons: ["取消", "是的,继续", "不,算了"], |
|||
title: "警告", |
|||
defaultId: 2, |
|||
cancelId: 0, |
|||
message: "警告", |
|||
detail: "重载主窗口将关闭所有子窗口,是否继续", |
|||
}) |
|||
if (choice == 1) { |
|||
this._WindowManager.getWndows().forEach(win => { |
|||
if (win.$$opts!.name !== this._WindowManager.mainInfo.name) { |
|||
win.close() |
|||
} |
|||
}) |
|||
} else { |
|||
return |
|||
} |
|||
} |
|||
this._Tabs.closeAll() |
|||
focusedWindow!.reload() |
|||
} |
|||
} |
@ -1,66 +0,0 @@ |
|||
import { inject } from "inversify" |
|||
import Tabs from "main/modules/tabs" |
|||
import WindowManager from "main/modules/window-manager" |
|||
import { broadcast } from "main/utils" |
|||
|
|||
class TabsCommand { |
|||
constructor( |
|||
@inject(Tabs) private _Tabs: Tabs, |
|||
@inject(WindowManager) private _WindowManager: WindowManager, |
|||
) { |
|||
this.listenerTabActive = this.listenerTabActive.bind(this) |
|||
this._Tabs.events.on("update", this.listenerTabActive) |
|||
} |
|||
|
|||
listenerTabActive() { |
|||
broadcast("main:TabsCommand.update", this.getAllTabs()) |
|||
} |
|||
|
|||
bindElement(rect) { |
|||
this._Tabs.updateRect(rect) |
|||
} |
|||
|
|||
reload() { |
|||
this._WindowManager.getMainWindow()?.reload() |
|||
} |
|||
|
|||
sync() { |
|||
this.listenerTabActive() |
|||
if (!this.getAllTabs().length) { |
|||
this.add("about:blank") |
|||
} |
|||
} |
|||
|
|||
add(url) { |
|||
this._Tabs.add(url, true, this._WindowManager.getMainWindow()!) |
|||
} |
|||
|
|||
nagivate(index: number, url: string) { |
|||
this._Tabs.navigate(+index, url) |
|||
} |
|||
|
|||
closeAll() { |
|||
this._Tabs.closeAll() |
|||
} |
|||
|
|||
setActive(index) { |
|||
this._Tabs.changeActive(index) |
|||
} |
|||
|
|||
closeTab(e) { |
|||
this._Tabs.remove(e.body.active) |
|||
} |
|||
|
|||
getAllTabs() { |
|||
return this._Tabs._tabs.map(v => ({ |
|||
url: v.url, |
|||
showUrl: v.showUrl, |
|||
title: v.title, |
|||
favicons: v.favicons, |
|||
isActive: v.isActive, |
|||
})) |
|||
} |
|||
} |
|||
|
|||
export { TabsCommand } |
|||
export default TabsCommand |
@ -1,15 +0,0 @@ |
|||
import { Container, ContainerModule } from "inversify" |
|||
import BasicCommand from "./BasicCommand" |
|||
import TabsCommand from "./TabsCommand" |
|||
|
|||
const modules = new ContainerModule(bind => { |
|||
bind("BasicCommand").to(BasicCommand).inSingletonScope() |
|||
bind("TabsCommand").to(TabsCommand).inSingletonScope() |
|||
}) |
|||
|
|||
async function destroyAllCommand(ioc: Container) { |
|||
await ioc.unloadAsync(modules) |
|||
} |
|||
|
|||
export { modules, destroyAllCommand } |
|||
export default modules |
@ -1,235 +0,0 @@ |
|||
import fs from "fs-extra" |
|||
import { app } from "electron" |
|||
import path from "path" |
|||
import { cloneDeep } from "lodash" |
|||
import { injectable } from "inversify" |
|||
import Config from "config" |
|||
import _debug from "debug" |
|||
import BaseClass from "main/base/base" |
|||
|
|||
const debug = _debug("app:setting") |
|||
|
|||
type IConfig = typeof Config.default_config |
|||
|
|||
type IOnFunc = (n: IConfig, c: IConfig, keys?: (keyof IConfig)[]) => void |
|||
type IT = (keyof IConfig)[] | keyof IConfig | "_" |
|||
|
|||
let storagePath = path.join(app.getPath("documents"), Config.app_title) |
|||
const storagePathDev = path.join(app.getPath("documents"), Config.app_title + "-dev") |
|||
|
|||
if (process.env.NODE_ENV === "development") { |
|||
storagePath = storagePathDev |
|||
} |
|||
|
|||
const _tempConfig = cloneDeep(Config.default_config as IConfig) |
|||
Object.keys(_tempConfig).forEach(key => { |
|||
if (typeof _tempConfig[key] === "string" && _tempConfig[key].includes("$storagePath$")) { |
|||
_tempConfig[key] = _tempConfig[key].replace(/\$storagePath\$/g, storagePath) |
|||
if (_tempConfig[key] && path.isAbsolute(_tempConfig[key])) { |
|||
_tempConfig[key] = path.normalize(_tempConfig[key]) |
|||
} |
|||
} |
|||
}) |
|||
|
|||
function isPath(str) { |
|||
// 使用正则表达式检查字符串是否以斜杠或盘符开头
|
|||
return /^(?:\/|[a-zA-Z]:\\)/.test(str) |
|||
} |
|||
|
|||
function init(config: IConfig) { |
|||
// 在配置初始化后执行
|
|||
Object.keys(config).forEach(key => { |
|||
if (config[key] && isPath(config[key]) && path.isAbsolute(config[key])) { |
|||
fs.ensureDirSync(config[key]) |
|||
} |
|||
}) |
|||
// 在配置初始化后执行
|
|||
// fs.ensureDirSync(config["snippet.storagePath"])
|
|||
// fs.ensureDirSync(config["bookmark.storagePath"])
|
|||
} |
|||
|
|||
// 判断是否是空文件夹
|
|||
function isEmptyDir(fPath: string) { |
|||
const pa = fs.readdirSync(fPath) |
|||
if (pa.length === 0) { |
|||
return true |
|||
} else { |
|||
return false |
|||
} |
|||
} |
|||
|
|||
@injectable() |
|||
class Setting extends BaseClass { |
|||
constructor() { |
|||
super() |
|||
debug(`Setting inited`) |
|||
this.init() |
|||
} |
|||
|
|||
destroy() { |
|||
// TODO
|
|||
} |
|||
|
|||
#cb: [IT, IOnFunc][] = [] |
|||
|
|||
onChange(fn: IOnFunc, that?: any) |
|||
onChange(key: IT, fn: IOnFunc, that?: any) |
|||
onChange(fnOrType: IT | IOnFunc, fnOrThat: IOnFunc | any = null, that: any = null) { |
|||
if (typeof fnOrType === "function") { |
|||
this.#cb.push(["_", fnOrType.bind(fnOrThat)]) |
|||
} else { |
|||
this.#cb.push([fnOrType, fnOrThat.bind(that)]) |
|||
} |
|||
} |
|||
|
|||
#runCB(n: IConfig, c: IConfig, keys: (keyof IConfig)[]) { |
|||
for (let i = 0; i < this.#cb.length; i++) { |
|||
const temp = this.#cb[i] |
|||
const k = temp[0] |
|||
const fn = temp[1] |
|||
if (k === "_") { |
|||
fn(n, c, keys) |
|||
} |
|||
if (typeof k === "string" && keys.includes(k as keyof IConfig)) { |
|||
fn(n, c) |
|||
} |
|||
if (Array.isArray(k) && k.filter(v => keys.indexOf(v) !== -1).length) { |
|||
fn(n, c) |
|||
} |
|||
} |
|||
} |
|||
|
|||
#pathFile: string = |
|||
process.env.NODE_ENV === "development" |
|||
? path.resolve(app.getPath("userData"), "./config_path-dev") |
|||
: path.resolve(app.getPath("userData"), "./config_path") |
|||
#config: IConfig = cloneDeep(_tempConfig) |
|||
#configPath(storagePath?: string): string { |
|||
return path.join(storagePath || this.#config.storagePath, "./config.json") |
|||
} |
|||
/** |
|||
* 读取配置文件变量同步 |
|||
* @param confingPath 配置文件路径 |
|||
*/ |
|||
#syncVar(confingPath?: string) { |
|||
const configFile = this.#configPath(confingPath) |
|||
if (!fs.pathExistsSync(configFile)) { |
|||
fs.ensureFileSync(configFile) |
|||
fs.writeJSONSync(configFile, {}) |
|||
} |
|||
const config = fs.readJSONSync(configFile) as IConfig |
|||
confingPath && (config.storagePath = confingPath) |
|||
// 优先取本地的值
|
|||
for (const key in config) { |
|||
// if (Object.prototype.hasOwnProperty.call(this.#config, key)) {
|
|||
// this.#config[key] = config[key] || this.#config[key]
|
|||
// }
|
|||
// 删除配置时本地的配置不会改变,想一下哪种方式更好
|
|||
this.#config[key] = config[key] || this.#config[key] |
|||
} |
|||
} |
|||
init() { |
|||
debug(`位置:${this.#pathFile}`) |
|||
|
|||
if (fs.pathExistsSync(this.#pathFile)) { |
|||
const confingPath = fs.readFileSync(this.#pathFile, { encoding: "utf8" }) |
|||
if (confingPath && fs.pathExistsSync(this.#configPath(confingPath))) { |
|||
this.#syncVar(confingPath) |
|||
// 防止增加了配置本地却没变的情况
|
|||
this.#sync(confingPath) |
|||
} else { |
|||
this.#syncVar(confingPath) |
|||
this.#sync(confingPath) |
|||
} |
|||
} else { |
|||
this.#syncVar() |
|||
this.#sync() |
|||
} |
|||
init.call(this, this.#config) |
|||
} |
|||
config() { |
|||
return this.#config |
|||
} |
|||
#sync(c?: string) { |
|||
const config = cloneDeep(this.#config) |
|||
delete config.storagePath |
|||
const p = this.#configPath(c) |
|||
fs.ensureFileSync(p) |
|||
fs.writeJSONSync(this.#configPath(c), config) |
|||
} |
|||
#change(p: string) { |
|||
const storagePath = this.#config.storagePath |
|||
if (fs.existsSync(storagePath) && !fs.existsSync(p)) { |
|||
fs.moveSync(storagePath, p) |
|||
} |
|||
if (fs.existsSync(p) && fs.existsSync(storagePath) && isEmptyDir(p)) { |
|||
fs.moveSync(storagePath, p, { overwrite: true }) |
|||
} |
|||
fs.writeFileSync(this.#pathFile, p, { encoding: "utf8" }) |
|||
} |
|||
reset(key: keyof IConfig) { |
|||
this.set(key, cloneDeep(_tempConfig[key])) |
|||
} |
|||
set(key: keyof IConfig | Partial<IConfig>, value?: any) { |
|||
const oldMainConfig = Object.assign({}, this.#config) |
|||
let isChange = false |
|||
const changeKeys: (keyof IConfig)[] = [] |
|||
const canChangeStorage = (targetPath: string) => { |
|||
if (fs.existsSync(oldMainConfig.storagePath) && fs.existsSync(targetPath) && !isEmptyDir(targetPath)) { |
|||
if (fs.existsSync(path.join(targetPath, "./config.json"))) { |
|||
return true |
|||
} |
|||
return false |
|||
} |
|||
return true |
|||
} |
|||
if (typeof key === "string") { |
|||
if (value != undefined && value !== this.#config[key]) { |
|||
if (key === "storagePath") { |
|||
if (!canChangeStorage(value)) { |
|||
throw "无法改变存储地址" |
|||
return |
|||
} |
|||
this.#change(value) |
|||
changeKeys.push("storagePath") |
|||
this.#config["storagePath"] = value |
|||
} else { |
|||
changeKeys.push(key) |
|||
this.#config[key as string] = value |
|||
} |
|||
isChange = true |
|||
} |
|||
} else { |
|||
if (key["storagePath"] !== undefined && key["storagePath"] !== this.#config["storagePath"]) { |
|||
if (!canChangeStorage(key["storagePath"])) { |
|||
throw "无法改变存储地址" |
|||
return |
|||
} |
|||
this.#change(key["storagePath"]) |
|||
this.#config["storagePath"] = key["storagePath"] |
|||
changeKeys.push("storagePath") |
|||
isChange = true |
|||
} |
|||
for (const _ in key) { |
|||
if (Object.prototype.hasOwnProperty.call(key, _)) { |
|||
const v = key[_] |
|||
if (v != undefined && _ !== "storagePath" && v !== this.#config[_]) { |
|||
this.#config[_] = v |
|||
changeKeys.push(_ as keyof IConfig) |
|||
isChange = true |
|||
} |
|||
} |
|||
} |
|||
} |
|||
if (isChange) { |
|||
this.#sync() |
|||
this.#runCB(this.#config, oldMainConfig, changeKeys) |
|||
} |
|||
} |
|||
values<T extends keyof IConfig>(key: T): IConfig[T] { |
|||
return this.#config[key] |
|||
} |
|||
} |
|||
|
|||
export default Setting |
|||
export { Setting } |
@ -0,0 +1,118 @@ |
|||
import { spawn } from "node:child_process" |
|||
import fs from "node:fs" |
|||
import path from "node:path" |
|||
import os from "node:os" |
|||
import { app } from "electron" |
|||
import extract from "extract-zip" |
|||
import { emitHotUpdateReady } from "common/event/Update/main" |
|||
|
|||
import _debug from "debug" |
|||
const debug = _debug("app:hot-updater") |
|||
|
|||
function getUpdateScriptTemplate() { |
|||
return process.platform === "win32" |
|||
? ` |
|||
@echo off |
|||
timeout /t 2 |
|||
taskkill /IM "{{EXE_NAME}}" /F |
|||
xcopy /Y /E "{{UPDATE_DIR}}\\*" "{{APP_PATH}}" |
|||
start "" "{{EXE_PATH}}" |
|||
` |
|||
: ` |
|||
#!/bin/bash |
|||
sleep 2 |
|||
pkill -f "{{EXE_NAME}}" |
|||
cp -Rf "{{UPDATE_DIR}}/*" "{{APP_PATH}}/" |
|||
open "{{EXE_PATH}}" |
|||
` |
|||
} |
|||
|
|||
function generateUpdateScript() { |
|||
const scriptContent = getUpdateScriptTemplate() |
|||
.replace(/{{APP_PATH}}/g, process.platform === "win32" ? "%APP_PATH%" : "$APP_PATH") |
|||
.replace(/{{UPDATE_DIR}}/g, process.platform === "win32" ? "%UPDATE_DIR%" : "$UPDATE_DIR") |
|||
.replace(/{{EXE_PATH}}/g, process.platform === "win32" ? "%EXE_PATH%" : "$EXE_PATH") |
|||
.replace(/{{EXE_NAME}}/g, process.platform === "win32" ? "%EXE_NAME%" : "$EXE_NAME") |
|||
|
|||
const scriptPath = path.join(os.tmpdir(), `update.${process.platform === "win32" ? "bat" : "sh"}`) |
|||
fs.writeFileSync(scriptPath, scriptContent) |
|||
return scriptPath |
|||
} |
|||
// 标记是否需要热更新
|
|||
let shouldPerformHotUpdate = false |
|||
let isReadyUpdate = false |
|||
// 更新临时目录路径
|
|||
// 使用应用名称和随机字符串创建唯一的临时目录
|
|||
const updateTempDirPath = path.join(os.tmpdir(), `${app.getName()}-update-${Math.random().toString(36).substring(2, 15)}`) |
|||
app.once("will-quit", event => { |
|||
if (!shouldPerformHotUpdate) return |
|||
event.preventDefault() |
|||
const appPath = app.getAppPath() |
|||
const appExePath = process.execPath |
|||
const exeName = path.basename(appExePath) |
|||
// 生成动态脚本
|
|||
const scriptPath = generateUpdateScript() |
|||
|
|||
fs.chmodSync(scriptPath, 0o755) |
|||
|
|||
// 执行脚本
|
|||
const child = spawn(scriptPath, [], { |
|||
detached: true, |
|||
shell: true, |
|||
env: { |
|||
APP_PATH: appPath, |
|||
UPDATE_DIR: updateTempDirPath, |
|||
EXE_PATH: appExePath, |
|||
EXE_NAME: exeName, |
|||
}, |
|||
}) |
|||
child.unref() |
|||
app.exit() |
|||
}) |
|||
|
|||
// 下载热更新包
|
|||
export async function fetchHotUpdatePackage(updatePackageUrl: string = "https://example.com/updates/latest.zip") { |
|||
if (isReadyUpdate) return |
|||
|
|||
// 清除临时目录
|
|||
clearUpdateTempDir() |
|||
// 创建临时目录
|
|||
if (!fs.existsSync(updateTempDirPath)) { |
|||
fs.mkdirSync(updateTempDirPath, { recursive: true }) |
|||
} |
|||
|
|||
// 下载文件的本地保存路径
|
|||
const downloadPath = path.join(updateTempDirPath, "update.zip") |
|||
|
|||
try { |
|||
// 使用 fetch 下载更新包
|
|||
const response = await fetch(updatePackageUrl) |
|||
if (!response.ok) { |
|||
throw new Error(`下载失败: ${response.status} ${response.statusText}`) |
|||
} |
|||
|
|||
// 将下载内容写入文件
|
|||
const arrayBuffer = await response.arrayBuffer() |
|||
fs.writeFileSync(downloadPath, Buffer.from(arrayBuffer)) |
|||
|
|||
// 解压更新包
|
|||
await extract(downloadPath, { dir: updateTempDirPath }) |
|||
|
|||
// 删除下载的zip文件
|
|||
fs.unlinkSync(downloadPath) |
|||
isReadyUpdate = true |
|||
emitHotUpdateReady() |
|||
} catch (error) { |
|||
debug("热更新包下载失败:", error) |
|||
throw error |
|||
} |
|||
} |
|||
|
|||
function clearUpdateTempDir() { |
|||
if (!fs.existsSync(updateTempDirPath)) return |
|||
fs.rmSync(updateTempDirPath, { recursive: true }) |
|||
} |
|||
|
|||
export function flagNeedUpdate() { |
|||
shouldPerformHotUpdate = true |
|||
} |
@ -1,8 +0,0 @@ |
|||
import { ElectronAPI } from "@electron-toolkit/preload" |
|||
|
|||
declare global { |
|||
interface Window { |
|||
electron: ElectronAPI |
|||
api: unknown |
|||
} |
|||
} |
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue