feat: implement OpenClaw process owner and runtime path utilities
- Add OpenClawProcessOwner class to manage the lifecycle of the OpenClaw process. - Introduce utility functions for managing OpenClaw runtime paths. - Update session store to normalize agent session keys and migrate existing keys. - Refactor main process to handle local provider API routing through a new dispatch function. - Enhance token usage writer to utilize a new session key parsing function. - Create agents management store to handle agent data and interactions. - Update chat store to integrate agent selection and session management. - Introduce AgentsSection component for displaying agent information in the UI. - Refactor HomePage to support agent selection and display current agent. - Update routing to reflect new agents page structure.
This commit is contained in:
@@ -3,6 +3,7 @@ import { createProvider } from '@electron/providers';
|
||||
import type { BaseProvider } from '@electron/providers/BaseProvider';
|
||||
import { providerApiService } from '@electron/service/provider-api-service';
|
||||
import logManager from '@electron/service/logger';
|
||||
import { normalizeAgentSessionKey } from '@runtime/lib/agents';
|
||||
import type { RawMessage } from '@runtime/shared/chat-model';
|
||||
import { sessionStore } from '../session-store';
|
||||
import type { GatewayEvent, GatewayRpcParams, GatewayRpcReturns } from '../types';
|
||||
@@ -110,7 +111,8 @@ export function handleChatSend(
|
||||
params: GatewayRpcParams['chat.send'],
|
||||
broadcast: (event: GatewayEvent) => void
|
||||
): GatewayRpcReturns['chat.send'] {
|
||||
const { sessionKey, message, options } = params;
|
||||
const sessionKey = normalizeAgentSessionKey(params.sessionKey);
|
||||
const { message, options } = params;
|
||||
const runId = randomUUID();
|
||||
|
||||
// 1. Append user message
|
||||
@@ -175,20 +177,21 @@ export function handleChatSend(
|
||||
export function handleChatHistory(
|
||||
params: GatewayRpcParams['chat.history']
|
||||
): GatewayRpcReturns['chat.history'] {
|
||||
return sessionStore.getMessages(params.sessionKey, params.limit ?? 50);
|
||||
return sessionStore.getMessages(normalizeAgentSessionKey(params.sessionKey), params.limit ?? 50);
|
||||
}
|
||||
|
||||
export function handleChatAbort(
|
||||
params: GatewayRpcParams['chat.abort'],
|
||||
broadcast: (event: GatewayEvent) => void
|
||||
): GatewayRpcReturns['chat.abort'] {
|
||||
const activeRun = sessionStore.getActiveRun(params.sessionKey);
|
||||
const sessionKey = normalizeAgentSessionKey(params.sessionKey);
|
||||
const activeRun = sessionStore.getActiveRun(sessionKey);
|
||||
if (activeRun) {
|
||||
activeRun.abortController.abort();
|
||||
sessionStore.clearActiveRun(params.sessionKey);
|
||||
sessionStore.clearActiveRun(sessionKey);
|
||||
broadcast({
|
||||
type: 'chat:aborted',
|
||||
sessionKey: params.sessionKey,
|
||||
sessionKey,
|
||||
runId: activeRun.runId,
|
||||
});
|
||||
}
|
||||
@@ -201,6 +204,6 @@ export function handleSessionList(): GatewayRpcReturns['session.list'] {
|
||||
export function handleSessionDelete(
|
||||
params: GatewayRpcParams['session.delete']
|
||||
): GatewayRpcReturns['session.delete'] {
|
||||
sessionStore.deleteSession(params.sessionKey);
|
||||
sessionStore.deleteSession(normalizeAgentSessionKey(params.sessionKey));
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
@@ -7,14 +7,60 @@ import * as providerHandlers from './handlers/provider';
|
||||
|
||||
class GatewayManager {
|
||||
private initialized = false;
|
||||
private status: 'connected' | 'disconnected' | 'reconnecting' = 'disconnected';
|
||||
|
||||
private setStatus(status: 'connected' | 'disconnected' | 'reconnecting'): void {
|
||||
this.status = status;
|
||||
this.broadcast({ type: 'gateway:status', status });
|
||||
}
|
||||
|
||||
async init(): Promise<void> {
|
||||
if (this.initialized) return;
|
||||
this.initialized = true;
|
||||
this.status = 'connected';
|
||||
logManager.info('GatewayManager initialized');
|
||||
this.broadcast({ type: 'gateway:status', status: 'connected' });
|
||||
}
|
||||
|
||||
async start(): Promise<void> {
|
||||
await this.init();
|
||||
}
|
||||
|
||||
async stop(): Promise<void> {
|
||||
this.initialized = false;
|
||||
this.setStatus('disconnected');
|
||||
}
|
||||
|
||||
async restart(): Promise<void> {
|
||||
this.initialized = false;
|
||||
this.setStatus('reconnecting');
|
||||
await this.init();
|
||||
}
|
||||
|
||||
getStatus(): {
|
||||
status: 'connected' | 'disconnected' | 'reconnecting';
|
||||
initialized: boolean;
|
||||
mode: 'in-process';
|
||||
} {
|
||||
return {
|
||||
status: this.status,
|
||||
initialized: this.initialized,
|
||||
mode: 'in-process',
|
||||
};
|
||||
}
|
||||
|
||||
async checkHealth(): Promise<{
|
||||
ok: boolean;
|
||||
status: 'connected' | 'disconnected' | 'reconnecting';
|
||||
initialized: boolean;
|
||||
mode: 'in-process';
|
||||
}> {
|
||||
return {
|
||||
ok: this.initialized && this.status === 'connected',
|
||||
...this.getStatus(),
|
||||
};
|
||||
}
|
||||
|
||||
async rpc(method: string, params: any): Promise<any> {
|
||||
if (!this.initialized) {
|
||||
await this.init();
|
||||
|
||||
112
electron/gateway/openclaw-process-owner.ts
Normal file
112
electron/gateway/openclaw-process-owner.ts
Normal file
@@ -0,0 +1,112 @@
|
||||
import {
|
||||
ensureOpenClawRuntimeLayout,
|
||||
getOpenClawRuntimePaths,
|
||||
type OpenClawRuntimePaths,
|
||||
} from '@electron/utils/paths';
|
||||
|
||||
export type OpenClawProcessOwnerState =
|
||||
| 'idle'
|
||||
| 'preparing'
|
||||
| 'running'
|
||||
| 'stopping'
|
||||
| 'stopped'
|
||||
| 'failed';
|
||||
|
||||
export interface OpenClawProcessOwnerStatus {
|
||||
state: OpenClawProcessOwnerState;
|
||||
prepared: boolean;
|
||||
runtimePaths: OpenClawRuntimePaths;
|
||||
lastError?: string;
|
||||
}
|
||||
|
||||
export interface OpenClawProcessOwnerOptions {
|
||||
runtimePaths?: Partial<OpenClawRuntimePaths>;
|
||||
}
|
||||
|
||||
export interface OpenClawProcessOwnerLike {
|
||||
prepare(): Promise<void>;
|
||||
start(): Promise<void>;
|
||||
stop(): Promise<void>;
|
||||
restart(): Promise<void>;
|
||||
getStatus(): OpenClawProcessOwnerStatus;
|
||||
getRuntimePaths(): OpenClawRuntimePaths;
|
||||
}
|
||||
|
||||
function mergeRuntimePaths(
|
||||
base: OpenClawRuntimePaths,
|
||||
override?: Partial<OpenClawRuntimePaths>,
|
||||
): OpenClawRuntimePaths {
|
||||
if (!override) {
|
||||
return base;
|
||||
}
|
||||
|
||||
return {
|
||||
configDir: override.configDir ?? base.configDir,
|
||||
runtimeDir: override.runtimeDir ?? base.runtimeDir,
|
||||
dir: override.dir ?? base.dir,
|
||||
resolvedDir: override.resolvedDir ?? base.resolvedDir,
|
||||
entryPath: override.entryPath ?? base.entryPath,
|
||||
};
|
||||
}
|
||||
|
||||
export class OpenClawProcessOwner implements OpenClawProcessOwnerLike {
|
||||
private status: OpenClawProcessOwnerStatus;
|
||||
|
||||
constructor(options?: OpenClawProcessOwnerOptions) {
|
||||
const runtimePaths = mergeRuntimePaths(
|
||||
getOpenClawRuntimePaths(),
|
||||
options?.runtimePaths,
|
||||
);
|
||||
|
||||
this.status = {
|
||||
state: 'idle',
|
||||
prepared: false,
|
||||
runtimePaths,
|
||||
};
|
||||
}
|
||||
|
||||
async prepare(): Promise<void> {
|
||||
if (this.status.prepared) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.status.state = 'preparing';
|
||||
ensureOpenClawRuntimeLayout(this.status.runtimePaths);
|
||||
this.status.prepared = true;
|
||||
this.status.state = 'idle';
|
||||
}
|
||||
|
||||
async start(): Promise<void> {
|
||||
if (this.status.state === 'running') {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.prepare();
|
||||
this.status.state = 'running';
|
||||
}
|
||||
|
||||
async stop(): Promise<void> {
|
||||
if (this.status.state === 'idle' || this.status.state === 'stopped') {
|
||||
return;
|
||||
}
|
||||
|
||||
this.status.state = 'stopping';
|
||||
this.status.state = 'stopped';
|
||||
}
|
||||
|
||||
async restart(): Promise<void> {
|
||||
await this.stop();
|
||||
await this.start();
|
||||
}
|
||||
|
||||
getStatus(): OpenClawProcessOwnerStatus {
|
||||
return {
|
||||
...this.status,
|
||||
runtimePaths: { ...this.status.runtimePaths },
|
||||
};
|
||||
}
|
||||
|
||||
getRuntimePaths(): OpenClawRuntimePaths {
|
||||
return { ...this.status.runtimePaths };
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import { app } from 'electron';
|
||||
import logManager from '@electron/service/logger';
|
||||
import { normalizeAgentSessionKey } from '@runtime/lib/agents';
|
||||
import type { RawMessage } from '@runtime/shared/chat-model';
|
||||
|
||||
let sessionsFilePath: string | null = null;
|
||||
@@ -41,12 +42,21 @@ class SessionStore {
|
||||
string,
|
||||
Omit<SessionEntry, 'activeRun'>
|
||||
>;
|
||||
let migrated = false;
|
||||
for (const [key, entry] of Object.entries(data)) {
|
||||
this.sessions.set(key, {
|
||||
const normalizedKey = normalizeAgentSessionKey(key);
|
||||
if (normalizedKey !== key) {
|
||||
migrated = true;
|
||||
}
|
||||
this.sessions.set(normalizedKey, {
|
||||
...entry,
|
||||
key: normalizedKey,
|
||||
activeRun: undefined,
|
||||
});
|
||||
}
|
||||
if (migrated) {
|
||||
this.saveToDisk();
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
logManager.error('Failed to load sessions from disk:', e);
|
||||
@@ -73,21 +83,22 @@ class SessionStore {
|
||||
|
||||
getOrCreate(key: string): SessionEntry {
|
||||
this.ensureLoaded();
|
||||
let session = this.sessions.get(key);
|
||||
const normalizedKey = normalizeAgentSessionKey(key);
|
||||
let session = this.sessions.get(normalizedKey);
|
||||
if (!session) {
|
||||
session = {
|
||||
key,
|
||||
key: normalizedKey,
|
||||
messages: [],
|
||||
updatedAt: Date.now(),
|
||||
};
|
||||
this.sessions.set(key, session);
|
||||
this.sessions.set(normalizedKey, session);
|
||||
}
|
||||
return session;
|
||||
}
|
||||
|
||||
get(key: string): SessionEntry | undefined {
|
||||
this.ensureLoaded();
|
||||
return this.sessions.get(key);
|
||||
return this.sessions.get(normalizeAgentSessionKey(key));
|
||||
}
|
||||
|
||||
getAllKeys(): string[] {
|
||||
@@ -114,18 +125,18 @@ class SessionStore {
|
||||
}
|
||||
|
||||
clearActiveRun(key: string): void {
|
||||
const session = this.sessions.get(key);
|
||||
const session = this.sessions.get(normalizeAgentSessionKey(key));
|
||||
if (session) {
|
||||
session.activeRun = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
getActiveRun(key: string): { runId: string; abortController: AbortController } | undefined {
|
||||
return this.sessions.get(key)?.activeRun;
|
||||
return this.sessions.get(normalizeAgentSessionKey(key))?.activeRun;
|
||||
}
|
||||
|
||||
deleteSession(key: string): void {
|
||||
this.sessions.delete(key);
|
||||
this.sessions.delete(normalizeAgentSessionKey(key));
|
||||
this.saveToDisk();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user