feat: enhance logging capabilities, implement runtime diagnostics, and update preinstalled skills metadata
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"generatedAt": "2026-04-27T03:36:36.328Z",
|
||||
"generatedAt": "2026-04-27T07:58:41.747Z",
|
||||
"skills": [
|
||||
{
|
||||
"slug": "pdf",
|
||||
|
||||
@@ -40,11 +40,14 @@ asarUnpack:
|
||||
npmRebuild: false
|
||||
afterPack: ./scripts/after-pack.cjs
|
||||
|
||||
# Auto-update configuration (to be configured later)
|
||||
# Auto-update configuration
|
||||
# publish:
|
||||
# - provider: generic
|
||||
# url: https://your-update-server.com/latest
|
||||
# useMultipleRangeRequest: false
|
||||
# - provider: s3
|
||||
# bucket: one-feel-bucket
|
||||
# region: oss-cn-guangzhou
|
||||
# endpoint: https://oss-cn-guangzhou.aliyuncs.com
|
||||
# path: setupPackage
|
||||
# acl: public-read
|
||||
|
||||
# macOS Configuration
|
||||
mac:
|
||||
|
||||
@@ -25,6 +25,8 @@ export async function handleLogRoutes(
|
||||
try {
|
||||
return ok({
|
||||
content: await logManager.readRecentLogText(parseTailLines(url.searchParams.get('tailLines'))),
|
||||
directory: logManager.getLogDirectoryPath(),
|
||||
currentFile: logManager.getCurrentLogFilePath(),
|
||||
});
|
||||
} catch (error) {
|
||||
return fail(500, error instanceof Error ? error.message : String(error));
|
||||
|
||||
@@ -7,7 +7,7 @@ import configManager from '@electron/service/config-service'
|
||||
import themeManager from '@electron/service/theme-service'
|
||||
import { runTaskOperationService } from '@electron/process/runTaskOperationService'
|
||||
import { initScriptStoreService } from '@electron/service/script-store-service'
|
||||
import log from 'electron-log';
|
||||
import logManager from '@electron/service/logger';
|
||||
import 'bytenode'; // Ensure bytenode is bundled/externalized correctly
|
||||
import { appUpdater } from '@electron/service/updater';
|
||||
import axios from 'axios';
|
||||
@@ -21,9 +21,19 @@ import { applyLaunchAtStartupSetting, syncLaunchAtStartupSettingFromConfig } fro
|
||||
import { ensureBuiltinSkillsInstalled, ensurePreinstalledSkillsInstalled } from '@electron/utils/skill-config';
|
||||
import { initTelemetry, shutdownTelemetry } from '@electron/utils/telemetry';
|
||||
import { syncGatewayConfigBeforeLaunch } from '@electron/gateway/config-sync';
|
||||
import { installRuntimeDiagnostics, logStartupCheckpoint, startLocalCrashReporter } from '@electron/utils/runtime-diagnostics';
|
||||
|
||||
// 初始化 updater,确保在 app ready 之前或者之中注册好 IPC
|
||||
appUpdater.init();
|
||||
startLocalCrashReporter();
|
||||
installRuntimeDiagnostics();
|
||||
logStartupCheckpoint('main-module-loaded');
|
||||
|
||||
try {
|
||||
appUpdater.init();
|
||||
logStartupCheckpoint('app-updater-initialized');
|
||||
} catch (error) {
|
||||
logManager.captureException('Failed to initialize app updater', error);
|
||||
}
|
||||
|
||||
// 注册 hostapi:fetch IPC 代理
|
||||
// 模型管理相关接口在本地处理(对齐 ClawX),其余接口代理到远端后端
|
||||
@@ -48,7 +58,7 @@ function refreshProviderRuntime(): { warnings: string[] } {
|
||||
try {
|
||||
return syncProviderRuntimeSnapshot();
|
||||
} catch (error) {
|
||||
log.error('provider runtime sync failed', error);
|
||||
logManager.captureException('provider runtime sync failed', error);
|
||||
return {
|
||||
warnings: [error instanceof Error ? error.message : String(error)],
|
||||
};
|
||||
@@ -237,12 +247,12 @@ function bindGatewayLifecycleEvents(): void {
|
||||
}
|
||||
|
||||
function requestQuitOnSignal(signal: NodeJS.Signals): void {
|
||||
log.info(`Received ${signal}; requesting app quit`);
|
||||
logManager.info(`Received ${signal}; requesting app quit`);
|
||||
app.quit();
|
||||
}
|
||||
|
||||
function emergencyGatewayCleanup(reason: string, error: unknown): void {
|
||||
log.error(`${reason}:`, error);
|
||||
logManager.captureException(reason, error);
|
||||
hostEventBus.closeAll();
|
||||
void closeHostApiServer().catch(() => {
|
||||
// ignore host API server close failures during emergency cleanup
|
||||
@@ -321,14 +331,14 @@ app.on('before-quit', (event) => {
|
||||
gatewayQuitCleanupInProgress = true;
|
||||
hostEventBus.closeAll();
|
||||
const closeServerPromise = closeHostApiServer().catch((error) => {
|
||||
log.warn('Host API server close failed during quit:', error);
|
||||
logManager.captureException('Host API server close failed during quit', error);
|
||||
});
|
||||
|
||||
const stopPromise = Promise.all([
|
||||
closeServerPromise,
|
||||
gatewayManager.stop(),
|
||||
]).catch((error) => {
|
||||
log.warn('gatewayManager.stop() error during quit:', error);
|
||||
logManager.captureException('gatewayManager.stop() error during quit', error);
|
||||
});
|
||||
const timeoutPromise = new Promise<'timeout'>((resolve) => {
|
||||
setTimeout(() => resolve('timeout'), GATEWAY_QUIT_TIMEOUT_MS);
|
||||
@@ -339,38 +349,43 @@ app.on('before-quit', (event) => {
|
||||
timeoutPromise,
|
||||
]).then(async (result) => {
|
||||
if (result === 'timeout') {
|
||||
log.warn('Gateway shutdown timed out during app quit; proceeding with forced quit');
|
||||
logManager.warn('Gateway shutdown timed out during app quit; proceeding with forced quit');
|
||||
try {
|
||||
const terminated = await gatewayManager.forceTerminateOwnedProcessForQuit();
|
||||
if (terminated) {
|
||||
log.warn('Forced gateway process termination completed after quit timeout');
|
||||
logManager.warn('Forced gateway process termination completed after quit timeout');
|
||||
}
|
||||
} catch (error) {
|
||||
log.warn('Forced gateway termination failed after quit timeout:', error);
|
||||
logManager.captureException('Forced gateway termination failed after quit timeout', error);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
await shutdownTelemetry();
|
||||
} catch (error) {
|
||||
log.warn('Telemetry shutdown failed during app quit:', error);
|
||||
logManager.captureException('Telemetry shutdown failed during app quit', error);
|
||||
}
|
||||
|
||||
gatewayQuitCleanupCompleted = true;
|
||||
app.quit();
|
||||
}).catch((error) => {
|
||||
gatewayQuitCleanupInProgress = false;
|
||||
log.warn('Gateway quit cleanup failed:', error);
|
||||
logManager.captureException('Gateway quit cleanup failed', error);
|
||||
gatewayQuitCleanupCompleted = true;
|
||||
app.quit();
|
||||
});
|
||||
});
|
||||
|
||||
app.whenReady().then(async () => {
|
||||
logStartupCheckpoint('app-ready');
|
||||
await configManager.init();
|
||||
logStartupCheckpoint('config-initialized');
|
||||
await syncLaunchAtStartupSettingFromConfig();
|
||||
logStartupCheckpoint('launch-at-startup-synced');
|
||||
await themeManager.init();
|
||||
logStartupCheckpoint('theme-initialized');
|
||||
await initTelemetry();
|
||||
logStartupCheckpoint('telemetry-initialized');
|
||||
bindGatewayLifecycleEvents();
|
||||
|
||||
try {
|
||||
@@ -386,8 +401,9 @@ app.whenReady().then(async () => {
|
||||
);
|
||||
},
|
||||
});
|
||||
logStartupCheckpoint('host-api-server-started');
|
||||
} catch (error) {
|
||||
log.error('Failed to start Host API server:', error);
|
||||
logManager.captureException('Failed to start Host API server', error);
|
||||
}
|
||||
|
||||
let launchAtStartup = Boolean(configManager.get(CONFIG_KEYS.LAUNCH_AT_STARTUP));
|
||||
@@ -409,23 +425,25 @@ app.whenReady().then(async () => {
|
||||
});
|
||||
|
||||
void ensureBuiltinSkillsInstalled().catch((error) => {
|
||||
log.warn('Failed to install built-in skills:', error);
|
||||
logManager.captureException('Failed to install built-in skills', error);
|
||||
});
|
||||
|
||||
void ensurePreinstalledSkillsInstalled().catch((error) => {
|
||||
log.warn('Failed to install preinstalled skills:', error);
|
||||
logManager.captureException('Failed to install preinstalled skills', error);
|
||||
});
|
||||
|
||||
try {
|
||||
await syncGatewayConfigBeforeLaunch();
|
||||
logStartupCheckpoint('gateway-config-synced');
|
||||
} catch (error) {
|
||||
log.warn('Failed to sync OpenClaw config before launch:', error);
|
||||
logManager.captureException('Failed to sync OpenClaw config before launch', error);
|
||||
}
|
||||
|
||||
refreshProviderRuntime();
|
||||
void gatewayManager.init().catch((error) => {
|
||||
log.warn('Failed to initialize GatewayManager:', error);
|
||||
logManager.captureException('Failed to initialize GatewayManager', error);
|
||||
});
|
||||
logStartupCheckpoint('gateway-init-requested');
|
||||
|
||||
onProviderChange(() => {
|
||||
const runtimeSync = refreshProviderRuntime();
|
||||
@@ -437,14 +455,20 @@ app.whenReady().then(async () => {
|
||||
});
|
||||
|
||||
setupMainWindow();
|
||||
logStartupCheckpoint('main-window-requested');
|
||||
|
||||
// 初始化脚本存储服务
|
||||
initScriptStoreService()
|
||||
logStartupCheckpoint('script-store-initialized');
|
||||
|
||||
// 开启任务操作子进程
|
||||
runTaskOperationService()
|
||||
logStartupCheckpoint('task-operation-service-started');
|
||||
|
||||
// 开启subagent子进程
|
||||
}).catch((error) => {
|
||||
logManager.captureException('Fatal error during app bootstrap', error);
|
||||
emergencyGatewayCleanup('Fatal error during app bootstrap', error);
|
||||
});
|
||||
|
||||
// Quit when all windows are closed, except on macOS. There, it's common
|
||||
@@ -452,7 +476,7 @@ app.whenReady().then(async () => {
|
||||
// explicitly with Cmd + Q.
|
||||
app.on('window-all-closed', () => {
|
||||
if (process.platform !== 'darwin' && !configManager.get(CONFIG_KEYS.MINIMIZE_TO_TRAY)) {
|
||||
log.info('app closing due to all windows being closed');
|
||||
logManager.info('app closing due to all windows being closed');
|
||||
app.quit();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { IPC_EVENTS } from '@runtime/lib/constants';
|
||||
import { promisify } from 'util';
|
||||
import { ipcMain } from 'electron';
|
||||
import { app, ipcMain } from 'electron';
|
||||
import log from 'electron-log';
|
||||
import * as path from 'path';
|
||||
import * as fs from 'fs';
|
||||
import { getUserDataDir } from '@electron/utils/paths';
|
||||
import { serializeUnknownError } from '@electron/utils/log-helpers';
|
||||
|
||||
// 转换为Promise形式的fs方法
|
||||
const readdirAsync = promisify(fs.readdir);
|
||||
@@ -35,6 +36,12 @@ class LogService {
|
||||
this.error('Failed to create log directory:', err);
|
||||
}
|
||||
|
||||
try {
|
||||
app.setAppLogsPath(logPath);
|
||||
} catch (err) {
|
||||
this.error('Failed to set app logs path:', err);
|
||||
}
|
||||
|
||||
// 配置electron-log
|
||||
log.transports.file.resolvePathFn = () => {
|
||||
// 使用当前日期作为日志文件名,格式为 YYYY-MM-DD.log
|
||||
@@ -48,6 +55,7 @@ class LogService {
|
||||
|
||||
// 配置日志文件大小限制,默认10MB
|
||||
log.transports.file.maxSize = 10 * 1024 * 1024; // 10MB
|
||||
log.transports.file.sync = true;
|
||||
|
||||
// 配置控制台日志级别,开发环境可以设置为debug,生产环境可以设置为info
|
||||
log.transports.console.level = process.env.NODE_ENV === 'development' ? 'debug' : 'info';
|
||||
@@ -160,6 +168,13 @@ class LogService {
|
||||
log.error(message, ...meta);
|
||||
}
|
||||
|
||||
public captureException(message: string, error: unknown, extra: Record<string, unknown> = {}): void {
|
||||
this.error(message, {
|
||||
...extra,
|
||||
error: serializeUnknownError(error),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
public logApiRequest(endpoint: string, data: any = {}, method: string = 'POST'): void {
|
||||
this.info(`API Request: ${endpoint}, Method: ${method}, Request: ${JSON.stringify(data)}`);
|
||||
@@ -219,6 +234,14 @@ class LogService {
|
||||
}
|
||||
}
|
||||
|
||||
public getLogDirectoryPath(): string {
|
||||
return this.logDirPath;
|
||||
}
|
||||
|
||||
public getCurrentLogFilePath(): string {
|
||||
return this._getCurrentLogFilePath();
|
||||
}
|
||||
|
||||
private _getCurrentLogFilePath(): string {
|
||||
const today = new Date();
|
||||
const formattedDate = `${today.getFullYear()}-${String(today.getMonth() + 1).padStart(2, '0')}-${String(today.getDate()).padStart(2, '0')}`;
|
||||
|
||||
66
electron/utils/log-helpers.ts
Normal file
66
electron/utils/log-helpers.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
export type SerializedError = {
|
||||
name?: string;
|
||||
message: string;
|
||||
stack?: string;
|
||||
code?: string | number;
|
||||
cause?: unknown;
|
||||
};
|
||||
|
||||
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
||||
}
|
||||
|
||||
export function serializeError(error: Error): SerializedError {
|
||||
const code = (error as Error & { code?: string | number }).code;
|
||||
const cause = (error as Error & { cause?: unknown }).cause;
|
||||
|
||||
return {
|
||||
name: error.name,
|
||||
message: error.message,
|
||||
stack: error.stack,
|
||||
...(typeof code !== 'undefined' ? { code } : {}),
|
||||
...(typeof cause !== 'undefined' ? { cause: serializeUnknownForLog(cause) } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
export function serializeUnknownError(error: unknown): SerializedError {
|
||||
if (error instanceof Error) {
|
||||
return serializeError(error);
|
||||
}
|
||||
|
||||
if (typeof error === 'string') {
|
||||
return { message: error };
|
||||
}
|
||||
|
||||
if (isPlainObject(error) && typeof error.message === 'string') {
|
||||
return {
|
||||
message: error.message,
|
||||
...(typeof error.name === 'string' ? { name: error.name } : {}),
|
||||
...(typeof error.stack === 'string' ? { stack: error.stack } : {}),
|
||||
...(typeof error.code === 'string' || typeof error.code === 'number' ? { code: error.code } : {}),
|
||||
...(typeof error.cause !== 'undefined' ? { cause: serializeUnknownForLog(error.cause) } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
message: `Non-Error value thrown: ${String(error)}`,
|
||||
};
|
||||
}
|
||||
|
||||
export function serializeUnknownForLog(value: unknown): unknown {
|
||||
if (value instanceof Error) {
|
||||
return serializeError(value);
|
||||
}
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
return value.map((item) => serializeUnknownForLog(item));
|
||||
}
|
||||
|
||||
if (isPlainObject(value)) {
|
||||
return Object.fromEntries(
|
||||
Object.entries(value).map(([key, entryValue]) => [key, serializeUnknownForLog(entryValue)]),
|
||||
);
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
160
electron/utils/runtime-diagnostics.ts
Normal file
160
electron/utils/runtime-diagnostics.ts
Normal file
@@ -0,0 +1,160 @@
|
||||
import { app, crashReporter, type BrowserWindow, type WebContents } from 'electron';
|
||||
import path from 'node:path';
|
||||
import logManager from '@electron/service/logger';
|
||||
import { ensureDir, getUserDataDir } from './paths';
|
||||
import { serializeUnknownError, serializeUnknownForLog } from './log-helpers';
|
||||
|
||||
let diagnosticsInstalled = false;
|
||||
let crashReporterStarted = false;
|
||||
|
||||
function describeWebContents(webContents: WebContents): Record<string, unknown> {
|
||||
return {
|
||||
webContentsId: webContents.id,
|
||||
type: webContents.getType(),
|
||||
url: webContents.getURL(),
|
||||
loading: webContents.isLoading(),
|
||||
destroyed: webContents.isDestroyed(),
|
||||
};
|
||||
}
|
||||
|
||||
function describeWindow(window: BrowserWindow): Record<string, unknown> {
|
||||
return {
|
||||
windowId: window.id,
|
||||
title: window.getTitle(),
|
||||
visible: window.isVisible(),
|
||||
minimized: window.isMinimized(),
|
||||
maximized: window.isMaximized(),
|
||||
destroyed: window.isDestroyed(),
|
||||
...describeWebContents(window.webContents),
|
||||
};
|
||||
}
|
||||
|
||||
function getCrashDumpsDirectoryPath(): string {
|
||||
return ensureDir(path.join(getUserDataDir(), 'logs', 'crashDumps'));
|
||||
}
|
||||
|
||||
function attachWindowDiagnostics(window: BrowserWindow): void {
|
||||
logManager.info('BrowserWindow created', describeWindow(window));
|
||||
|
||||
window.on('unresponsive', () => {
|
||||
logManager.warn('BrowserWindow became unresponsive', describeWindow(window));
|
||||
});
|
||||
|
||||
window.on('responsive', () => {
|
||||
logManager.info('BrowserWindow became responsive again', describeWindow(window));
|
||||
});
|
||||
|
||||
window.webContents.on('did-fail-load', (_event, errorCode, errorDescription, validatedURL, isMainFrame, frameProcessId, frameRoutingId) => {
|
||||
logManager.error('WebContents failed to load', {
|
||||
...describeWindow(window),
|
||||
errorCode,
|
||||
errorDescription,
|
||||
validatedURL,
|
||||
isMainFrame,
|
||||
frameProcessId,
|
||||
frameRoutingId,
|
||||
});
|
||||
});
|
||||
|
||||
window.webContents.on('render-process-gone', (_event, details) => {
|
||||
logManager.error('Window render process exited unexpectedly', {
|
||||
...describeWindow(window),
|
||||
reason: details.reason,
|
||||
exitCode: details.exitCode,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export function logStartupCheckpoint(stage: string, details: Record<string, unknown> = {}): void {
|
||||
logManager.info(`startup:${stage}`, {
|
||||
pid: process.pid,
|
||||
packaged: app.isPackaged,
|
||||
...details,
|
||||
});
|
||||
}
|
||||
|
||||
export function startLocalCrashReporter(): void {
|
||||
if (crashReporterStarted) {
|
||||
return;
|
||||
}
|
||||
|
||||
const crashDumpsDir = getCrashDumpsDirectoryPath();
|
||||
|
||||
try {
|
||||
app.setPath('crashDumps', crashDumpsDir);
|
||||
} catch (error) {
|
||||
logManager.captureException('Failed to set crashDumps path', error, { crashDumpsDir });
|
||||
}
|
||||
|
||||
try {
|
||||
crashReporter.start({
|
||||
companyName: 'ZhiNian Team',
|
||||
productName: app.getName() || 'NIANXX',
|
||||
submitURL: 'https://127.0.0.1/disabled-crash-upload',
|
||||
uploadToServer: false,
|
||||
compress: true,
|
||||
ignoreSystemCrashHandler: false,
|
||||
rateLimit: false,
|
||||
globalExtra: {
|
||||
appVersion: app.getVersion(),
|
||||
packaged: String(app.isPackaged),
|
||||
platform: process.platform,
|
||||
arch: process.arch,
|
||||
},
|
||||
});
|
||||
|
||||
crashReporterStarted = true;
|
||||
logManager.info('Crash reporter started for local dump collection', {
|
||||
crashDumpsDir,
|
||||
uploadToServer: false,
|
||||
});
|
||||
} catch (error) {
|
||||
logManager.captureException('Failed to start crash reporter', error, { crashDumpsDir });
|
||||
}
|
||||
}
|
||||
|
||||
export function installRuntimeDiagnostics(): void {
|
||||
if (diagnosticsInstalled) {
|
||||
return;
|
||||
}
|
||||
|
||||
diagnosticsInstalled = true;
|
||||
|
||||
process.on('warning', (warning) => {
|
||||
logManager.warn('Node.js process warning', serializeUnknownError(warning));
|
||||
});
|
||||
|
||||
process.on('uncaughtExceptionMonitor', (error) => {
|
||||
logManager.captureException('uncaughtExceptionMonitor', error);
|
||||
});
|
||||
|
||||
process.on('unhandledRejection', (reason) => {
|
||||
logManager.error('unhandledRejection observed by runtime diagnostics', {
|
||||
reason: serializeUnknownForLog(reason),
|
||||
});
|
||||
});
|
||||
|
||||
app.on('browser-window-created', (_event, window) => {
|
||||
attachWindowDiagnostics(window);
|
||||
});
|
||||
|
||||
app.on('render-process-gone', (_event, webContents, details) => {
|
||||
logManager.error('App render process exited unexpectedly', {
|
||||
...describeWebContents(webContents),
|
||||
reason: details.reason,
|
||||
exitCode: details.exitCode,
|
||||
});
|
||||
});
|
||||
|
||||
app.on('child-process-gone', (_event, details) => {
|
||||
logManager.error('Child process exited unexpectedly', serializeUnknownForLog(details));
|
||||
});
|
||||
|
||||
logManager.info('Runtime diagnostics installed', {
|
||||
platform: process.platform,
|
||||
arch: process.arch,
|
||||
pid: process.pid,
|
||||
userDataDir: getUserDataDir(),
|
||||
logsDir: logManager.getLogDirectoryPath(),
|
||||
});
|
||||
}
|
||||
46
tests/log-helpers.test.ts
Normal file
46
tests/log-helpers.test.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { serializeError, serializeUnknownError, serializeUnknownForLog } from '../electron/utils/log-helpers';
|
||||
|
||||
describe('log helpers', () => {
|
||||
it('serializes Error instances with code and cause', () => {
|
||||
const cause = new Error('inner failure');
|
||||
const error = new Error('outer failure') as Error & { code?: string; cause?: unknown };
|
||||
error.code = 'E_TEST';
|
||||
error.cause = cause;
|
||||
|
||||
expect(serializeError(error)).toMatchObject({
|
||||
name: 'Error',
|
||||
message: 'outer failure',
|
||||
code: 'E_TEST',
|
||||
cause: {
|
||||
name: 'Error',
|
||||
message: 'inner failure',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('normalizes string and object rejection values', () => {
|
||||
expect(serializeUnknownError('boom')).toEqual({ message: 'boom' });
|
||||
expect(serializeUnknownError({ message: 'failed', code: 500 })).toEqual({
|
||||
message: 'failed',
|
||||
code: 500,
|
||||
});
|
||||
});
|
||||
|
||||
it('recursively serializes nested error values for structured logging', () => {
|
||||
const payload = serializeUnknownForLog({
|
||||
task: 'bootstrap',
|
||||
errors: [new Error('bad dependency')],
|
||||
});
|
||||
|
||||
expect(payload).toEqual({
|
||||
task: 'bootstrap',
|
||||
errors: [
|
||||
expect.objectContaining({
|
||||
name: 'Error',
|
||||
message: 'bad dependency',
|
||||
}),
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -16,6 +16,12 @@ const projectAliases = {
|
||||
function isMainProcessExternal(id: string): boolean {
|
||||
if (!id || id.startsWith('\0')) return false;
|
||||
if (id.startsWith('.') || id.startsWith('/') || /^[A-Za-z]:[\\/]/.test(id)) return false;
|
||||
|
||||
// Keep ws bundled into the Electron main chunk.
|
||||
// The packaged app imports it directly from gateway runtime code,
|
||||
// and relying on transitive/peer installation causes "Cannot find module 'ws'"
|
||||
// in production builds.
|
||||
if (id === 'ws' || id.startsWith('ws/')) return false;
|
||||
|
||||
// Project-specific aliases that should be bundled (not external)
|
||||
const internalAliases = [
|
||||
|
||||
Reference in New Issue
Block a user