mirror of
https://github.com/dream-pep/koring-launcher.git
synced 2026-09-12 05:45:18 +08:00
feat: 新增跨进程统一日志系统并替换旧式控制台日志
- 新增渲染进程日志工具与主进程日志核心,支持按作用域分类日志 - 调试模式下日志会写入轮转文件,否则仅输出到控制台 - 暴露渲染器到主进程的日志 IPC 桥接接口 - 替换全项目所有旧式 console 日志调用为统一日志接口 - 新增调试页面日志状态面板 - 更新相关 TypeScript 类型定义与项目文档
This commit is contained in:
@@ -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
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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(); },
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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),
|
||||
|
||||
|
||||
@@ -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
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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">
|
||||
|
||||
@@ -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("选择壁纸未返回路径(取消或导入失败)");
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
Vendored
+3
@@ -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 }>;
|
||||
|
||||
Reference in New Issue
Block a user