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:
duanshuwen
2026-04-17 21:32:06 +08:00
parent eca70425cf
commit e9f3a29886
33 changed files with 1526 additions and 2428 deletions

View File

@@ -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 };
}

View File

@@ -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();

View 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 };
}
}

View File

@@ -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();
}
}