Chore/build npm (#9)
Co-authored-by: DigHuang <114602213+DigHuang@users.noreply.github.com> Co-authored-by: Felix <24791380+vcfgv@users.noreply.github.com> Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -13,13 +13,13 @@ import {
|
||||
getOpenClawDir,
|
||||
getOpenClawEntryPath,
|
||||
isOpenClawBuilt,
|
||||
isOpenClawSubmodulePresent,
|
||||
isOpenClawInstalled
|
||||
isOpenClawPresent
|
||||
} from '../utils/paths';
|
||||
import { getSetting } from '../utils/store';
|
||||
import { getApiKey } from '../utils/secure-storage';
|
||||
import { getProviderEnvVar } from '../utils/openclaw-auth';
|
||||
import { GatewayEventType, JsonRpcNotification, isNotification, isResponse } from './protocol';
|
||||
import { logger } from '../utils/logger';
|
||||
|
||||
/**
|
||||
* Gateway connection status
|
||||
@@ -107,6 +107,7 @@ export class GatewayManager extends EventEmitter {
|
||||
*/
|
||||
async start(): Promise<void> {
|
||||
if (this.status.state === 'running') {
|
||||
logger.info('Gateway already running, skipping start');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -116,27 +117,34 @@ export class GatewayManager extends EventEmitter {
|
||||
|
||||
try {
|
||||
// Check if Gateway is already running
|
||||
logger.info('Checking for existing Gateway...');
|
||||
const existing = await this.findExistingGateway();
|
||||
if (existing) {
|
||||
console.log('Found existing Gateway on port', existing.port);
|
||||
logger.info(`Found existing Gateway on port ${existing.port}`);
|
||||
await this.connect(existing.port);
|
||||
this.startHealthCheck();
|
||||
return;
|
||||
}
|
||||
|
||||
logger.info('No existing Gateway found, starting new process...');
|
||||
|
||||
// Start new Gateway process
|
||||
await this.startProcess();
|
||||
|
||||
// Wait for Gateway to be ready
|
||||
logger.info('Waiting for Gateway to be ready...');
|
||||
await this.waitForReady();
|
||||
|
||||
// Connect WebSocket
|
||||
logger.info('Connecting WebSocket...');
|
||||
await this.connect(this.status.port);
|
||||
|
||||
// Start health monitoring
|
||||
this.startHealthCheck();
|
||||
logger.info('Gateway started successfully');
|
||||
|
||||
} catch (error) {
|
||||
logger.error('Gateway start failed:', error);
|
||||
this.setStatus({ state: 'error', error: String(error) });
|
||||
throw error;
|
||||
}
|
||||
@@ -331,72 +339,84 @@ export class GatewayManager extends EventEmitter {
|
||||
|
||||
/**
|
||||
* Start Gateway process
|
||||
* Uses OpenClaw submodule - supports both production (dist) and development modes
|
||||
* Uses OpenClaw npm package from node_modules (dev) or resources (production)
|
||||
*/
|
||||
private async startProcess(): Promise<void> {
|
||||
const openclawDir = getOpenClawDir();
|
||||
const entryScript = getOpenClawEntryPath();
|
||||
|
||||
// Verify OpenClaw submodule exists
|
||||
if (!isOpenClawSubmodulePresent()) {
|
||||
throw new Error(
|
||||
'OpenClaw submodule not found. Please run: git submodule update --init'
|
||||
);
|
||||
}
|
||||
logger.info('=== Gateway startProcess begin ===');
|
||||
logger.info(`app.isPackaged: ${app.isPackaged}`);
|
||||
logger.info(`openclawDir: ${openclawDir}`);
|
||||
logger.info(`entryScript: ${entryScript}`);
|
||||
logger.info(`openclawDir exists: ${existsSync(openclawDir)}`);
|
||||
logger.info(`entryScript exists: ${existsSync(entryScript)}`);
|
||||
logger.info(`process.execPath: ${process.execPath}`);
|
||||
logger.info(`process.resourcesPath: ${process.resourcesPath}`);
|
||||
logger.info(`process.cwd(): ${process.cwd()}`);
|
||||
logger.info(`process.platform: ${process.platform}, process.arch: ${process.arch}`);
|
||||
|
||||
// Verify dependencies are installed
|
||||
if (!isOpenClawInstalled()) {
|
||||
throw new Error(
|
||||
'OpenClaw dependencies not installed. Please run: cd openclaw && pnpm install'
|
||||
);
|
||||
// Verify OpenClaw package exists
|
||||
if (!isOpenClawPresent()) {
|
||||
const errMsg = `OpenClaw package not found at: ${openclawDir}`;
|
||||
logger.error(errMsg);
|
||||
throw new Error(errMsg);
|
||||
}
|
||||
|
||||
// Get or generate gateway token
|
||||
const gatewayToken = await getSetting('gatewayToken');
|
||||
console.log('Using gateway token:', gatewayToken.substring(0, 10) + '...');
|
||||
logger.info(`Using gateway token: ${gatewayToken.substring(0, 10)}...`);
|
||||
|
||||
let command: string;
|
||||
let args: string[];
|
||||
|
||||
// Check if OpenClaw is built (production mode) or use pnpm dev mode
|
||||
if (isOpenClawBuilt() && existsSync(entryScript)) {
|
||||
// Production mode: use openclaw.mjs directly
|
||||
console.log('Starting Gateway in production mode (using dist)');
|
||||
// Determine the Node.js executable
|
||||
// In packaged Electron app, use process.execPath with ELECTRON_RUN_AS_NODE=1
|
||||
// which makes the Electron binary behave as plain Node.js.
|
||||
// In development, use system 'node'.
|
||||
const gatewayArgs = ['gateway', '--port', String(this.status.port), '--token', gatewayToken, '--dev', '--allow-unconfigured'];
|
||||
|
||||
if (app.isPackaged) {
|
||||
// Production: always use Electron binary as Node.js via ELECTRON_RUN_AS_NODE
|
||||
if (existsSync(entryScript)) {
|
||||
command = process.execPath;
|
||||
args = [entryScript, ...gatewayArgs];
|
||||
logger.info('Starting Gateway in PACKAGED mode (ELECTRON_RUN_AS_NODE)');
|
||||
} else {
|
||||
const errMsg = `OpenClaw entry script not found at: ${entryScript}`;
|
||||
logger.error(errMsg);
|
||||
throw new Error(errMsg);
|
||||
}
|
||||
} else if (isOpenClawBuilt() && existsSync(entryScript)) {
|
||||
// Development with built package: use system node
|
||||
command = 'node';
|
||||
args = [entryScript, 'gateway', '--port', String(this.status.port), '--token', gatewayToken, '--dev', '--allow-unconfigured'];
|
||||
args = [entryScript, ...gatewayArgs];
|
||||
logger.info('Starting Gateway in DEV mode (node + built dist)');
|
||||
} else {
|
||||
// Development mode: use pnpm gateway:dev which handles tsx compilation
|
||||
console.log('Starting Gateway in development mode (using pnpm)');
|
||||
// Development without build: use pnpm dev
|
||||
command = 'pnpm';
|
||||
args = ['run', 'dev', 'gateway', '--port', String(this.status.port), '--token', gatewayToken, '--dev', '--allow-unconfigured'];
|
||||
args = ['run', 'dev', ...gatewayArgs];
|
||||
logger.info('Starting Gateway in DEV mode (pnpm dev)');
|
||||
}
|
||||
|
||||
console.log(`Spawning Gateway: ${command} ${args.join(' ')}`);
|
||||
console.log(`Working directory: ${openclawDir}`);
|
||||
logger.info(`Spawning: ${command} ${args.join(' ')}`);
|
||||
logger.info(`Working directory: ${openclawDir}`);
|
||||
|
||||
// Resolve bundled bin path for uv
|
||||
let binPath = '';
|
||||
const platform = process.platform;
|
||||
const arch = process.arch;
|
||||
// Map arch if necessary (e.g. x64 is standard, but ensure consistency with script)
|
||||
const target = `${platform}-${arch}`;
|
||||
|
||||
if (app.isPackaged) {
|
||||
// In production, we flattened the structure to 'bin/' using electron-builder macros
|
||||
binPath = path.join(process.resourcesPath, 'bin');
|
||||
} else {
|
||||
// In dev, resources are at project root/resources/bin/<platform>-<arch>
|
||||
binPath = path.join(process.cwd(), 'resources', 'bin', target);
|
||||
}
|
||||
const binPath = app.isPackaged
|
||||
? path.join(process.resourcesPath, 'bin')
|
||||
: path.join(process.cwd(), 'resources', 'bin', target);
|
||||
|
||||
// Only inject if the bundled directory exists
|
||||
const finalPath = existsSync(binPath)
|
||||
const binPathExists = existsSync(binPath);
|
||||
const finalPath = binPathExists
|
||||
? `${binPath}${path.delimiter}${process.env.PATH || ''}`
|
||||
: process.env.PATH || '';
|
||||
|
||||
if (existsSync(binPath)) {
|
||||
console.log('Injecting bundled bin path:', binPath);
|
||||
}
|
||||
logger.info(`Bundled bin path: ${binPath}, exists: ${binPathExists}`);
|
||||
|
||||
// Load provider API keys from secure storage to pass as environment variables
|
||||
const providerEnv: Record<string, string> = {};
|
||||
@@ -408,72 +428,73 @@ export class GatewayManager extends EventEmitter {
|
||||
const envVar = getProviderEnvVar(providerType);
|
||||
if (envVar) {
|
||||
providerEnv[envVar] = key;
|
||||
console.log(`Loaded API key for ${providerType} -> ${envVar}`);
|
||||
logger.info(`Loaded API key for ${providerType} -> ${envVar}`);
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn(`Failed to load API key for ${providerType}:`, err);
|
||||
logger.warn(`Failed to load API key for ${providerType}:`, err);
|
||||
}
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const spawnEnv: Record<string, string | undefined> = {
|
||||
...process.env,
|
||||
PATH: finalPath,
|
||||
...providerEnv,
|
||||
OPENCLAW_GATEWAY_TOKEN: gatewayToken,
|
||||
OPENCLAW_SKIP_CHANNELS: '',
|
||||
CLAWDBOT_SKIP_CHANNELS: '',
|
||||
};
|
||||
|
||||
// Critical: In packaged mode, make Electron binary act as Node.js
|
||||
if (app.isPackaged) {
|
||||
spawnEnv['ELECTRON_RUN_AS_NODE'] = '1';
|
||||
}
|
||||
|
||||
this.process = spawn(command, args, {
|
||||
cwd: openclawDir,
|
||||
stdio: ['ignore', 'pipe', 'pipe'],
|
||||
detached: false,
|
||||
shell: process.platform === 'win32', // Use shell on Windows for pnpm
|
||||
env: {
|
||||
...process.env,
|
||||
PATH: finalPath, // Inject bundled bin path if it exists
|
||||
// Provider API keys
|
||||
...providerEnv,
|
||||
// Also set token via environment variable as fallback
|
||||
OPENCLAW_GATEWAY_TOKEN: gatewayToken,
|
||||
// Ensure OPENCLAW_SKIP_CHANNELS is NOT set so channels auto-start
|
||||
// and config hot-reload can restart channels when config changes
|
||||
OPENCLAW_SKIP_CHANNELS: '',
|
||||
CLAWDBOT_SKIP_CHANNELS: '',
|
||||
},
|
||||
shell: !app.isPackaged && process.platform === 'win32', // shell only in dev on Windows
|
||||
env: spawnEnv,
|
||||
});
|
||||
|
||||
this.process.on('error', (error) => {
|
||||
console.error('Gateway process error:', error);
|
||||
logger.error('Gateway process spawn error:', error);
|
||||
reject(error);
|
||||
});
|
||||
|
||||
this.process.on('exit', (code) => {
|
||||
console.log('Gateway process exited with code:', code);
|
||||
logger.info(`Gateway process exited with code: ${code}`);
|
||||
this.emit('exit', code);
|
||||
|
||||
if (this.status.state === 'running') {
|
||||
this.setStatus({ state: 'stopped' });
|
||||
// Attempt to reconnect
|
||||
this.scheduleReconnect();
|
||||
}
|
||||
});
|
||||
|
||||
// Log stdout
|
||||
this.process.stdout?.on('data', (data) => {
|
||||
console.log('Gateway:', data.toString());
|
||||
const msg = data.toString().trimEnd();
|
||||
logger.debug(`[Gateway stdout] ${msg}`);
|
||||
});
|
||||
|
||||
// Log stderr (filter out noisy control-ui token_mismatch messages)
|
||||
// Log stderr
|
||||
this.process.stderr?.on('data', (data) => {
|
||||
const msg = data.toString();
|
||||
// Suppress the constant Control UI token_mismatch noise
|
||||
// These come from the browser-based Control UI auto-polling with no token
|
||||
if (msg.includes('openclaw-control-ui') && msg.includes('token_mismatch')) {
|
||||
return;
|
||||
}
|
||||
if (msg.includes('closed before connect') && msg.includes('token mismatch')) {
|
||||
return;
|
||||
}
|
||||
console.error('Gateway error:', msg);
|
||||
const msg = data.toString().trimEnd();
|
||||
// Suppress noisy control-ui token_mismatch messages
|
||||
if (msg.includes('openclaw-control-ui') && msg.includes('token_mismatch')) return;
|
||||
if (msg.includes('closed before connect') && msg.includes('token mismatch')) return;
|
||||
logger.warn(`[Gateway stderr] ${msg}`);
|
||||
});
|
||||
|
||||
// Store PID
|
||||
if (this.process.pid) {
|
||||
logger.info(`Gateway process PID: ${this.process.pid}`);
|
||||
this.setStatus({ pid: this.process.pid });
|
||||
} else {
|
||||
logger.warn('Gateway process spawned but PID is undefined');
|
||||
}
|
||||
|
||||
resolve();
|
||||
@@ -486,7 +507,6 @@ export class GatewayManager extends EventEmitter {
|
||||
private async waitForReady(retries = 30, interval = 1000): Promise<void> {
|
||||
for (let i = 0; i < retries; i++) {
|
||||
try {
|
||||
// Try a quick WebSocket connection to see if the gateway is listening
|
||||
const ready = await new Promise<boolean>((resolve) => {
|
||||
const testWs = new WebSocket(`ws://localhost:${this.status.port}/ws`);
|
||||
const timeout = setTimeout(() => {
|
||||
@@ -507,16 +527,22 @@ export class GatewayManager extends EventEmitter {
|
||||
});
|
||||
|
||||
if (ready) {
|
||||
logger.info(`Gateway ready after ${i + 1} attempt(s)`);
|
||||
return;
|
||||
}
|
||||
} catch {
|
||||
// Gateway not ready yet
|
||||
}
|
||||
|
||||
if (i > 0 && i % 5 === 0) {
|
||||
logger.info(`Still waiting for Gateway... (attempt ${i + 1}/${retries})`);
|
||||
}
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, interval));
|
||||
}
|
||||
|
||||
throw new Error('Gateway failed to start');
|
||||
logger.error(`Gateway failed to become ready after ${retries} attempts on port ${this.status.port}`);
|
||||
throw new Error(`Gateway failed to start after ${retries} retries (port ${this.status.port})`);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -10,6 +10,7 @@ import { createTray } from './tray';
|
||||
import { createMenu } from './menu';
|
||||
|
||||
import { appUpdater, registerUpdateHandlers } from './updater';
|
||||
import { logger } from '../utils/logger';
|
||||
|
||||
// Disable GPU acceleration for better compatibility
|
||||
app.disableHardwareAcceleration();
|
||||
@@ -69,6 +70,17 @@ function createWindow(): BrowserWindow {
|
||||
* Initialize the application
|
||||
*/
|
||||
async function initialize(): Promise<void> {
|
||||
// Initialize logger first
|
||||
logger.init();
|
||||
logger.info('=== ClawX Application Starting ===');
|
||||
logger.info(`Platform: ${process.platform}, Arch: ${process.arch}`);
|
||||
logger.info(`Electron: ${process.versions.electron}, Node: ${process.versions.node}`);
|
||||
logger.info(`App path: ${app.getAppPath()}`);
|
||||
logger.info(`User data: ${app.getPath('userData')}`);
|
||||
logger.info(`Is packaged: ${app.isPackaged}`);
|
||||
logger.info(`Resources path: ${process.resourcesPath}`);
|
||||
logger.info(`Exec path: ${process.execPath}`);
|
||||
|
||||
// Set application menu
|
||||
createMenu();
|
||||
|
||||
@@ -129,10 +141,11 @@ async function initialize(): Promise<void> {
|
||||
|
||||
// Start Gateway automatically (optional based on settings)
|
||||
try {
|
||||
logger.info('Auto-starting Gateway...');
|
||||
await gatewayManager.start();
|
||||
console.log('Gateway started successfully');
|
||||
logger.info('Gateway auto-start succeeded');
|
||||
} catch (error) {
|
||||
console.error('Failed to start Gateway:', error);
|
||||
logger.error('Gateway auto-start failed:', error);
|
||||
// Notify renderer about the error
|
||||
mainWindow?.webContents.send('gateway:error', String(error));
|
||||
}
|
||||
|
||||
@@ -20,9 +20,10 @@ import {
|
||||
isEncryptionAvailable,
|
||||
type ProviderConfig,
|
||||
} from '../utils/secure-storage';
|
||||
import { getOpenClawStatus } from '../utils/paths';
|
||||
import { getOpenClawStatus, getOpenClawDir } from '../utils/paths';
|
||||
import { getSetting } from '../utils/store';
|
||||
import { saveProviderKeyToOpenClaw, setOpenClawDefaultModel } from '../utils/openclaw-auth';
|
||||
import { logger } from '../utils/logger';
|
||||
import {
|
||||
saveChannelConfig,
|
||||
getChannelConfig,
|
||||
@@ -68,6 +69,9 @@ export function registerIpcHandlers(
|
||||
// UV handlers
|
||||
registerUvHandlers();
|
||||
|
||||
// Log handlers (for UI to read gateway/app logs)
|
||||
registerLogHandlers();
|
||||
|
||||
// Skill config handlers (direct file access, no Gateway RPC)
|
||||
registerSkillConfigHandlers();
|
||||
|
||||
@@ -306,6 +310,37 @@ function registerUvHandlers(): void {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Log-related IPC handlers
|
||||
* Allows the renderer to read application logs for diagnostics
|
||||
*/
|
||||
function registerLogHandlers(): void {
|
||||
// Get recent logs from memory ring buffer
|
||||
ipcMain.handle('log:getRecent', async (_, count?: number) => {
|
||||
return logger.getRecentLogs(count);
|
||||
});
|
||||
|
||||
// Read log file content (last N lines)
|
||||
ipcMain.handle('log:readFile', async (_, tailLines?: number) => {
|
||||
return logger.readLogFile(tailLines);
|
||||
});
|
||||
|
||||
// Get log file path (so user can open in file explorer)
|
||||
ipcMain.handle('log:getFilePath', async () => {
|
||||
return logger.getLogFilePath();
|
||||
});
|
||||
|
||||
// Get log directory path
|
||||
ipcMain.handle('log:getDir', async () => {
|
||||
return logger.getLogDir();
|
||||
});
|
||||
|
||||
// List all log files
|
||||
ipcMain.handle('log:listFiles', async () => {
|
||||
return logger.listLogFiles();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Gateway-related IPC handlers
|
||||
*/
|
||||
@@ -433,19 +468,26 @@ function registerGatewayHandlers(
|
||||
|
||||
/**
|
||||
* OpenClaw-related IPC handlers
|
||||
* For checking submodule status and channel configuration
|
||||
* For checking package status and channel configuration
|
||||
*/
|
||||
function registerOpenClawHandlers(): void {
|
||||
|
||||
// Get OpenClaw submodule status
|
||||
// Get OpenClaw package status
|
||||
ipcMain.handle('openclaw:status', () => {
|
||||
return getOpenClawStatus();
|
||||
const status = getOpenClawStatus();
|
||||
logger.info('openclaw:status IPC called', status);
|
||||
return status;
|
||||
});
|
||||
|
||||
// Check if OpenClaw is ready (submodule present and dependencies installed)
|
||||
// Check if OpenClaw is ready (package present)
|
||||
ipcMain.handle('openclaw:isReady', () => {
|
||||
const status = getOpenClawStatus();
|
||||
return status.submoduleExists && status.isInstalled;
|
||||
return status.packageExists;
|
||||
});
|
||||
|
||||
// Get the resolved OpenClaw directory path (for diagnostics)
|
||||
ipcMain.handle('openclaw:getDir', () => {
|
||||
return getOpenClawDir();
|
||||
});
|
||||
|
||||
// ==================== Channel Configuration Handlers ====================
|
||||
|
||||
@@ -100,6 +100,14 @@ const electronAPI = {
|
||||
'skill:updateConfig',
|
||||
'skill:getConfig',
|
||||
'skill:getAllConfigs',
|
||||
// Logs
|
||||
'log:getRecent',
|
||||
'log:readFile',
|
||||
'log:getFilePath',
|
||||
'log:getDir',
|
||||
'log:listFiles',
|
||||
// OpenClaw extras
|
||||
'openclaw:getDir',
|
||||
];
|
||||
|
||||
if (validChannels.includes(channel)) {
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
/**
|
||||
* Logger Utility
|
||||
* Centralized logging with levels and file output
|
||||
* Centralized logging with levels, file output, and log retrieval for UI
|
||||
*/
|
||||
import { app } from 'electron';
|
||||
import { join } from 'path';
|
||||
import { existsSync, mkdirSync, appendFileSync } from 'fs';
|
||||
import { existsSync, mkdirSync, appendFileSync, readFileSync, readdirSync, statSync } from 'fs';
|
||||
|
||||
/**
|
||||
* Log levels
|
||||
@@ -19,26 +19,37 @@ export enum LogLevel {
|
||||
/**
|
||||
* Current log level (can be changed at runtime)
|
||||
*/
|
||||
let currentLevel = LogLevel.INFO;
|
||||
let currentLevel = LogLevel.DEBUG; // Default to DEBUG for better diagnostics
|
||||
|
||||
/**
|
||||
* Log file path
|
||||
*/
|
||||
let logFilePath: string | null = null;
|
||||
let logDir: string | null = null;
|
||||
|
||||
/**
|
||||
* Initialize logger
|
||||
* In-memory ring buffer for recent logs (useful for UI display)
|
||||
*/
|
||||
const RING_BUFFER_SIZE = 500;
|
||||
const recentLogs: string[] = [];
|
||||
|
||||
/**
|
||||
* Initialize logger — safe to call before app.isReady()
|
||||
*/
|
||||
export function initLogger(): void {
|
||||
try {
|
||||
const logDir = join(app.getPath('userData'), 'logs');
|
||||
|
||||
logDir = join(app.getPath('userData'), 'logs');
|
||||
|
||||
if (!existsSync(logDir)) {
|
||||
mkdirSync(logDir, { recursive: true });
|
||||
}
|
||||
|
||||
|
||||
const timestamp = new Date().toISOString().split('T')[0];
|
||||
logFilePath = join(logDir, `clawx-${timestamp}.log`);
|
||||
|
||||
// Write a separator for new session
|
||||
const sessionHeader = `\n${'='.repeat(80)}\n[${new Date().toISOString()}] === ClawX Session Start (v${app.getVersion()}) ===\n${'='.repeat(80)}\n`;
|
||||
appendFileSync(logFilePath, sessionHeader);
|
||||
} catch (error) {
|
||||
console.error('Failed to initialize logger:', error);
|
||||
}
|
||||
@@ -51,22 +62,53 @@ export function setLogLevel(level: LogLevel): void {
|
||||
currentLevel = level;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get log file directory path
|
||||
*/
|
||||
export function getLogDir(): string | null {
|
||||
return logDir;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current log file path
|
||||
*/
|
||||
export function getLogFilePath(): string | null {
|
||||
return logFilePath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format log message
|
||||
*/
|
||||
function formatMessage(level: string, message: string, ...args: unknown[]): string {
|
||||
const timestamp = new Date().toISOString();
|
||||
const formattedArgs = args.length > 0 ? ' ' + args.map(arg =>
|
||||
typeof arg === 'object' ? JSON.stringify(arg) : String(arg)
|
||||
).join(' ') : '';
|
||||
|
||||
return `[${timestamp}] [${level}] ${message}${formattedArgs}`;
|
||||
const formattedArgs = args.length > 0 ? ' ' + args.map(arg => {
|
||||
if (arg instanceof Error) {
|
||||
return `${arg.message}\n${arg.stack || ''}`;
|
||||
}
|
||||
if (typeof arg === 'object') {
|
||||
try {
|
||||
return JSON.stringify(arg, null, 2);
|
||||
} catch {
|
||||
return String(arg);
|
||||
}
|
||||
}
|
||||
return String(arg);
|
||||
}).join(' ') : '';
|
||||
|
||||
return `[${timestamp}] [${level.padEnd(5)}] ${message}${formattedArgs}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Write to log file
|
||||
* Write to log file and ring buffer
|
||||
*/
|
||||
function writeToFile(formatted: string): void {
|
||||
function writeLog(formatted: string): void {
|
||||
// Ring buffer
|
||||
recentLogs.push(formatted);
|
||||
if (recentLogs.length > RING_BUFFER_SIZE) {
|
||||
recentLogs.shift();
|
||||
}
|
||||
|
||||
// File
|
||||
if (logFilePath) {
|
||||
try {
|
||||
appendFileSync(logFilePath, formatted + '\n');
|
||||
@@ -83,7 +125,7 @@ export function debug(message: string, ...args: unknown[]): void {
|
||||
if (currentLevel <= LogLevel.DEBUG) {
|
||||
const formatted = formatMessage('DEBUG', message, ...args);
|
||||
console.debug(formatted);
|
||||
writeToFile(formatted);
|
||||
writeLog(formatted);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -94,7 +136,7 @@ export function info(message: string, ...args: unknown[]): void {
|
||||
if (currentLevel <= LogLevel.INFO) {
|
||||
const formatted = formatMessage('INFO', message, ...args);
|
||||
console.info(formatted);
|
||||
writeToFile(formatted);
|
||||
writeLog(formatted);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -105,7 +147,7 @@ export function warn(message: string, ...args: unknown[]): void {
|
||||
if (currentLevel <= LogLevel.WARN) {
|
||||
const formatted = formatMessage('WARN', message, ...args);
|
||||
console.warn(formatted);
|
||||
writeToFile(formatted);
|
||||
writeLog(formatted);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -116,7 +158,66 @@ export function error(message: string, ...args: unknown[]): void {
|
||||
if (currentLevel <= LogLevel.ERROR) {
|
||||
const formatted = formatMessage('ERROR', message, ...args);
|
||||
console.error(formatted);
|
||||
writeToFile(formatted);
|
||||
writeLog(formatted);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get recent logs from ring buffer (for UI display)
|
||||
* @param count Number of recent log lines to return (default: all)
|
||||
* @param minLevel Minimum log level to include (default: DEBUG)
|
||||
*/
|
||||
export function getRecentLogs(count?: number, minLevel?: LogLevel): string[] {
|
||||
const filtered = minLevel != null
|
||||
? recentLogs.filter(line => {
|
||||
if (minLevel <= LogLevel.DEBUG) return true;
|
||||
if (minLevel === LogLevel.INFO) return !line.includes('] [DEBUG');
|
||||
if (minLevel === LogLevel.WARN) return line.includes('] [WARN') || line.includes('] [ERROR');
|
||||
return line.includes('] [ERROR');
|
||||
})
|
||||
: recentLogs;
|
||||
|
||||
return count ? filtered.slice(-count) : [...filtered];
|
||||
}
|
||||
|
||||
/**
|
||||
* Read the current day's log file content (last N lines)
|
||||
*/
|
||||
export function readLogFile(tailLines = 200): string {
|
||||
if (!logFilePath || !existsSync(logFilePath)) {
|
||||
return '(No log file found)';
|
||||
}
|
||||
try {
|
||||
const content = readFileSync(logFilePath, 'utf-8');
|
||||
const lines = content.split('\n');
|
||||
if (lines.length <= tailLines) return content;
|
||||
return lines.slice(-tailLines).join('\n');
|
||||
} catch (err) {
|
||||
return `(Failed to read log file: ${err})`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* List available log files
|
||||
*/
|
||||
export function listLogFiles(): Array<{ name: string; path: string; size: number; modified: string }> {
|
||||
if (!logDir || !existsSync(logDir)) return [];
|
||||
try {
|
||||
return readdirSync(logDir)
|
||||
.filter(f => f.endsWith('.log'))
|
||||
.map(f => {
|
||||
const fullPath = join(logDir!, f);
|
||||
const stat = statSync(fullPath);
|
||||
return {
|
||||
name: f,
|
||||
path: fullPath,
|
||||
size: stat.size,
|
||||
modified: stat.mtime.toISOString(),
|
||||
};
|
||||
})
|
||||
.sort((a, b) => b.modified.localeCompare(a.modified));
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -130,4 +231,9 @@ export const logger = {
|
||||
error,
|
||||
setLevel: setLogLevel,
|
||||
init: initLogger,
|
||||
getLogDir,
|
||||
getLogFilePath,
|
||||
getRecentLogs,
|
||||
readLogFile,
|
||||
listLogFiles,
|
||||
};
|
||||
|
||||
@@ -5,7 +5,8 @@
|
||||
import { app } from 'electron';
|
||||
import { join } from 'path';
|
||||
import { homedir } from 'os';
|
||||
import { existsSync, mkdirSync } from 'fs';
|
||||
import { existsSync, mkdirSync, readFileSync } from 'fs';
|
||||
import { logger } from './logger';
|
||||
|
||||
/**
|
||||
* Expand ~ to home directory
|
||||
@@ -72,13 +73,16 @@ export function getPreloadPath(): string {
|
||||
}
|
||||
|
||||
/**
|
||||
* Get OpenClaw submodule directory
|
||||
* Get OpenClaw package directory
|
||||
* - Production (packaged): from resources/openclaw (copied by electron-builder extraResources)
|
||||
* - Development: from node_modules/openclaw
|
||||
*/
|
||||
export function getOpenClawDir(): string {
|
||||
if (app.isPackaged) {
|
||||
return join(process.resourcesPath, 'openclaw');
|
||||
}
|
||||
return join(__dirname, '../../openclaw');
|
||||
// Development: use node_modules/openclaw
|
||||
return join(__dirname, '../../node_modules/openclaw');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -89,44 +93,65 @@ export function getOpenClawEntryPath(): string {
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if OpenClaw submodule exists
|
||||
* Check if OpenClaw package exists
|
||||
*/
|
||||
export function isOpenClawSubmodulePresent(): boolean {
|
||||
return existsSync(getOpenClawDir()) && existsSync(join(getOpenClawDir(), 'package.json'));
|
||||
export function isOpenClawPresent(): boolean {
|
||||
const dir = getOpenClawDir();
|
||||
const pkgJsonPath = join(dir, 'package.json');
|
||||
const exists = existsSync(dir) && existsSync(pkgJsonPath);
|
||||
logger.debug(`isOpenClawPresent: dir=${dir}, exists=${exists}`);
|
||||
return exists;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if OpenClaw is built (has dist folder with entry.js)
|
||||
* Check if OpenClaw is built (has dist folder)
|
||||
* For the npm package, this should always be true since npm publishes the built dist.
|
||||
*/
|
||||
export function isOpenClawBuilt(): boolean {
|
||||
return existsSync(join(getOpenClawDir(), 'dist', 'entry.js'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if OpenClaw has node_modules installed
|
||||
*/
|
||||
export function isOpenClawInstalled(): boolean {
|
||||
return existsSync(join(getOpenClawDir(), 'node_modules'));
|
||||
const dir = getOpenClawDir();
|
||||
// Check for dist/entry.js or just the dist directory with JS files
|
||||
const entryPath = join(dir, 'dist', 'entry.js');
|
||||
const distDir = join(dir, 'dist');
|
||||
const hasEntry = existsSync(entryPath);
|
||||
const hasDist = existsSync(distDir);
|
||||
logger.debug(`isOpenClawBuilt: distDir=${distDir}, hasDist=${hasDist}, hasEntry=${hasEntry}`);
|
||||
return hasDist;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get OpenClaw status for environment check
|
||||
*/
|
||||
export interface OpenClawStatus {
|
||||
submoduleExists: boolean;
|
||||
isInstalled: boolean;
|
||||
packageExists: boolean;
|
||||
isBuilt: boolean;
|
||||
entryPath: string;
|
||||
dir: string;
|
||||
version?: string;
|
||||
}
|
||||
|
||||
export function getOpenClawStatus(): OpenClawStatus {
|
||||
const dir = getOpenClawDir();
|
||||
return {
|
||||
submoduleExists: isOpenClawSubmodulePresent(),
|
||||
isInstalled: isOpenClawInstalled(),
|
||||
let version: string | undefined;
|
||||
|
||||
// Try to read version from package.json
|
||||
try {
|
||||
const pkgPath = join(dir, 'package.json');
|
||||
if (existsSync(pkgPath)) {
|
||||
const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
|
||||
version = pkg.version;
|
||||
}
|
||||
} catch {
|
||||
// Ignore version read errors
|
||||
}
|
||||
|
||||
const status: OpenClawStatus = {
|
||||
packageExists: isOpenClawPresent(),
|
||||
isBuilt: isOpenClawBuilt(),
|
||||
entryPath: getOpenClawEntryPath(),
|
||||
dir,
|
||||
version,
|
||||
};
|
||||
|
||||
logger.info('OpenClaw status:', status);
|
||||
return status;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user