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:
358
src/stores/agents.ts
Normal file
358
src/stores/agents.ts
Normal file
@@ -0,0 +1,358 @@
|
||||
import { useSyncExternalStore } from 'react';
|
||||
import {
|
||||
DEFAULT_AGENT_ID,
|
||||
DEFAULT_MAIN_SESSION_SUFFIX,
|
||||
buildMainSessionKey,
|
||||
buildChannelAccountOwnerKey as buildAgentChannelAccountOwnerKey,
|
||||
normalizeAgentId,
|
||||
normalizeChannelAccountId,
|
||||
normalizeChannelType,
|
||||
resolveChannelAccountOwner,
|
||||
type AgentChannelBindingInput,
|
||||
type AgentChannelUnbindingInput,
|
||||
type AgentSummary,
|
||||
type AgentsSnapshot,
|
||||
} from '@runtime/lib/agents';
|
||||
import { onGatewayEvent } from '../lib/gateway-client';
|
||||
import { hostApiFetch } from '../lib/host-api';
|
||||
import { isRuntimeChangedGatewayEvent, runtimeEventHasTopic } from '../lib/runtime-events';
|
||||
|
||||
export interface AgentsStoreState {
|
||||
initialized: boolean;
|
||||
loading: boolean;
|
||||
error: string | null;
|
||||
warning: string | null;
|
||||
agents: AgentSummary[];
|
||||
defaultAgentId: string;
|
||||
defaultProviderAccountId: string | null;
|
||||
defaultModelRef: string | null;
|
||||
mainSessionSuffix: string;
|
||||
configuredChannelTypes: string[];
|
||||
channelOwners: Record<string, string>;
|
||||
channelAccountOwners: Record<string, string>;
|
||||
}
|
||||
|
||||
type AgentsSnapshotResponse = AgentsSnapshot & {
|
||||
success?: boolean;
|
||||
models?: AgentSummary[];
|
||||
defaultAccountId?: string | null;
|
||||
warning?: string | null;
|
||||
};
|
||||
|
||||
type MutationResult = AgentsSnapshotResponse | { success?: boolean } | void;
|
||||
|
||||
const listeners = new Set<() => void>();
|
||||
|
||||
let loadAgentsInFlight: Promise<void> | null = null;
|
||||
let gatewayRuntimeSubscribed = false;
|
||||
let state: AgentsStoreState = {
|
||||
initialized: false,
|
||||
loading: false,
|
||||
error: null,
|
||||
warning: null,
|
||||
agents: [],
|
||||
defaultAgentId: DEFAULT_AGENT_ID,
|
||||
defaultProviderAccountId: null,
|
||||
defaultModelRef: null,
|
||||
mainSessionSuffix: DEFAULT_MAIN_SESSION_SUFFIX,
|
||||
configuredChannelTypes: [],
|
||||
channelOwners: {},
|
||||
channelAccountOwners: {},
|
||||
};
|
||||
|
||||
function emit(): void {
|
||||
for (const listener of listeners) {
|
||||
listener();
|
||||
}
|
||||
}
|
||||
|
||||
function patchState(patch: Partial<AgentsStoreState>): AgentsStoreState {
|
||||
state = { ...state, ...patch };
|
||||
emit();
|
||||
return state;
|
||||
}
|
||||
|
||||
function sanitizeAgent(agent: AgentSummary, mainSessionSuffix: string): AgentSummary {
|
||||
const normalizedId = normalizeAgentId(agent.id);
|
||||
const normalizedMainSessionKey = agent.mainSessionKey || buildMainSessionKey(normalizedId, mainSessionSuffix);
|
||||
|
||||
return {
|
||||
id: normalizedId,
|
||||
name: agent.name || normalizedId,
|
||||
isDefault: Boolean(agent.isDefault),
|
||||
providerAccountId: agent.providerAccountId ?? null,
|
||||
modelRef: agent.modelRef ?? null,
|
||||
modelDisplay: agent.modelDisplay || agent.modelRef || agent.name || normalizedId,
|
||||
mainSessionKey: normalizedMainSessionKey,
|
||||
vendorId: agent.vendorId ?? null,
|
||||
source: agent.source,
|
||||
overrideModelRef: agent.overrideModelRef ?? null,
|
||||
inheritedModel: Boolean(agent.inheritedModel),
|
||||
workspace: agent.workspace,
|
||||
agentDir: agent.agentDir,
|
||||
channelTypes: Array.isArray(agent.channelTypes) ? agent.channelTypes : [],
|
||||
};
|
||||
}
|
||||
|
||||
function collectAgents(snapshot?: AgentsSnapshotResponse): AgentSummary[] {
|
||||
const rawAgents = Array.isArray(snapshot?.agents)
|
||||
? snapshot.agents
|
||||
: Array.isArray(snapshot?.models)
|
||||
? snapshot.models
|
||||
: [];
|
||||
const mainSessionSuffix = snapshot?.mainSessionSuffix || DEFAULT_MAIN_SESSION_SUFFIX;
|
||||
return rawAgents.map((agent) => sanitizeAgent(agent, mainSessionSuffix));
|
||||
}
|
||||
|
||||
function normalizeWarning(snapshot?: AgentsSnapshotResponse): string | null {
|
||||
const warning = typeof snapshot?.warning === 'string' ? snapshot.warning.trim() : '';
|
||||
return warning || null;
|
||||
}
|
||||
|
||||
function patchSnapshot(snapshot: AgentsSnapshotResponse): void {
|
||||
patchState({
|
||||
initialized: true,
|
||||
loading: false,
|
||||
error: null,
|
||||
warning: normalizeWarning(snapshot),
|
||||
agents: collectAgents(snapshot),
|
||||
defaultAgentId: snapshot?.defaultAgentId ? normalizeAgentId(snapshot.defaultAgentId) : DEFAULT_AGENT_ID,
|
||||
defaultProviderAccountId: snapshot?.defaultProviderAccountId ?? snapshot?.defaultAccountId ?? null,
|
||||
defaultModelRef: snapshot?.defaultModelRef ?? null,
|
||||
mainSessionSuffix: snapshot?.mainSessionSuffix || DEFAULT_MAIN_SESSION_SUFFIX,
|
||||
configuredChannelTypes: snapshot?.configuredChannelTypes ?? [],
|
||||
channelOwners: snapshot?.channelOwners ?? {},
|
||||
channelAccountOwners: snapshot?.channelAccountOwners ?? {},
|
||||
});
|
||||
}
|
||||
|
||||
function isAgentsSnapshotResponse(value: unknown): value is AgentsSnapshotResponse {
|
||||
if (!value || typeof value !== 'object') return false;
|
||||
const candidate = value as Partial<AgentsSnapshotResponse>;
|
||||
return Array.isArray(candidate.agents)
|
||||
|| Array.isArray(candidate.models)
|
||||
|| typeof candidate.defaultAgentId === 'string';
|
||||
}
|
||||
|
||||
async function syncMutationResult(result: MutationResult): Promise<void> {
|
||||
if (isAgentsSnapshotResponse(result)) {
|
||||
patchSnapshot(result);
|
||||
return;
|
||||
}
|
||||
|
||||
await loadAgents();
|
||||
}
|
||||
|
||||
function subscribeToRuntimeRefreshEvents(): void {
|
||||
if (gatewayRuntimeSubscribed) {
|
||||
return;
|
||||
}
|
||||
|
||||
gatewayRuntimeSubscribed = true;
|
||||
onGatewayEvent((event) => {
|
||||
if (isRuntimeChangedGatewayEvent(event) && runtimeEventHasTopic(event, 'agents', 'providers', 'models', 'channels')) {
|
||||
void loadAgents();
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.type === 'gateway:status' && event.status === 'connected' && state.initialized) {
|
||||
void loadAgents();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function loadAgents(): Promise<void> {
|
||||
subscribeToRuntimeRefreshEvents();
|
||||
|
||||
if (loadAgentsInFlight) {
|
||||
await loadAgentsInFlight;
|
||||
return;
|
||||
}
|
||||
|
||||
loadAgentsInFlight = (async () => {
|
||||
patchState({ loading: true, error: null });
|
||||
|
||||
try {
|
||||
const snapshot = await hostApiFetch<AgentsSnapshotResponse>('/api/agents');
|
||||
patchSnapshot(snapshot);
|
||||
} catch (error) {
|
||||
patchState({
|
||||
initialized: true,
|
||||
loading: false,
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
});
|
||||
}
|
||||
})();
|
||||
|
||||
try {
|
||||
await loadAgentsInFlight;
|
||||
} finally {
|
||||
loadAgentsInFlight = null;
|
||||
}
|
||||
}
|
||||
|
||||
function subscribe(listener: () => void): () => void {
|
||||
listeners.add(listener);
|
||||
return () => listeners.delete(listener);
|
||||
}
|
||||
|
||||
function getSnapshot(): AgentsStoreState {
|
||||
return state;
|
||||
}
|
||||
|
||||
function getAgentById(agentId: string | null | undefined): AgentSummary | undefined {
|
||||
const normalizedId = normalizeAgentId(agentId);
|
||||
return state.agents.find((agent) => agent.id === normalizedId);
|
||||
}
|
||||
|
||||
export const agentsStore = {
|
||||
subscribe,
|
||||
getSnapshot,
|
||||
getState: () => state,
|
||||
init: loadAgents,
|
||||
load: loadAgents,
|
||||
fetchAgents: loadAgents,
|
||||
getAgentById,
|
||||
resolveMainSessionKey(agentId: string | null | undefined): string {
|
||||
const normalizedId = normalizeAgentId(agentId || state.defaultAgentId);
|
||||
return getAgentById(normalizedId)?.mainSessionKey || buildMainSessionKey(normalizedId, state.mainSessionSuffix);
|
||||
},
|
||||
getChannelOwner(channelType: string): string | null {
|
||||
const normalizedChannelType = normalizeChannelType(channelType);
|
||||
const owner = state.channelOwners[normalizedChannelType];
|
||||
return typeof owner === 'string' && owner.trim() ? owner : null;
|
||||
},
|
||||
getChannelAccountOwner(channelType: string, accountId?: string | null): string | null {
|
||||
return resolveChannelAccountOwner(state.channelAccountOwners, channelType, accountId);
|
||||
},
|
||||
async createAgent(name: string, options?: { inheritWorkspace?: boolean }): Promise<void> {
|
||||
patchState({ error: null });
|
||||
try {
|
||||
const snapshot = await hostApiFetch<AgentsSnapshotResponse>('/api/agents', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ name, inheritWorkspace: options?.inheritWorkspace }),
|
||||
});
|
||||
await syncMutationResult(snapshot);
|
||||
} catch (error) {
|
||||
patchState({ error: error instanceof Error ? error.message : String(error) });
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
async updateAgent(agentId: string, name: string): Promise<void> {
|
||||
patchState({ error: null });
|
||||
try {
|
||||
const snapshot = await hostApiFetch<AgentsSnapshotResponse>(`/api/agents/${encodeURIComponent(agentId)}`, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify({ name }),
|
||||
});
|
||||
await syncMutationResult(snapshot);
|
||||
} catch (error) {
|
||||
patchState({ error: error instanceof Error ? error.message : String(error) });
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
async updateAgentModel(
|
||||
agentId: string,
|
||||
modelRef: string | null,
|
||||
options?: { providerAccountId?: string | null },
|
||||
): Promise<void> {
|
||||
patchState({ error: null });
|
||||
try {
|
||||
const snapshot = await hostApiFetch<AgentsSnapshotResponse>(`/api/agents/${encodeURIComponent(agentId)}/model`, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify({ modelRef, providerAccountId: options?.providerAccountId ?? null }),
|
||||
});
|
||||
await syncMutationResult(snapshot);
|
||||
} catch (error) {
|
||||
patchState({ error: error instanceof Error ? error.message : String(error) });
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
async assignChannel(agentId: string, channelType: string): Promise<void> {
|
||||
patchState({ error: null });
|
||||
try {
|
||||
const snapshot = await hostApiFetch<AgentsSnapshotResponse>(`/api/agents/${encodeURIComponent(agentId)}/channels/${encodeURIComponent(channelType)}`, {
|
||||
method: 'PUT',
|
||||
});
|
||||
await syncMutationResult(snapshot);
|
||||
} catch (error) {
|
||||
patchState({ error: error instanceof Error ? error.message : String(error) });
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
async removeChannel(agentId: string, channelType: string): Promise<void> {
|
||||
patchState({ error: null });
|
||||
try {
|
||||
const snapshot = await hostApiFetch<AgentsSnapshotResponse>(`/api/agents/${encodeURIComponent(agentId)}/channels/${encodeURIComponent(channelType)}`, {
|
||||
method: 'DELETE',
|
||||
});
|
||||
await syncMutationResult(snapshot);
|
||||
} catch (error) {
|
||||
patchState({ error: error instanceof Error ? error.message : String(error) });
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
async bindChannelAccount(input: AgentChannelBindingInput): Promise<void> {
|
||||
patchState({ error: null });
|
||||
try {
|
||||
const result = await hostApiFetch<MutationResult>('/api/channels/binding', {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify({
|
||||
agentId: normalizeAgentId(input.agentId),
|
||||
channelType: normalizeChannelType(input.channelType),
|
||||
accountId: normalizeChannelAccountId(input.accountId),
|
||||
}),
|
||||
});
|
||||
await syncMutationResult(result);
|
||||
} catch (error) {
|
||||
patchState({ error: error instanceof Error ? error.message : String(error) });
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
async assignChannelAccount(agentId: string, channelType: string, accountId: string): Promise<void> {
|
||||
await agentsStore.bindChannelAccount({ agentId, channelType, accountId });
|
||||
},
|
||||
async unbindChannelAccount(input: AgentChannelUnbindingInput): Promise<void> {
|
||||
patchState({ error: null });
|
||||
try {
|
||||
const result = await hostApiFetch<MutationResult>('/api/channels/binding', {
|
||||
method: 'DELETE',
|
||||
body: JSON.stringify({
|
||||
channelType: normalizeChannelType(input.channelType),
|
||||
accountId: normalizeChannelAccountId(input.accountId),
|
||||
}),
|
||||
});
|
||||
await syncMutationResult(result);
|
||||
} catch (error) {
|
||||
patchState({ error: error instanceof Error ? error.message : String(error) });
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
async clearChannelBinding(channelType: string, accountId: string): Promise<void> {
|
||||
await agentsStore.unbindChannelAccount({ channelType, accountId });
|
||||
},
|
||||
buildChannelAccountOwnerKey(channelType: string, accountId?: string | null): string {
|
||||
return buildAgentChannelAccountOwnerKey(channelType, accountId);
|
||||
},
|
||||
async deleteAgent(agentId: string): Promise<void> {
|
||||
patchState({ error: null });
|
||||
try {
|
||||
const snapshot = await hostApiFetch<AgentsSnapshotResponse>(`/api/agents/${encodeURIComponent(agentId)}`, {
|
||||
method: 'DELETE',
|
||||
});
|
||||
await syncMutationResult(snapshot);
|
||||
} catch (error) {
|
||||
patchState({ error: error instanceof Error ? error.message : String(error) });
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
clearError(): void {
|
||||
patchState({ error: null });
|
||||
},
|
||||
refresh: loadAgents,
|
||||
};
|
||||
|
||||
export function useAgentsStore<T = AgentsStoreState>(selector?: (state: AgentsStoreState) => T): T {
|
||||
const select = selector ?? ((current: AgentsStoreState) => current as unknown as T);
|
||||
return useSyncExternalStore(subscribe, () => select(getSnapshot()), () => select(getSnapshot()));
|
||||
}
|
||||
@@ -13,7 +13,7 @@ import { extractText, isToolOnlyMessage } from '../shared/chat-model';
|
||||
import { gatewayRpc, onGatewayEvent } from '../lib/gateway-client';
|
||||
import { hostApiFetch } from '../lib/host-api';
|
||||
import type { GatewayEvent } from '../types/runtime';
|
||||
import { modelsStore } from './models';
|
||||
import { agentsStore } from './agents';
|
||||
|
||||
const SESSION_LOAD_MIN_INTERVAL_MS = 1200;
|
||||
const HISTORY_LOAD_MIN_INTERVAL_MS = 800;
|
||||
@@ -177,19 +177,19 @@ function queueStreamingDelta(delta: string, runId?: string): void {
|
||||
function getAgentIdFromSessionKey(sessionKey: string): string {
|
||||
const parsed = parseSessionKey(normalizeAgentSessionKey(sessionKey));
|
||||
if (parsed.isAgentSession) return parsed.agentId;
|
||||
return modelsStore.getState().defaultAgentId || FALLBACK_AGENT_ID;
|
||||
return agentsStore.getState().defaultAgentId || FALLBACK_AGENT_ID;
|
||||
}
|
||||
|
||||
function getDefaultAgentId(): string {
|
||||
return modelsStore.getState().defaultAgentId || FALLBACK_AGENT_ID;
|
||||
return agentsStore.getState().defaultAgentId || FALLBACK_AGENT_ID;
|
||||
}
|
||||
|
||||
function getDefaultMainSessionKey(): string {
|
||||
return modelsStore.resolveMainSessionKey(getDefaultAgentId()) || FALLBACK_MAIN_SESSION_KEY;
|
||||
return agentsStore.resolveMainSessionKey(getDefaultAgentId()) || FALLBACK_MAIN_SESSION_KEY;
|
||||
}
|
||||
|
||||
function resolveMainSessionKeyForAgent(agentId: string | null | undefined): string {
|
||||
return modelsStore.resolveMainSessionKey(agentId || getDefaultAgentId()) || getDefaultMainSessionKey();
|
||||
return agentsStore.resolveMainSessionKey(agentId || getDefaultAgentId()) || getDefaultMainSessionKey();
|
||||
}
|
||||
|
||||
function buildNewSessionKey(agentId: string | null | undefined): string {
|
||||
@@ -284,7 +284,8 @@ async function resolveDefaultAccountId(): Promise<string | null> {
|
||||
}
|
||||
|
||||
async function resolveProviderAccountIdForAgent(agentId: string | null | undefined): Promise<string | null> {
|
||||
const mappedAccountId = modelsStore.resolveProviderAccountId(agentId);
|
||||
const mappedAccountId = agentsStore.getAgentById(agentId)?.providerAccountId
|
||||
?? agentsStore.getState().defaultProviderAccountId;
|
||||
if (mappedAccountId) {
|
||||
return mappedAccountId;
|
||||
}
|
||||
@@ -359,7 +360,7 @@ async function subscribeToGateway(): Promise<void> {
|
||||
}
|
||||
|
||||
async function loadSessions(): Promise<void> {
|
||||
await modelsStore.init();
|
||||
await agentsStore.init();
|
||||
const now = Date.now();
|
||||
if (loadSessionsInFlight) {
|
||||
await loadSessionsInFlight;
|
||||
@@ -719,7 +720,7 @@ async function sendMessage(text: string, attachments: StagedAttachment[] = []):
|
||||
const targetAgentId = getAgentIdFromSessionKey(targetSessionKey);
|
||||
const providerAccountId = await resolveProviderAccountIdForAgent(targetAgentId);
|
||||
if (!providerAccountId) {
|
||||
patchState({ error: '请先前往模型管理页面配置并设置一个默认模型' });
|
||||
patchState({ error: '请先为当前 Agent 配置可用模型,或先在 Models 页面设置默认模型' });
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -929,7 +930,7 @@ function clearError(): void {
|
||||
}
|
||||
|
||||
async function initChatStore(): Promise<void> {
|
||||
await modelsStore.init();
|
||||
await agentsStore.init();
|
||||
await subscribeToGateway();
|
||||
await loadSessions();
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
export * from './settings';
|
||||
export * from './models';
|
||||
export * from './agents';
|
||||
export * from './chat';
|
||||
export * from './task';
|
||||
export * from './channel';
|
||||
|
||||
Reference in New Issue
Block a user