feat: refactor HomePage to integrate agents store and update related components
feat: add runtime event handling for providers in ProvidersSection feat: update routing to include Channels and Agents pages feat: extend route types and navigation items for Channels and Agents feat: implement agents store for managing agent data and interactions fix: update chat store to utilize agents store for agent-related functionality chore: export agents store from index fix: enhance runtime types for better event handling fix: update Vite config to handle dev server URL correctly
This commit is contained in:
189
electron/api/routes/agents.ts
Normal file
189
electron/api/routes/agents.ts
Normal file
@@ -0,0 +1,189 @@
|
||||
import type { HostApiContext } from '../context';
|
||||
import type { NormalizedHostApiRequest } from '../route-utils';
|
||||
import { fail, ok, parseJsonBody } from '../route-utils';
|
||||
import { syncProviderRuntimeSnapshot } from '@electron/service/provider-runtime-sync';
|
||||
import {
|
||||
assignChannelToAgent,
|
||||
clearChannelBinding,
|
||||
createAgentConfig,
|
||||
deleteAgentConfig,
|
||||
listAgentsSnapshot,
|
||||
updateAgentModelConfig,
|
||||
updateAgentName,
|
||||
} from '../../utils/agent-config';
|
||||
|
||||
function getProviderSnapshot(ctx: HostApiContext) {
|
||||
const accounts = ctx.providerApiService
|
||||
.getAccounts()
|
||||
.filter((account) => account.enabled !== false);
|
||||
const defaultAccountId = ctx.providerApiService.getDefault().accountId;
|
||||
return { accounts, defaultAccountId };
|
||||
}
|
||||
|
||||
function formatRuntimeWarning(warnings: string[]): string | null {
|
||||
if (warnings.length === 0) return null;
|
||||
return `Runtime sync warnings: ${warnings.join('; ')}`;
|
||||
}
|
||||
|
||||
export async function handleAgentRoutes(
|
||||
request: NormalizedHostApiRequest,
|
||||
ctx: HostApiContext,
|
||||
) {
|
||||
const { pathname, method } = request;
|
||||
const { accounts, defaultAccountId } = getProviderSnapshot(ctx);
|
||||
|
||||
if (pathname === '/api/agents' && method === 'GET') {
|
||||
return ok({
|
||||
success: true,
|
||||
...listAgentsSnapshot(accounts, defaultAccountId),
|
||||
});
|
||||
}
|
||||
|
||||
if (pathname === '/api/agents' && method === 'POST') {
|
||||
try {
|
||||
const body = parseJsonBody<{ name?: string; inheritWorkspace?: boolean }>(request.body);
|
||||
if (!body?.name || !String(body.name).trim()) {
|
||||
return fail(400, 'name is required');
|
||||
}
|
||||
|
||||
const snapshot = createAgentConfig(body.name, { inheritWorkspace: body.inheritWorkspace }, accounts, defaultAccountId);
|
||||
const runtimeSync = syncProviderRuntimeSnapshot({ accounts, defaultAccountId, snapshot });
|
||||
ctx.gatewayManager.reloadProviders({
|
||||
topics: ['agents', 'providers', 'models'],
|
||||
reason: 'agents:created',
|
||||
warnings: runtimeSync.warnings,
|
||||
});
|
||||
|
||||
return ok({
|
||||
success: true,
|
||||
warning: formatRuntimeWarning(runtimeSync.warnings),
|
||||
...snapshot,
|
||||
});
|
||||
} catch (error) {
|
||||
return fail(500, error instanceof Error ? error.message : String(error));
|
||||
}
|
||||
}
|
||||
|
||||
if (!pathname.startsWith('/api/agents/')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const suffix = pathname.slice('/api/agents/'.length);
|
||||
const parts = suffix.split('/').filter(Boolean).map((part) => decodeURIComponent(part));
|
||||
|
||||
if (parts.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (method === 'PUT' && parts.length === 1) {
|
||||
try {
|
||||
const body = parseJsonBody<{ name?: string }>(request.body);
|
||||
if (!body?.name || !String(body.name).trim()) {
|
||||
return fail(400, 'name is required');
|
||||
}
|
||||
|
||||
const snapshot = updateAgentName(parts[0], body.name, accounts, defaultAccountId);
|
||||
const runtimeSync = syncProviderRuntimeSnapshot({ accounts, defaultAccountId, snapshot });
|
||||
ctx.gatewayManager.notifyRuntimeChanged({
|
||||
topics: ['agents'],
|
||||
reason: 'agents:renamed',
|
||||
warnings: runtimeSync.warnings,
|
||||
});
|
||||
|
||||
return ok({
|
||||
success: true,
|
||||
warning: formatRuntimeWarning(runtimeSync.warnings),
|
||||
...snapshot,
|
||||
});
|
||||
} catch (error) {
|
||||
return fail(500, error instanceof Error ? error.message : String(error));
|
||||
}
|
||||
}
|
||||
|
||||
if (method === 'PUT' && parts.length === 2 && parts[1] === 'model') {
|
||||
try {
|
||||
const body = parseJsonBody<{ modelRef?: string | null; providerAccountId?: string | null }>(request.body);
|
||||
const snapshot = updateAgentModelConfig(
|
||||
parts[0],
|
||||
body?.modelRef ?? null,
|
||||
body?.providerAccountId ?? null,
|
||||
accounts,
|
||||
defaultAccountId,
|
||||
);
|
||||
const runtimeSync = syncProviderRuntimeSnapshot({ accounts, defaultAccountId, snapshot });
|
||||
ctx.gatewayManager.reloadProviders({
|
||||
topics: ['agents', 'providers', 'models'],
|
||||
reason: 'agents:model-updated',
|
||||
warnings: runtimeSync.warnings,
|
||||
});
|
||||
|
||||
return ok({
|
||||
success: true,
|
||||
warning: formatRuntimeWarning(runtimeSync.warnings),
|
||||
...snapshot,
|
||||
});
|
||||
} catch (error) {
|
||||
return fail(500, error instanceof Error ? error.message : String(error));
|
||||
}
|
||||
}
|
||||
|
||||
if (method === 'PUT' && parts.length === 3 && parts[1] === 'channels') {
|
||||
try {
|
||||
const body = parseJsonBody<{ accountId?: string | null }>(request.body);
|
||||
const snapshot = assignChannelToAgent(parts[0], parts[2], body?.accountId ?? null, accounts, defaultAccountId);
|
||||
ctx.gatewayManager.notifyRuntimeChanged({
|
||||
topics: ['agents', 'channels', 'channel-targets'],
|
||||
reason: 'agents:channel-assigned',
|
||||
channelType: parts[2],
|
||||
accountId: body?.accountId ?? undefined,
|
||||
});
|
||||
return ok({
|
||||
success: true,
|
||||
...snapshot,
|
||||
});
|
||||
} catch (error) {
|
||||
return fail(500, error instanceof Error ? error.message : String(error));
|
||||
}
|
||||
}
|
||||
|
||||
if (method === 'DELETE' && parts.length === 1) {
|
||||
try {
|
||||
const snapshot = deleteAgentConfig(parts[0], accounts, defaultAccountId);
|
||||
const runtimeSync = syncProviderRuntimeSnapshot({ accounts, defaultAccountId, snapshot });
|
||||
await ctx.gatewayManager.restart({
|
||||
topics: ['agents', 'providers', 'models', 'channels', 'channel-targets'],
|
||||
reason: 'agents:deleted',
|
||||
warnings: runtimeSync.warnings,
|
||||
});
|
||||
|
||||
return ok({
|
||||
success: true,
|
||||
warning: formatRuntimeWarning(runtimeSync.warnings),
|
||||
...snapshot,
|
||||
});
|
||||
} catch (error) {
|
||||
return fail(500, error instanceof Error ? error.message : String(error));
|
||||
}
|
||||
}
|
||||
|
||||
if (method === 'DELETE' && parts.length === 3 && parts[1] === 'channels') {
|
||||
try {
|
||||
const accountId = request.url.searchParams.get('accountId')?.trim() || null;
|
||||
const snapshot = clearChannelBinding(parts[2], accountId, accounts, defaultAccountId);
|
||||
ctx.gatewayManager.notifyRuntimeChanged({
|
||||
topics: ['agents', 'channels', 'channel-targets'],
|
||||
reason: 'agents:channel-cleared',
|
||||
channelType: parts[2],
|
||||
accountId: accountId ?? undefined,
|
||||
});
|
||||
return ok({
|
||||
success: true,
|
||||
...snapshot,
|
||||
});
|
||||
} catch (error) {
|
||||
return fail(500, error instanceof Error ? error.message : String(error));
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
133
electron/api/routes/channels.ts
Normal file
133
electron/api/routes/channels.ts
Normal file
@@ -0,0 +1,133 @@
|
||||
import type { HostApiContext } from '../context';
|
||||
import type { NormalizedHostApiRequest } from '../route-utils';
|
||||
import { fail, ok, parseJsonBody } from '../route-utils';
|
||||
import {
|
||||
assignChannelToAgent,
|
||||
clearChannelBinding,
|
||||
listAgentsSnapshot,
|
||||
} from '../../utils/agent-config';
|
||||
import {
|
||||
listSelectedChannelAccountGroups,
|
||||
listSelectedChannelTargets,
|
||||
} from '../../utils/channels';
|
||||
|
||||
function getProviderSnapshot(ctx: HostApiContext) {
|
||||
const accounts = ctx.providerApiService
|
||||
.getAccounts()
|
||||
.filter((account) => account.enabled !== false);
|
||||
const defaultAccountId = ctx.providerApiService.getDefault().accountId;
|
||||
return { accounts, defaultAccountId };
|
||||
}
|
||||
|
||||
export async function handleChannelRoutes(
|
||||
request: NormalizedHostApiRequest,
|
||||
ctx: HostApiContext,
|
||||
) {
|
||||
const { pathname, method } = request;
|
||||
const { accounts, defaultAccountId } = getProviderSnapshot(ctx);
|
||||
const snapshot = listAgentsSnapshot(accounts, defaultAccountId);
|
||||
|
||||
if (pathname === '/api/channels/accounts' && method === 'GET') {
|
||||
return ok({
|
||||
success: true,
|
||||
channels: listSelectedChannelAccountGroups(snapshot),
|
||||
});
|
||||
}
|
||||
|
||||
if (pathname === '/api/channels/targets' && method === 'GET') {
|
||||
const channelType = request.url.searchParams.get('channelType')?.trim() || '';
|
||||
const accountId = request.url.searchParams.get('accountId')?.trim() || null;
|
||||
const query = request.url.searchParams.get('query')?.trim() || null;
|
||||
|
||||
if (!channelType) {
|
||||
return fail(400, 'channelType is required');
|
||||
}
|
||||
|
||||
return ok({
|
||||
success: true,
|
||||
targets: listSelectedChannelTargets(channelType, accountId, query),
|
||||
});
|
||||
}
|
||||
|
||||
if (pathname === '/api/channels/binding' && method === 'PUT') {
|
||||
try {
|
||||
const body = parseJsonBody<{
|
||||
channelType?: string;
|
||||
accountId?: string | null;
|
||||
agentId?: string;
|
||||
}>(request.body);
|
||||
const channelType = String(body?.channelType ?? '').trim();
|
||||
const accountId = String(body?.accountId ?? '').trim();
|
||||
const agentId = String(body?.agentId ?? '').trim();
|
||||
|
||||
if (!channelType) {
|
||||
return fail(400, 'channelType is required');
|
||||
}
|
||||
if (!accountId) {
|
||||
return fail(400, 'accountId is required');
|
||||
}
|
||||
if (!agentId) {
|
||||
return fail(400, 'agentId is required');
|
||||
}
|
||||
if (!snapshot.agents.some((agent) => agent.id === agentId)) {
|
||||
return fail(404, `Agent "${agentId}" not found`);
|
||||
}
|
||||
|
||||
const groupedChannels = listSelectedChannelAccountGroups(snapshot);
|
||||
const group = groupedChannels.find((entry) => entry.channelType === channelType);
|
||||
if (!group || !group.accounts.some((entry) => entry.accountId === accountId)) {
|
||||
return fail(404, `Channel account "${channelType}:${accountId}" not found`);
|
||||
}
|
||||
|
||||
const result = assignChannelToAgent(agentId, channelType, accountId, accounts, defaultAccountId);
|
||||
ctx.gatewayManager.notifyRuntimeChanged({
|
||||
topics: ['agents', 'channels', 'channel-targets'],
|
||||
reason: 'channels:binding-updated',
|
||||
channelType,
|
||||
accountId,
|
||||
});
|
||||
|
||||
return ok({
|
||||
success: true,
|
||||
...result,
|
||||
});
|
||||
} catch (error) {
|
||||
return fail(500, error instanceof Error ? error.message : String(error));
|
||||
}
|
||||
}
|
||||
|
||||
if (pathname === '/api/channels/binding' && method === 'DELETE') {
|
||||
try {
|
||||
const body = parseJsonBody<{
|
||||
channelType?: string;
|
||||
accountId?: string | null;
|
||||
}>(request.body);
|
||||
const channelType = String(body?.channelType ?? '').trim();
|
||||
const accountId = String(body?.accountId ?? '').trim();
|
||||
|
||||
if (!channelType) {
|
||||
return fail(400, 'channelType is required');
|
||||
}
|
||||
if (!accountId) {
|
||||
return fail(400, 'accountId is required');
|
||||
}
|
||||
|
||||
const result = clearChannelBinding(channelType, accountId, accounts, defaultAccountId);
|
||||
ctx.gatewayManager.notifyRuntimeChanged({
|
||||
topics: ['agents', 'channels', 'channel-targets'],
|
||||
reason: 'channels:binding-cleared',
|
||||
channelType,
|
||||
accountId,
|
||||
});
|
||||
|
||||
return ok({
|
||||
success: true,
|
||||
...result,
|
||||
});
|
||||
} catch (error) {
|
||||
return fail(500, error instanceof Error ? error.message : String(error));
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
86
electron/api/routes/cron.ts
Normal file
86
electron/api/routes/cron.ts
Normal file
@@ -0,0 +1,86 @@
|
||||
import type { HostApiContext } from '../context';
|
||||
import type { NormalizedHostApiRequest } from '../route-utils';
|
||||
import { fail, ok, parseJsonBody } from '../route-utils';
|
||||
import {
|
||||
createCronJob,
|
||||
deleteCronJob,
|
||||
listCronJobs,
|
||||
toggleCronJob,
|
||||
triggerCronJob,
|
||||
updateCronJob,
|
||||
} from '../../utils/cron-store';
|
||||
import type { CronJobCreateInput, CronJobUpdateInput } from '@src/lib/cron-types';
|
||||
|
||||
export async function handleCronRoutes(
|
||||
request: NormalizedHostApiRequest,
|
||||
_ctx: HostApiContext,
|
||||
) {
|
||||
const { pathname, method } = request;
|
||||
|
||||
if (pathname === '/api/cron/jobs' && method === 'GET') {
|
||||
return ok(listCronJobs());
|
||||
}
|
||||
|
||||
if (pathname === '/api/cron/jobs' && method === 'POST') {
|
||||
try {
|
||||
const body = parseJsonBody<CronJobCreateInput & { agentId?: string | null }>(request.body);
|
||||
return ok(createCronJob(body), 201);
|
||||
} catch (error) {
|
||||
return fail(400, error instanceof Error ? error.message : String(error));
|
||||
}
|
||||
}
|
||||
|
||||
if (pathname === '/api/cron/toggle' && method === 'POST') {
|
||||
try {
|
||||
const body = parseJsonBody<{ id?: string; enabled?: boolean }>(request.body);
|
||||
if (!body?.id) {
|
||||
return fail(400, 'id is required');
|
||||
}
|
||||
|
||||
return ok(toggleCronJob(body.id, body.enabled !== false));
|
||||
} catch (error) {
|
||||
return fail(400, error instanceof Error ? error.message : String(error));
|
||||
}
|
||||
}
|
||||
|
||||
if (pathname === '/api/cron/trigger' && method === 'POST') {
|
||||
try {
|
||||
const body = parseJsonBody<{ id?: string }>(request.body);
|
||||
if (!body?.id) {
|
||||
return fail(400, 'id is required');
|
||||
}
|
||||
|
||||
return ok(triggerCronJob(body.id));
|
||||
} catch (error) {
|
||||
return fail(400, error instanceof Error ? error.message : String(error));
|
||||
}
|
||||
}
|
||||
|
||||
if (!pathname.startsWith('/api/cron/jobs/')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const jobId = decodeURIComponent(pathname.slice('/api/cron/jobs/'.length)).trim();
|
||||
if (!jobId) {
|
||||
return fail(400, 'id is required');
|
||||
}
|
||||
|
||||
if (method === 'PUT') {
|
||||
try {
|
||||
const body = parseJsonBody<CronJobUpdateInput & { agentId?: string | null }>(request.body);
|
||||
return ok(updateCronJob(jobId, body));
|
||||
} catch (error) {
|
||||
return fail(400, error instanceof Error ? error.message : String(error));
|
||||
}
|
||||
}
|
||||
|
||||
if (method === 'DELETE') {
|
||||
try {
|
||||
return ok(deleteCronJob(jobId));
|
||||
} catch (error) {
|
||||
return fail(400, error instanceof Error ? error.message : String(error));
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
@@ -1,9 +1,10 @@
|
||||
import crypto from 'node:crypto';
|
||||
import { app, nativeImage } from 'electron';
|
||||
import { nativeImage } from 'electron';
|
||||
import { extname, join } from 'node:path';
|
||||
import type { HostApiContext } from '../context';
|
||||
import type { NormalizedHostApiRequest } from '../route-utils';
|
||||
import { ok, parseJsonBody } from '../route-utils';
|
||||
import { getUserDataDir } from '@electron/utils/paths';
|
||||
|
||||
const EXT_MIME_MAP: Record<string, string> = {
|
||||
'.png': 'image/png',
|
||||
@@ -29,7 +30,7 @@ function mimeToExt(mimeType: string): string {
|
||||
return '';
|
||||
}
|
||||
|
||||
const OUTBOUND_DIR = join(app.getPath('userData'), 'openclaw-media', 'outbound');
|
||||
const OUTBOUND_DIR = join(getUserDataDir(), 'openclaw-media', 'outbound');
|
||||
|
||||
async function generateImagePreview(filePath: string, mimeType: string): Promise<string | null> {
|
||||
try {
|
||||
|
||||
@@ -62,7 +62,7 @@ export async function handleModelRoutes(
|
||||
ctx: HostApiContext,
|
||||
) {
|
||||
const { pathname, method } = request;
|
||||
if ((pathname !== '/api/models' && pathname !== '/api/agents') || method !== 'GET') {
|
||||
if (pathname !== '/api/models' || method !== 'GET') {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user