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:
Haze
2026-02-06 00:24:36 +08:00
Unverified
parent a04aaf54e3
commit 29ee21754a
9 changed files with 205 additions and 11 deletions

3
.gitmodules vendored Normal file
View File

@@ -0,0 +1,3 @@
[submodule "openclaw"]
path = openclaw
url = /Users/guoyuliang/Project/openclaw

View File

@@ -20,6 +20,22 @@ extraResources:
- "**/*"
- "!icons/*.md"
- "!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
asarUnpack:

View File

@@ -4,8 +4,16 @@
*/
import { spawn, ChildProcess } from 'child_process';
import { EventEmitter } from 'events';
import { existsSync } from 'fs';
import WebSocket from 'ws';
import { PORTS } from '../utils/config';
import {
getOpenClawDir,
getOpenClawEntryPath,
isOpenClawBuilt,
isOpenClawSubmodulePresent,
isOpenClawInstalled
} from '../utils/paths';
import { GatewayEventType, JsonRpcNotification, isNotification, isResponse } from './protocol';
/**
@@ -308,17 +316,59 @@ export class GatewayManager extends EventEmitter {
/**
* Start Gateway process
* Uses OpenClaw submodule - supports both production (dist) and development modes
*/
private async startProcess(): Promise<void> {
return new Promise((resolve, reject) => {
// Find openclaw command
const command = 'openclaw';
const args = ['gateway', 'run', '--port', String(this.status.port)];
const openclawDir = getOpenClawDir();
const entryScript = getOpenClawEntryPath();
// 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, {
cwd: openclawDir,
stdio: ['ignore', 'pipe', 'pipe'],
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) => {

View File

@@ -19,6 +19,7 @@ import {
isEncryptionAvailable,
type ProviderConfig,
} from '../utils/secure-storage';
import { getOpenClawStatus } from '../utils/paths';
/**
* Register all IPC handlers
@@ -30,6 +31,9 @@ export function registerIpcHandlers(
// Gateway handlers
registerGatewayHandlers(gatewayManager, mainWindow);
// OpenClaw handlers
registerOpenClawHandlers();
// Provider handlers
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
*/

View File

@@ -22,6 +22,9 @@ const electronAPI = {
'gateway:restart',
'gateway:rpc',
'gateway:health',
// OpenClaw
'openclaw:status',
'openclaw:isReady',
// Shell
'shell:openExternal',
'shell:showItemInFolder',

View File

@@ -70,3 +70,63 @@ export function getResourcesDir(): string {
export function getPreloadPath(): string {
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

Submodule openclaw added at 8524666454

View File

@@ -38,7 +38,12 @@
"publish:mac": "electron-builder --mac --publish always",
"publish:win": "electron-builder --win --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": {
"electron-store": "^10.0.0",

View File

@@ -396,12 +396,47 @@ function RuntimeContent({ onStatusChange }: RuntimeContentProps) {
}));
}
// Check OpenClaw (simulated - in real app would check if openclaw is installed)
await new Promise((resolve) => setTimeout(resolve, 500));
// Check OpenClaw submodule status
try {
const openclawStatus = await window.electron.ipcRenderer.invoke('openclaw:status') as {
submoduleExists: boolean;
isInstalled: boolean;
isBuilt: boolean;
dir: string;
};
if (!openclawStatus.submoduleExists) {
setChecks((prev) => ({
...prev,
openclaw: { status: 'success', message: 'OpenClaw package ready' },
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
await new Promise((resolve) => setTimeout(resolve, 500));