feat: 新增跨进程统一日志系统并替换旧式控制台日志

- 新增渲染进程日志工具与主进程日志核心,支持按作用域分类日志
- 调试模式下日志会写入轮转文件,否则仅输出到控制台
- 暴露渲染器到主进程的日志 IPC 桥接接口
- 替换全项目所有旧式 console 日志调用为统一日志接口
- 新增调试页面日志状态面板
- 更新相关 TypeScript 类型定义与项目文档
This commit is contained in:
2026-09-04 22:06:59 +08:00
parent d5004b61d8
commit e861661a55
16 changed files with 222 additions and 47 deletions
+1
View File
@@ -67,6 +67,7 @@ build/ ← generated by switch-icon.js (gitignored)
- `electron/auth.ts`: Auth data persistence (JSON file)
- `electron/core/`: @xmcl/* integrations (auth, installer, launcher, modrinth, instance)
- `electron/core/background-image.ts`: 背景图处理服务 —— 自选壁纸复制到 userData 并按屏幕尺寸降采样/重编码**落盘**,配置文件只存**文件路径**(不使用 BASE64
- `electron/core/logger.ts`: 统一日志 —— 全局包装 `ipcMain.handle`channel/耗时/成败);开启 debug 模式(`config.advanced.debugMode`)后写 `userData/koring.log`(5MB 轮转),否则仅控制台;渲染端经 `log:write` 桥汇入
- `electron/resource-protocol.ts`: `koring-res://` 特权自定义协议 —— 渲染进程以「资源引用」流式读取本地壁纸;仅服务 userData 内 `background-custom*` 白名单文件(realpath 二次校验,防目录穿越)
- `electron/handlers/`: IPC handlers (config, auth, install, launch, mods, instance, background, task, system, window)
File diff suppressed because one or more lines are too long
+7
View File
@@ -2,8 +2,11 @@ import * as fs from 'fs';
import * as path from 'path';
import * as yaml from 'js-yaml';
import electron from 'electron';
import { createLogger } from './core/logger';
const { app } = electron;
const log = createLogger('config');
const CONFIG_FILE = 'Koring.yml';
const CURRENT_VERSION = 1;
@@ -234,6 +237,7 @@ export function loadConfig(): AppConfig {
export function saveConfig(config: AppConfig, force = false): void {
const filePath = configPath();
const sparse = diffValue(config, DEFAULTS) as Record<string, unknown> | undefined;
log.debug(`saveConfig → ${filePath} (force=${force}, 稀疏键=${sparse ? Object.keys(sparse).length : 0})`);
if (!sparse || Object.keys(sparse).length === 0) {
if (force) {
@@ -292,6 +296,7 @@ export function flushConfig(): void {
saveTimer = null;
}
if (current) {
log.debug('flushConfig:内存配置落盘');
saveConfig(current);
}
}
@@ -300,6 +305,7 @@ export function flushConfig(): void {
export function updateConfig(patch: Record<string, unknown>): AppConfig {
const base = getConfig();
current = mergeDeep(base, patch) as AppConfig;
log.debug('config:update 补丁顶层键', Object.keys(patch ?? {}));
scheduleSave();
return current;
}
@@ -310,6 +316,7 @@ export function deleteConfigKey(key: string): AppConfig {
const next = { ...(base as unknown as Record<string, unknown>) };
delete next[key];
current = next as unknown as AppConfig;
log.debug(`config:delete 顶层键 ${key}`);
scheduleSave();
return current;
}
+10 -2
View File
@@ -11,9 +11,12 @@
import electron from 'electron';
import * as fs from 'fs';
import * as path from 'path';
import { createLogger } from './logger';
const { nativeImage } = electron;
const log = createLogger('background-image');
export interface OptimizedBackground {
/** 实际使用的文件路径(优化后文件;无需优化时为原始缓存文件) */
filePath: string;
@@ -221,8 +224,13 @@ export function importUserBackground(srcPath: string, maxEdge = 4096): Optimized
// 删除旧的原始缓存
clearStaleBackgroundFiles(userDataDir, []);
fs.copyFileSync(srcPath, rawPath);
return optimizeBackgroundFile(rawPath, maxEdge);
} catch {
const result = optimizeBackgroundFile(rawPath, maxEdge);
if (result) {
log.info(`导入壁纸完成 → ${path.basename(result.filePath)} (${result.width}x${result.height}, ${result.bytes}B, optimized=${result.optimized})`);
}
return result;
} catch (e) {
log.error('导入壁纸失败:', e);
return null;
}
}
+6 -3
View File
@@ -1,5 +1,8 @@
import * as https from 'https';
import { readAuth, writeAuth } from '../auth';
import { createLogger } from './logger';
const log = createLogger('core/koring-auth');
const CLIENT_ID = '547qe8ky1pr69f08b71kj';
const DEVICE_AUTH_URL = 'https://oac.lingke.ink/oidc/device/auth';
@@ -36,8 +39,8 @@ function postForm(url: string, data: Record<string, string>): Promise<any> {
const params = new URLSearchParams(data);
const body = params.toString().replace(/\+/g, '%20');
const urlObj = new URL(url);
console.log(`[koring-auth] POST ${url}`);
console.log(`[koring-auth] body: ${body}`);
log.info(`[koring-auth] POST ${url}`);
log.info(`[koring-auth] body: ${body}`);
const req = https.request(
{
hostname: urlObj.hostname,
@@ -53,7 +56,7 @@ function postForm(url: string, data: Record<string, string>): Promise<any> {
let raw = '';
res.on('data', (chunk) => (raw += chunk));
res.on('end', () => {
console.log(`[koring-auth] response (${res.statusCode}): ${raw}`);
log.info(`[koring-auth] response (${res.statusCode}): ${raw}`);
try {
resolve(JSON.parse(raw));
} catch {
+11 -8
View File
@@ -4,9 +4,12 @@ import fs from 'fs';
import { writeCrashLog, readCrashLog, clearCrashLog, type CrashEntry } from '../core/crash-logger';
import { configPath } from '../config';
import { authPath } from '../auth';
import { createLogger } from '../core/logger';
const { app, ipcMain, BrowserWindow } = electron;
const log = createLogger('crash-monitor');
const isDev = !app.isPackaged;
let crashWin: electron.BrowserWindow | null = null;
@@ -99,14 +102,14 @@ export function setupCrashListeners(mainWindow: electron.BrowserWindow) {
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('');
log.info('%c[崩溃工具] 已加载', 'color: #f59e0b; font-weight: bold; font-size: 14px;');
log.info('%c可用命令:', 'color: #3b82f6; font-weight: bold;');
log.info('%c crash.simulate() %c— 模拟渲染进程崩溃', 'color: #ef4444; font-weight: bold;', 'color: inherit;');
log.info('%c crash.testDialog() %c— 测试崩溃弹窗', 'color: #ef4444; font-weight: bold;', 'color: inherit;');
log.info('%c crash.readLog() %c— 读取崩溃日志', 'color: #ef4444; font-weight: bold;', 'color: inherit;');
log.info('%c crash.factoryReset()%c— 强还原配置', 'color: #ef4444; font-weight: bold;', 'color: inherit;');
log.info('%c crash.restart() %c— 重启应用', 'color: #ef4444; font-weight: bold;', 'color: inherit;');
log.info('');
window.crash = {
simulate: function() { window.electronAPI?.simulateCrash(); },
+4 -1
View File
@@ -9,6 +9,9 @@ import {
deleteKoringAuth,
} from '../core/koring-auth';
import { getConfig, updateConfig, deleteConfigKey } from '../config';
import { createLogger } from '../core/logger';
const log = createLogger('koring-auth');
export function registerKoringAuthHandlers() {
ipcMain.handle('koring-auth:request-device-code', async () => {
@@ -39,7 +42,7 @@ export function registerKoringAuthHandlers() {
},
});
} catch (e) {
console.error('[koring-auth] failed to save user to config:', e);
log.error('[koring-auth] failed to save user to config:', e);
}
return { success: true, data: { user }, error: null };
+5
View File
@@ -86,6 +86,11 @@ contextBridge.exposeInMainWorld('electronAPI', {
return { url: null, bytes: 0 };
},
// 日志:渲染端经 IPC 汇入主进程统一日志(debug 模式写文件;否则仅控制台)
log: (level: 'debug' | 'info' | 'warn' | 'error', scope: string, message: string) => {
ipcRenderer.send('log:write', { level, scope, message });
},
// Open external URL in system browser
openExternal: (url: string) => ipcRenderer.invoke('shell:openExternal', url),
+11 -1
View File
@@ -14,9 +14,12 @@ import * as fs from 'fs';
import * as path from 'path';
import { Readable } from 'stream';
import { isManagedBackgroundFile, isPathInside, mimeForFile } from './core/background-image';
import { createLogger } from './core/logger';
const { protocol, app } = electron;
const log = createLogger('resource-protocol');
export const RESOURCE_SCHEME = 'koring-res';
/** 必须在 app ready 之前调用(privileged scheme 注册) */
@@ -41,6 +44,7 @@ export function registerResourceProtocol(): void {
try {
const url = new URL(request.url);
if (url.host !== 'userdata') {
log.warn(`拒绝非 userdata 主机: ${url.host}`);
return new Response('Forbidden', { status: 403 });
}
@@ -53,11 +57,13 @@ export function registerResourceProtocol(): void {
// 路径穿越 / 绝对路径直接拒绝
if (!relative || relative.includes('..') || path.isAbsolute(relative)) {
log.warn(`拒绝非法路径: ${relative}`);
return new Response('Forbidden', { status: 403 });
}
const fileName = path.basename(relative);
if (fileName !== relative || !isManagedBackgroundFile(fileName)) {
log.warn(`拒绝白名单外文件: ${fileName}`);
return new Response('Forbidden', { status: 403 });
}
@@ -65,15 +71,18 @@ export function registerResourceProtocol(): void {
const target = path.join(userDataDir, fileName);
if (!isPathInside(userDataDir, target)) {
log.warn(`拒绝越权文件: ${target}`);
return new Response('Forbidden', { status: 403 });
}
const stat = await fs.promises.stat(target);
if (!stat.isFile()) {
log.warn(`文件不存在: ${target}`);
return new Response('Not Found', { status: 404 });
}
const body = Readable.toWeb(fs.createReadStream(target)) as unknown as BodyInit;
log.debug(`服务资源 ${fileName} (${stat.size}B, ${mimeForFile(target)})`);
return new Response(body, {
status: 200,
headers: {
@@ -82,7 +91,8 @@ export function registerResourceProtocol(): void {
'cache-control': 'no-store',
},
});
} catch {
} catch (e) {
log.error(`协议请求处理失败: ${request.url}`, e);
return new Response('Not Found', { status: 404 });
}
});
+30 -27
View File
@@ -5,9 +5,12 @@ import * as crypto from 'crypto';
import * as os from 'os';
import semver from 'semver';
import { getConfig, updateConfig, flushConfig } from './config';
import { createLogger } from './core/logger';
const { app } = electron;
const log = createLogger('updater');
export type UpdateState =
| 'idle'
| 'checking'
@@ -170,7 +173,7 @@ class UpdateService {
this.currentVersion = app.getVersion();
if (!app.isPackaged) {
console.log('[updater] 开发模式:跳过自动更新');
log.info('[updater] 开发模式:跳过自动更新');
this.emit();
return;
}
@@ -184,7 +187,7 @@ class UpdateService {
// 应用能启动即说明上次安装已完成/已结束,清理持久化的进行中状态
const persisted = getConfig().update;
if (persisted && persisted.state && persisted.state !== 'idle') {
console.log(`[updater] 上次更新状态 ${persisted.state} (v${persisted.version}),已重置`);
log.info(`[updater] 上次更新状态 ${persisted.state} (v${persisted.version}),已重置`);
this.persistIdleConfig();
}
@@ -208,7 +211,7 @@ class UpdateService {
const first = Array.isArray(anyInfo?.files) ? anyInfo.files[0] : null;
this.expectedSha512 = String(first?.sha512 ?? '');
this.expectedSize = Number(first?.size ?? 0);
console.log(`[updater] 可用更新 ${info.version},期望 sha512=${this.expectedSha512.slice(0, 12)}… size=${this.expectedSize}`);
log.info(`[updater] 可用更新 ${info.version},期望 sha512=${this.expectedSha512.slice(0, 12)}… size=${this.expectedSize}`);
this.emit();
});
autoUpdater.on('update-not-available', () => {
@@ -229,7 +232,7 @@ class UpdateService {
autoUpdater.on('update-downloaded', async (info) => {
const errMsg = await this.verifyDownloadedPackage();
if (errMsg) {
console.error(`[updater] 安装包核验失败: ${errMsg}`);
log.error(`[updater] 安装包核验失败: ${errMsg}`);
this.state = 'downloaded';
this.verified = false;
this.version = info.version;
@@ -237,7 +240,7 @@ class UpdateService {
this.emit();
return;
}
console.log('[updater] 安装包核验通过(sha512 + 大小)');
log.info('[updater] 安装包核验通过(sha512 + 大小)');
this.state = 'downloaded';
this.verified = true;
this.version = info.version;
@@ -246,7 +249,7 @@ class UpdateService {
});
autoUpdater.on('error', (err: Error) => {
const message = String(err?.message ?? err);
console.warn(`[updater] electron-updater error: ${message}`);
log.warn(`[updater] electron-updater error: ${message}`);
if (this.suppressErrors) return; // 兜底循环内,忽略
if (this.downloadToken?.cancelled) return; // 主动暂停/取消,忽略
this.state = 'error';
@@ -331,7 +334,7 @@ class UpdateService {
},
});
} catch (e) {
console.warn('[updater] 更新进度写入配置失败:', e);
log.warn('[updater] 更新进度写入配置失败:', e);
}
}
@@ -361,7 +364,7 @@ class UpdateService {
// woker 恢复默认 latest 频道(allowPrerelease=false 走 /releases/latest,频道不影响识别)
autoUpdater.channel = 'latest';
}
console.log(`[updater] 更新通道: ${def.label}${def.key}allowPrerelease=${def.allowPrerelease}channel=${autoUpdater.channel}`);
log.info(`[updater] 更新通道: ${def.label}${def.key}allowPrerelease=${def.allowPrerelease}channel=${autoUpdater.channel}`);
}
/** 通道定义列表(UI 动态渲染;可扩展) */
@@ -372,7 +375,7 @@ class UpdateService {
/** 切换更新通道(校验 + 持久化 + 立即生效,下次检查生效) */
setChannel(key: string): UpdateStatusPayload {
if (!UPDATE_CHANNELS.some((c) => c.key === key)) {
console.warn(`[updater] 未知更新通道: ${key}`);
log.warn(`[updater] 未知更新通道: ${key}`);
return this.buildPayload();
}
if (this.channelKey === key) return this.buildPayload();
@@ -381,7 +384,7 @@ class UpdateService {
try {
updateConfig({ update: { channel: key } });
} catch (e) {
console.warn('[updater] 通道写入配置失败:', e);
log.warn('[updater] 通道写入配置失败:', e);
}
this.emit();
return this.buildPayload();
@@ -398,7 +401,7 @@ class UpdateService {
setTestVersion(version: string): UpdateStatusPayload {
const v = semver.valid(version.trim());
if (!v) {
console.warn(`[updater] 无效测试版本号: ${version}`);
log.warn(`[updater] 无效测试版本号: ${version}`);
return this.buildPayload();
}
this.currentVersion = v;
@@ -406,9 +409,9 @@ class UpdateService {
// currentVersion 在类型声明中为 readonly,但运行时可直接赋值(测试工具用)
(autoUpdater as unknown as { currentVersion: unknown }).currentVersion = semver.parse(v);
} catch (e) {
console.warn('[updater] 设置 autoUpdater.currentVersion 失败:', e);
log.warn('[updater] 设置 autoUpdater.currentVersion 失败:', e);
}
console.log(`[updater] 测试版本号 → ${v}`);
log.info(`[updater] 测试版本号 → ${v}`);
this.emit();
return this.buildPayload();
}
@@ -430,7 +433,7 @@ class UpdateService {
private correctAvailability(): void {
if (this.state !== 'available' || !this.version) return;
if (this.isNewerCandidate(this.currentVersion, this.version)) return;
console.warn(`[updater] ${this.version} 不是 ${this.currentVersion} 的新版本(项目版本序,忽略 beta 通道标记),回退为无更新`);
log.warn(`[updater] ${this.version} 不是 ${this.currentVersion} 的新版本(项目版本序,忽略 beta 通道标记),回退为无更新`);
this.state = 'not-available';
this.version = undefined;
this.error = undefined;
@@ -481,7 +484,7 @@ class UpdateService {
this.correctAvailability();
return this.buildPayload();
} catch (err) {
console.warn(`[updater] GitHub 官方更新源不可用: ${String((err as Error)?.message ?? err)}`);
log.warn(`[updater] GitHub 官方更新源不可用: ${String((err as Error)?.message ?? err)}`);
}
// 2) 加速源兜底:镜像页面发现最新 tag → generic feed → 检查
@@ -489,11 +492,11 @@ class UpdateService {
try {
const tag = await this.discoverLatestTag(mirror);
if (!tag) {
console.warn(`[updater] ${mirror} 无法发现最新版本,跳过`);
log.warn(`[updater] ${mirror} 无法发现最新版本,跳过`);
continue;
}
const feedUrl = `${mirror}/https://github.com/${OWNER}/${REPO}/releases/download/${tag}/`;
console.log(`[updater] 切换加速源: ${mirror} (feed: ${feedUrl})`);
log.info(`[updater] 切换加速源: ${mirror} (feed: ${feedUrl})`);
autoUpdater.setFeedURL({ provider: 'generic', url: feedUrl });
this.source = mirror;
this.state = 'checking';
@@ -506,9 +509,9 @@ class UpdateService {
// 不要就此返回 not-available,继续尝试下一个源
const mirrorResult = this.buildPayload();
if (mirrorResult.state !== 'not-available') return mirrorResult;
console.warn(`[updater] ${mirror} 反馈无可用更新,尝试下一个源`);
log.warn(`[updater] ${mirror} 反馈无可用更新,尝试下一个源`);
} catch (err) {
console.warn(`[updater] 加速源 ${mirror} 检查失败: ${String((err as Error)?.message ?? err)}`);
log.warn(`[updater] 加速源 ${mirror} 检查失败: ${String((err as Error)?.message ?? err)}`);
}
}
@@ -569,7 +572,7 @@ class UpdateService {
candidates.sort(compareVersionTags);
const latest = candidates[candidates.length - 1];
if (candidates.length > 1) {
console.log(`[updater] ${mirror} 候选版本: ${candidates.join(', ')} → 取 ${latest}`);
log.info(`[updater] ${mirror} 候选版本: ${candidates.join(', ')} → 取 ${latest}`);
}
return latest;
}
@@ -615,7 +618,7 @@ class UpdateService {
const notes = await res.text();
if (!notes.trim()) continue;
if (/^\s*<!doctype html/i.test(notes) || /^\s*<html[\s>]/i.test(notes)) continue;
console.log(`[updater] 发布说明来源: ${source} (${tag})`);
log.info(`[updater] 发布说明来源: ${source} (${tag})`);
return { tag, version: tag.replace(/^v/, ''), notes, source, isLatest: false };
}
} catch {
@@ -636,7 +639,7 @@ class UpdateService {
if (this.state !== 'available' && this.state !== 'paused') return;
// 复核目标版本确为当前版本的新版本(项目版本序),否则回退为无更新
if (this.state === 'available' && this.version && !this.isNewerCandidate(this.currentVersion, this.version)) {
console.warn(`[updater] 下载被拒:${this.version} 不是 ${this.currentVersion} 的新版本`);
log.warn(`[updater] 下载被拒:${this.version} 不是 ${this.currentVersion} 的新版本`);
this.state = 'not-available';
this.version = undefined;
this.emit();
@@ -644,7 +647,7 @@ class UpdateService {
}
if (this.state === 'paused') {
// 继续下载(可能从断点续传,也可能重新开始,取决于 electron-updater 缓存)
console.log('[updater] 继续下载');
log.info('[updater] 继续下载');
}
this.state = 'downloading';
this.progress = null;
@@ -709,8 +712,8 @@ class UpdateService {
: await dialog.showMessageBox(opts);
if (response !== 0) {
// 取消并删除安装包
console.warn('[updater] 用户取消安装并删除校验异常包');
await this.removeDownloadedPackage().catch((e) => console.warn('[updater] 删除安装包失败:', e));
log.warn('[updater] 用户取消安装并删除校验异常包');
await this.removeDownloadedPackage().catch((e) => log.warn('[updater] 删除安装包失败:', e));
this.state = 'idle';
this.version = undefined;
this.error = undefined;
@@ -718,7 +721,7 @@ class UpdateService {
this.emit();
return;
}
console.warn('[updater] 用户确认继续安装(校验异常但已确认)');
log.warn('[updater] 用户确认继续安装(校验异常但已确认)');
}
this.state = 'installing';
@@ -739,7 +742,7 @@ class UpdateService {
if (!filePath) return;
if (fs.existsSync(filePath)) {
await fs.promises.unlink(filePath);
console.log(`[updater] 已删除安装包: ${filePath}`);
log.info(`[updater] 已删除安装包: ${filePath}`);
}
}
@@ -6,6 +6,9 @@ import { useDevStore } from "@/stores/devStore";
import { DEFAULT_BG } from "@/lib/mode";
import { resourceRegistry } from "@/resources/registry";
import { estimateDataUrlBytes } from "@/resources/image";
import { createRendererLogger } from "@/lib/logger";
const log = createRendererLogger("BackgroundLayer");
/** 可直接用于 CSS 的源(默认图/相对 URL/http(s)/file:/data: */
const isCssSource = (v: string) => /^(data:|https?:|file:|\/|\.\/|\.\.\/)/i.test(v);
@@ -59,10 +62,12 @@ export function BackgroundLayer() {
window.electronAPI?.resolveBackgroundResource?.(image).then((res) => {
if (cancelled || !res) return;
if (res.url) {
log.info(`背景资源解析成功:${image}${res.url} (${res.bytes}B)`);
setBgImage(res.url);
setBgBytes(res.bytes || 0);
} else {
// 文件不可用/越权:回退默认背景(文件已失效,回退优于显示破损背景)
log.warn(`背景资源解析失败/越权,回退默认背景:${image}`);
setBgImage(DEFAULT_BG);
setBgBytes(0);
}
@@ -80,6 +85,7 @@ export function BackgroundLayer() {
setPrevious(oldActive);
setActive(bgImage);
activeRef.current = bgImage;
log.debug(`背景切换 ${oldActive ?? "(无)"}${bgImage ?? "(无)"}`);
// 已有旧背景且确实发生了内容切换(首次出现不渐入)
if (oldActive != null && bgImage != null && oldActive !== bgImage) {
setFading(true);
+70
View File
@@ -0,0 +1,70 @@
/**
* 渲染端统一日志。
*
* 规则(与主进程 electron/core/logger.ts 对齐):
* - 默认(非 debug):error/warn/info 输出到控制台(DevTools),debug 不输出;
* - 用户开启「调试模式」(config.advanced.debugMode)后:debug 也输出,
* 并经由 electronAPI.log → 主进程 log:write 桥写入 userData/koring.log。
*/
import { useConfigStore } from "@/stores/configStore";
export type LogLevel = "debug" | "info" | "warn" | "error";
export interface RendererLogger {
debug: (msg: string, ...args: unknown[]) => void;
info: (msg: string, ...args: unknown[]) => void;
warn: (msg: string, ...args: unknown[]) => void;
error: (msg: string, ...args: unknown[]) => void;
}
function serialize(value: unknown): string {
if (typeof value === "string") {
return value.length > 800 ? `${value.slice(0, 800)}…(+${value.length - 800})` : value;
}
if (value instanceof Error) return value.message;
if (typeof value === "object" && value !== null) {
try {
return JSON.stringify(value) ?? String(value);
} catch {
return String(value);
}
}
return String(value);
}
function isDebugMode(): boolean {
try {
return useConfigStore.getState().config?.advanced?.debugMode === true;
} catch {
return false;
}
}
function emit(scope: string, level: LogLevel, msg: string, args: unknown[]): void {
const text = args.length ? `${msg} ${args.map(serialize).join(" ")}` : msg;
const line = `[${scope}][${level.toUpperCase()}] ${text}`;
const debugOn = isDebugMode();
if (level === "error") console.error(line);
else if (level === "warn") console.warn(line);
else if (level === "info") console.info(line);
else if (debugOn) console.debug(line);
if (!debugOn) return;
window.electronAPI?.log?.(level, scope, line);
}
function make(scope: string): RendererLogger {
const bound = (level: LogLevel) => (msg: string, ...args: unknown[]) => emit(scope, level, msg, args);
return {
debug: bound("debug"),
info: bound("info"),
warn: bound("warn"),
error: bound("error"),
};
}
export function createRendererLogger(scope: string): RendererLogger {
return make(scope);
}
+36
View File
@@ -30,6 +30,7 @@ export function ResourceDebug() {
const [proc, setProc] = useState<SystemMemorySnapshot | null>(null);
const [heap, setHeap] = useState<JsHeapInfo | null>(null);
const [domNodes, setDomNodes] = useState(0);
const [logInfo, setLogInfo] = useState<{ filePath: string | null; debugMode: boolean } | null>(null);
const timerRef = useRef<ReturnType<typeof setInterval> | null>(null);
const snapshot = useResourceStore((s) => s.snapshot);
@@ -46,6 +47,14 @@ export function ResourceDebug() {
} catch {
setProc(null);
}
try {
const info = (await window.electronAPI?.invoke?.("log:getInfo")) as
| { filePath: string | null; debugMode: boolean }
| undefined;
if (info) setLogInfo(info);
} catch {
// 忽略日志状态查询失败
}
};
useEffect(() => {
@@ -104,6 +113,33 @@ export function ResourceDebug() {
</div>
</div>
{/* 统一日志状态 */}
<div>
<h3 className="text-sm font-semibold text-foreground/40 uppercase tracking-wider mb-3">
debug
</h3>
<GlassCard>
<div className="flex flex-wrap gap-x-8 gap-y-2">
<div>
<p className="text-[12px] text-muted-foreground"></p>
<p className="text-lg font-semibold text-foreground">
{logInfo?.debugMode ? "开启" : "关闭"}
</p>
</div>
<div>
<p className="text-[12px] text-muted-foreground"></p>
<p className="text-sm font-medium text-foreground break-all">
{logInfo?.filePath ?? "(未开启 → 仅输出到控制台)"}
</p>
</div>
<div>
<p className="text-[12px] text-muted-foreground"></p>
<p className="text-sm font-medium text-foreground"> </p>
</div>
</div>
</GlassCard>
</div>
{/* 进程工作集 */}
<div>
<h3 className="text-sm font-semibold text-foreground/40 uppercase tracking-wider mb-3">
+10 -1
View File
@@ -5,6 +5,9 @@ import { Switch, Button, Slider } from "@heroui/react";
import { DEFAULT_BG } from "@/lib/mode";
import clsx from "clsx";
import { SettingCard, SettingRow, PageHeader, SectionTitle } from "@/components/setting";
import { createRendererLogger } from "@/lib/logger";
const log = createRendererLogger("theme-bg");
/** 可直接用于 CSS/`<img>` 的源(data:/http(s):/file:/相对 URL */
const isCssSource = (v: string) => /^(data:|https?:|file:|\/|\.\/|\.\.\/)/i.test(v);
@@ -101,7 +104,10 @@ export function ThemeBgSetting() {
};
}
window.electronAPI?.resolveBackgroundResource?.(image).then((res) => {
if (!cancelled) setPreviewUrl(res?.url ?? null);
if (!cancelled) {
setPreviewUrl(res?.url ?? null);
log.debug(`预览资源解析 ${image}${res?.url ?? "(失败)"}`);
}
});
return () => {
cancelled = true;
@@ -112,7 +118,10 @@ export function ThemeBgSetting() {
// 返回的是 userData 内的文件路径(配置/Store 以路径保存,不使用 BASE64)
const filePath = await window.electronAPI?.pickBackgroundImage?.();
if (filePath) {
log.info(`选择壁纸完成 → ${filePath}`);
setImage(filePath);
} else {
log.warn("选择壁纸未返回路径(取消或导入失败)");
}
};
+11 -3
View File
@@ -16,6 +16,9 @@ import {
type ResourceEntrySnapshot,
type ResourceKind,
} from "./types";
import { createRendererLogger } from "@/lib/logger";
const log = createRendererLogger("resourceRegistry");
export interface AcquireOptions<T> {
/** 估算占用字节数(用于预算与面板统计;未提供则记 0) */
@@ -83,12 +86,14 @@ class ResourceRegistry {
if (existing.settled) {
this.hits += 1;
this.emit();
log.debug(`acquire 命中 ${kind}:${key} (refs=${existing.refs})`);
return Promise.resolve(existing.payload as T | null);
}
return existing.inFlight as Promise<T | null>;
}
this.misses += 1;
log.debug(`acquire 创建 ${kind}:${key}`);
const entry: InternalEntry = {
key,
kind,
@@ -134,6 +139,7 @@ class ResourceRegistry {
if (!entry) return;
entry.refs = Math.max(0, entry.refs - 1);
entry.lastUsed = Date.now();
log.debug(`release ${key} (refs=${entry.refs})`);
if (!entry.cache && entry.refs === 0) {
this.drop(entry);
this.emit();
@@ -160,16 +166,16 @@ class ResourceRegistry {
}
this.emit();
}
/** 释放全部「引用数为 0」的缓存条目(监控面板「释放缓存」按钮) */
clearFree(): void {
let dropped = false;
let dropped = 0;
for (const entry of [...this.entries.values()]) {
if (entry.refs <= 0 && entry.settled) {
this.drop(entry);
dropped = true;
dropped += 1;
}
}
log.debug(`clearFree 释放缓存条目 ${dropped}`);
if (dropped) this.emitNow();
}
@@ -188,6 +194,7 @@ class ResourceRegistry {
bytes -= entry.bytes;
this.drop(entry);
this.evictions += 1;
log.debug(`LRU 逐出 ${entry.kind}:${entry.key}`);
}
}
}
@@ -238,6 +245,7 @@ class ResourceRegistry {
private drop(entry: InternalEntry): void {
this.entries.delete(entry.key);
log.debug(`释放资源 ${entry.kind}:${entry.key} (${entry.bytes}B, settled=${entry.settled})`);
if (entry.settled && entry.payload != null) {
try {
entry.onRelease?.(entry.payload);
+3
View File
@@ -25,6 +25,9 @@ interface ElectronAPI {
// Config reset
resetConfig: () => Promise<void>;
// 渲染端日志 → 主进程统一日志(debug 模式写文件)
log: (level: "debug" | "info" | "warn" | "error", scope: string, message: string) => void;
// 壁纸(文件路径存储 → koring-res:// 资源引用,不使用 BASE64
pickBackgroundImage: () => Promise<string | null>;
resolveBackgroundResource: (value: string) => Promise<{ url: string | null; bytes: number }>;