feat(gateway): integrate OpenClaw as git submodule
- Add OpenClaw as git submodule at ./openclaw/ - Update GatewayManager to start gateway from submodule path - Support both production (dist) and development (pnpm dev) modes - Add IPC handler for OpenClaw status check - Update Setup wizard to check real OpenClaw submodule status - Configure electron-builder to include submodule in packaged app - Add npm scripts for submodule management: - postinstall: auto-init submodule - openclaw:init: initialize and install dependencies - openclaw:install: install dependencies only - openclaw:build: build OpenClaw - openclaw:update: update to latest version
This commit is contained in:
3
.gitmodules
vendored
Normal file
3
.gitmodules
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
[submodule "openclaw"]
|
||||||
|
path = openclaw
|
||||||
|
url = /Users/guoyuliang/Project/openclaw
|
||||||
@@ -20,6 +20,22 @@ extraResources:
|
|||||||
- "**/*"
|
- "**/*"
|
||||||
- "!icons/*.md"
|
- "!icons/*.md"
|
||||||
- "!icons/*.svg"
|
- "!icons/*.svg"
|
||||||
|
# OpenClaw submodule - include only necessary files for runtime
|
||||||
|
- from: openclaw/
|
||||||
|
to: openclaw/
|
||||||
|
filter:
|
||||||
|
- "openclaw.mjs"
|
||||||
|
- "package.json"
|
||||||
|
- "dist/**/*"
|
||||||
|
- "skills/**/*"
|
||||||
|
- "extensions/**/*"
|
||||||
|
- "scripts/run-node.mjs"
|
||||||
|
- "!**/*.test.ts"
|
||||||
|
- "!**/*.test.js"
|
||||||
|
- "!**/test/**"
|
||||||
|
- "!**/.git"
|
||||||
|
- "!**/.github"
|
||||||
|
- "!**/docs/**"
|
||||||
|
|
||||||
asar: true
|
asar: true
|
||||||
asarUnpack:
|
asarUnpack:
|
||||||
|
|||||||
@@ -4,8 +4,16 @@
|
|||||||
*/
|
*/
|
||||||
import { spawn, ChildProcess } from 'child_process';
|
import { spawn, ChildProcess } from 'child_process';
|
||||||
import { EventEmitter } from 'events';
|
import { EventEmitter } from 'events';
|
||||||
|
import { existsSync } from 'fs';
|
||||||
import WebSocket from 'ws';
|
import WebSocket from 'ws';
|
||||||
import { PORTS } from '../utils/config';
|
import { PORTS } from '../utils/config';
|
||||||
|
import {
|
||||||
|
getOpenClawDir,
|
||||||
|
getOpenClawEntryPath,
|
||||||
|
isOpenClawBuilt,
|
||||||
|
isOpenClawSubmodulePresent,
|
||||||
|
isOpenClawInstalled
|
||||||
|
} from '../utils/paths';
|
||||||
import { GatewayEventType, JsonRpcNotification, isNotification, isResponse } from './protocol';
|
import { GatewayEventType, JsonRpcNotification, isNotification, isResponse } from './protocol';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -308,17 +316,59 @@ export class GatewayManager extends EventEmitter {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Start Gateway process
|
* Start Gateway process
|
||||||
|
* Uses OpenClaw submodule - supports both production (dist) and development modes
|
||||||
*/
|
*/
|
||||||
private async startProcess(): Promise<void> {
|
private async startProcess(): Promise<void> {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
// Find openclaw command
|
const openclawDir = getOpenClawDir();
|
||||||
const command = 'openclaw';
|
const entryScript = getOpenClawEntryPath();
|
||||||
const args = ['gateway', 'run', '--port', String(this.status.port)];
|
|
||||||
|
// Verify OpenClaw submodule exists
|
||||||
|
if (!isOpenClawSubmodulePresent()) {
|
||||||
|
reject(new Error(
|
||||||
|
'OpenClaw submodule not found. Please run: git submodule update --init'
|
||||||
|
));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify dependencies are installed
|
||||||
|
if (!isOpenClawInstalled()) {
|
||||||
|
reject(new Error(
|
||||||
|
'OpenClaw dependencies not installed. Please run: cd openclaw && pnpm install'
|
||||||
|
));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
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)');
|
||||||
|
command = 'node';
|
||||||
|
args = [entryScript, 'gateway', 'run', '--port', String(this.status.port), '--allow-unconfigured'];
|
||||||
|
} else {
|
||||||
|
// Development mode: use pnpm gateway:dev which handles tsx compilation
|
||||||
|
console.log('Starting Gateway in development mode (using pnpm)');
|
||||||
|
command = 'pnpm';
|
||||||
|
args = ['run', 'dev', 'gateway', 'run', '--port', String(this.status.port), '--allow-unconfigured'];
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`Spawning Gateway: ${command} ${args.join(' ')}`);
|
||||||
|
console.log(`Working directory: ${openclawDir}`);
|
||||||
|
|
||||||
this.process = spawn(command, args, {
|
this.process = spawn(command, args, {
|
||||||
|
cwd: openclawDir,
|
||||||
stdio: ['ignore', 'pipe', 'pipe'],
|
stdio: ['ignore', 'pipe', 'pipe'],
|
||||||
detached: false,
|
detached: false,
|
||||||
shell: true,
|
shell: process.platform === 'win32', // Use shell on Windows for pnpm
|
||||||
|
env: {
|
||||||
|
...process.env,
|
||||||
|
// Skip channel auto-connect during startup for faster boot
|
||||||
|
OPENCLAW_SKIP_CHANNELS: '1',
|
||||||
|
CLAWDBOT_SKIP_CHANNELS: '1',
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
this.process.on('error', (error) => {
|
this.process.on('error', (error) => {
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import {
|
|||||||
isEncryptionAvailable,
|
isEncryptionAvailable,
|
||||||
type ProviderConfig,
|
type ProviderConfig,
|
||||||
} from '../utils/secure-storage';
|
} from '../utils/secure-storage';
|
||||||
|
import { getOpenClawStatus } from '../utils/paths';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Register all IPC handlers
|
* Register all IPC handlers
|
||||||
@@ -30,6 +31,9 @@ export function registerIpcHandlers(
|
|||||||
// Gateway handlers
|
// Gateway handlers
|
||||||
registerGatewayHandlers(gatewayManager, mainWindow);
|
registerGatewayHandlers(gatewayManager, mainWindow);
|
||||||
|
|
||||||
|
// OpenClaw handlers
|
||||||
|
registerOpenClawHandlers();
|
||||||
|
|
||||||
// Provider handlers
|
// Provider handlers
|
||||||
registerProviderHandlers();
|
registerProviderHandlers();
|
||||||
|
|
||||||
@@ -154,6 +158,23 @@ function registerGatewayHandlers(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* OpenClaw-related IPC handlers
|
||||||
|
* For checking submodule status
|
||||||
|
*/
|
||||||
|
function registerOpenClawHandlers(): void {
|
||||||
|
// Get OpenClaw submodule status
|
||||||
|
ipcMain.handle('openclaw:status', () => {
|
||||||
|
return getOpenClawStatus();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Check if OpenClaw is ready (submodule present and dependencies installed)
|
||||||
|
ipcMain.handle('openclaw:isReady', () => {
|
||||||
|
const status = getOpenClawStatus();
|
||||||
|
return status.submoduleExists && status.isInstalled;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Provider-related IPC handlers
|
* Provider-related IPC handlers
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -22,6 +22,9 @@ const electronAPI = {
|
|||||||
'gateway:restart',
|
'gateway:restart',
|
||||||
'gateway:rpc',
|
'gateway:rpc',
|
||||||
'gateway:health',
|
'gateway:health',
|
||||||
|
// OpenClaw
|
||||||
|
'openclaw:status',
|
||||||
|
'openclaw:isReady',
|
||||||
// Shell
|
// Shell
|
||||||
'shell:openExternal',
|
'shell:openExternal',
|
||||||
'shell:showItemInFolder',
|
'shell:showItemInFolder',
|
||||||
|
|||||||
@@ -70,3 +70,63 @@ export function getResourcesDir(): string {
|
|||||||
export function getPreloadPath(): string {
|
export function getPreloadPath(): string {
|
||||||
return join(__dirname, '../preload/index.js');
|
return join(__dirname, '../preload/index.js');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get OpenClaw submodule directory
|
||||||
|
*/
|
||||||
|
export function getOpenClawDir(): string {
|
||||||
|
if (app.isPackaged) {
|
||||||
|
return join(process.resourcesPath, 'openclaw');
|
||||||
|
}
|
||||||
|
return join(__dirname, '../../openclaw');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get OpenClaw entry script path (openclaw.mjs)
|
||||||
|
*/
|
||||||
|
export function getOpenClawEntryPath(): string {
|
||||||
|
return join(getOpenClawDir(), 'openclaw.mjs');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if OpenClaw submodule exists
|
||||||
|
*/
|
||||||
|
export function isOpenClawSubmodulePresent(): boolean {
|
||||||
|
return existsSync(getOpenClawDir()) && existsSync(join(getOpenClawDir(), 'package.json'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if OpenClaw is built (has dist folder with entry.js)
|
||||||
|
*/
|
||||||
|
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'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get OpenClaw status for environment check
|
||||||
|
*/
|
||||||
|
export interface OpenClawStatus {
|
||||||
|
submoduleExists: boolean;
|
||||||
|
isInstalled: boolean;
|
||||||
|
isBuilt: boolean;
|
||||||
|
entryPath: string;
|
||||||
|
dir: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getOpenClawStatus(): OpenClawStatus {
|
||||||
|
const dir = getOpenClawDir();
|
||||||
|
return {
|
||||||
|
submoduleExists: isOpenClawSubmodulePresent(),
|
||||||
|
isInstalled: isOpenClawInstalled(),
|
||||||
|
isBuilt: isOpenClawBuilt(),
|
||||||
|
entryPath: getOpenClawEntryPath(),
|
||||||
|
dir,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|||||||
1
openclaw
Submodule
1
openclaw
Submodule
Submodule openclaw added at 8524666454
@@ -38,7 +38,12 @@
|
|||||||
"publish:mac": "electron-builder --mac --publish always",
|
"publish:mac": "electron-builder --mac --publish always",
|
||||||
"publish:win": "electron-builder --win --publish always",
|
"publish:win": "electron-builder --win --publish always",
|
||||||
"publish:linux": "electron-builder --linux --publish always",
|
"publish:linux": "electron-builder --linux --publish always",
|
||||||
"release": "pnpm run build:vite && pnpm run publish"
|
"release": "pnpm run build:vite && pnpm run publish",
|
||||||
|
"postinstall": "git submodule update --init",
|
||||||
|
"openclaw:init": "git submodule update --init && cd openclaw && pnpm install",
|
||||||
|
"openclaw:install": "cd openclaw && pnpm install",
|
||||||
|
"openclaw:build": "cd openclaw && pnpm build",
|
||||||
|
"openclaw:update": "git submodule update --remote openclaw && cd openclaw && pnpm install"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"electron-store": "^10.0.0",
|
"electron-store": "^10.0.0",
|
||||||
|
|||||||
@@ -396,12 +396,47 @@ function RuntimeContent({ onStatusChange }: RuntimeContentProps) {
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check OpenClaw (simulated - in real app would check if openclaw is installed)
|
// Check OpenClaw submodule status
|
||||||
await new Promise((resolve) => setTimeout(resolve, 500));
|
try {
|
||||||
setChecks((prev) => ({
|
const openclawStatus = await window.electron.ipcRenderer.invoke('openclaw:status') as {
|
||||||
...prev,
|
submoduleExists: boolean;
|
||||||
openclaw: { status: 'success', message: 'OpenClaw package ready' },
|
isInstalled: boolean;
|
||||||
}));
|
isBuilt: boolean;
|
||||||
|
dir: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!openclawStatus.submoduleExists) {
|
||||||
|
setChecks((prev) => ({
|
||||||
|
...prev,
|
||||||
|
openclaw: {
|
||||||
|
status: 'error',
|
||||||
|
message: 'OpenClaw submodule not found. Run: git submodule update --init'
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
} else if (!openclawStatus.isInstalled) {
|
||||||
|
setChecks((prev) => ({
|
||||||
|
...prev,
|
||||||
|
openclaw: {
|
||||||
|
status: 'error',
|
||||||
|
message: 'Dependencies not installed. Run: cd openclaw && pnpm install'
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
} else {
|
||||||
|
const modeLabel = openclawStatus.isBuilt ? 'production' : 'development';
|
||||||
|
setChecks((prev) => ({
|
||||||
|
...prev,
|
||||||
|
openclaw: {
|
||||||
|
status: 'success',
|
||||||
|
message: `OpenClaw package ready (${modeLabel} mode)`
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
setChecks((prev) => ({
|
||||||
|
...prev,
|
||||||
|
openclaw: { status: 'error', message: `Failed to check: ${error}` },
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
// Check Gateway
|
// Check Gateway
|
||||||
await new Promise((resolve) => setTimeout(resolve, 500));
|
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||||
|
|||||||
Reference in New Issue
Block a user