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

@@ -0,0 +1,113 @@
import { sessionStore } from '@electron/gateway/session-store';
import { getTranscriptFilePath } from '@electron/utils/token-usage-writer';
import { buildAgentSessionKey, normalizeAgentSessionKey, parseSessionKey } from '@runtime/lib/agents';
import type { HostApiContext } from '../context';
import type { NormalizedHostApiRequest } from '../route-utils';
import { fail, ok, parseJsonBody } from '../route-utils';
function parseSessionIdentity(request: NormalizedHostApiRequest): { agentId: string; sessionId: string; sessionKey: string } | null {
const sessionKey = normalizeAgentSessionKey(request.url.searchParams.get('sessionKey')?.trim() || '');
const parsed = parseSessionKey(sessionKey);
if (sessionKey && parsed.isAgentSession) {
return {
agentId: parsed.agentId,
sessionId: parsed.sessionId,
sessionKey: parsed.sessionKey,
};
}
const agentId = request.url.searchParams.get('agentId')?.trim() || '';
const sessionId = request.url.searchParams.get('sessionId')?.trim() || '';
if (!agentId || !sessionId) return null;
return {
agentId: parseSessionKey(buildAgentSessionKey(agentId, sessionId)).agentId,
sessionId: parseSessionKey(buildAgentSessionKey(agentId, sessionId)).sessionId,
sessionKey: buildAgentSessionKey(agentId, sessionId),
};
}
export async function handleSessionRoutes(
request: NormalizedHostApiRequest,
_ctx: HostApiContext,
) {
const { pathname, method } = request;
if (pathname === '/api/sessions/transcript' && method === 'GET') {
const identity = parseSessionIdentity(request);
if (!identity) {
return fail(400, 'sessionKey or agentId/sessionId is required');
}
try {
const fsP = await import('node:fs/promises');
const transcriptPath = getTranscriptFilePath(identity.sessionKey);
let raw: string;
try {
raw = await fsP.readFile(transcriptPath, 'utf8');
} catch (error: any) {
const requestedSessionKey = request.url.searchParams.get('sessionKey')?.trim() || '';
if (error?.code === 'ENOENT' && requestedSessionKey && requestedSessionKey !== identity.sessionKey) {
raw = await fsP.readFile(getTranscriptFilePath(requestedSessionKey), 'utf8');
} else {
throw error;
}
}
const lines = raw.split(/\r?\n/).filter(Boolean);
const messages = lines.flatMap((line) => {
try {
const entry = JSON.parse(line) as { type?: string; message?: unknown; timestamp?: string };
return entry.type === 'message' && entry.message ? [{ ...entry.message, timestamp: entry.timestamp }] : [];
} catch {
return [];
}
});
return ok({
agentId: identity.agentId,
sessionId: identity.sessionId,
sessionKey: identity.sessionKey,
transcriptPath,
messages,
});
} catch (error: any) {
if (error?.code === 'ENOENT') {
return fail(404, 'Transcript not found');
}
return fail(500, error instanceof Error ? error.message : String(error));
}
}
if (pathname === '/api/sessions/delete' && method === 'POST') {
const body = parseJsonBody<{ sessionKey: string }>(request.body);
const rawSessionKey = String(body?.sessionKey || '').trim();
const sessionKey = normalizeAgentSessionKey(rawSessionKey);
if (!sessionKey) {
return fail(400, 'sessionKey is required');
}
sessionStore.deleteSession(sessionKey);
const transcriptPath = getTranscriptFilePath(sessionKey);
try {
const fsP = await import('node:fs/promises');
await fsP.rename(transcriptPath, transcriptPath.replace(/\.jsonl$/, '.deleted.jsonl'));
} catch {
if (rawSessionKey && rawSessionKey !== sessionKey) {
try {
const fsP = await import('node:fs/promises');
const legacyTranscriptPath = getTranscriptFilePath(rawSessionKey);
await fsP.rename(legacyTranscriptPath, legacyTranscriptPath.replace(/\.jsonl$/, '.deleted.jsonl'));
} catch {
// Best effort: transcript may not exist yet.
}
}
}
return ok({ success: true });
}
return null;
}