perf: speed up initial chat, channels, skills, and cron loading (#901)

This commit is contained in:
paisley
2026-04-23 18:49:47 +08:00
committed by GitHub
parent 4b14f977fa
commit 5137e706c9
8 changed files with 317 additions and 95 deletions

View File

@@ -98,6 +98,12 @@ interface DeleteTarget {
accountId?: string;
}
type FetchPageDataOptions = {
probe?: boolean;
configOnly?: boolean;
forceAgentsRefresh?: boolean;
};
function removeDeletedTarget(groups: ChannelGroupItem[], target: DeleteTarget): ChannelGroupItem[] {
if (target.accountId) {
return groups
@@ -143,7 +149,9 @@ export function Channels() {
const [deleteTarget, setDeleteTarget] = useState<DeleteTarget | null>(null);
const convergenceRefreshTimersRef = useRef<number[]>([]);
const fetchInFlightRef = useRef(false);
const queuedFetchOptionsRef = useRef<{ probe?: boolean } | null>(null);
const queuedFetchOptionsRef = useRef<FetchPageDataOptions | null>(null);
const agentsFetchInFlightRef = useRef<Promise<void> | null>(null);
const hasLoadedAgentsRef = useRef(false);
const displayedChannelTypes = getPrimaryChannels();
const visibleChannelGroups = channelGroups;
@@ -159,16 +167,46 @@ export function Channels() {
const agentsRef = useRef(agents);
agentsRef.current = agents;
const ensureAgentsLoaded = useCallback(async () => {
if (hasLoadedAgentsRef.current) return;
if (agentsFetchInFlightRef.current) {
await agentsFetchInFlightRef.current;
return;
}
agentsFetchInFlightRef.current = (async () => {
try {
const agentsRes = await hostApiFetch<{ success: boolean; agents?: AgentItem[]; error?: string }>('/api/agents');
if (!agentsRes.success) {
throw new Error(agentsRes.error || 'Failed to load agents');
}
setAgents(agentsRes.agents || []);
hasLoadedAgentsRef.current = true;
} catch (agentsError) {
console.warn(`[channels-ui] load agents failed error=${String(agentsError)}`);
} finally {
agentsFetchInFlightRef.current = null;
}
})();
await agentsFetchInFlightRef.current;
}, []);
const mergeFetchOptions = (
base: { probe?: boolean } | null,
incoming: { probe?: boolean } | undefined,
): { probe?: boolean } => {
base: FetchPageDataOptions | null,
incoming: FetchPageDataOptions | undefined,
): FetchPageDataOptions => {
if (!base) return incoming ?? {};
if (!incoming) return base;
return {
probe: Boolean(base?.probe) || Boolean(incoming?.probe),
// If either request needs runtime data, do not keep config-only mode.
configOnly: Boolean(base?.configOnly) && Boolean(incoming?.configOnly),
forceAgentsRefresh: Boolean(base?.forceAgentsRefresh) || Boolean(incoming?.forceAgentsRefresh),
};
};
const fetchPageData = useCallback(async (options?: { probe?: boolean }) => {
const fetchPageData = useCallback(async (options?: FetchPageDataOptions) => {
if (fetchInFlightRef.current) {
queuedFetchOptionsRef.current = mergeFetchOptions(queuedFetchOptionsRef.current, options);
return;
@@ -176,20 +214,27 @@ export function Channels() {
fetchInFlightRef.current = true;
const startedAt = Date.now();
const probe = options?.probe === true;
console.info(`[channels-ui] fetch start probe=${probe ? '1' : '0'}`);
const configOnly = options?.configOnly === true;
console.info(`[channels-ui] fetch start mode=${configOnly ? 'config' : 'runtime'} probe=${probe ? '1' : '0'}`);
// Only show loading spinner on first load (stale-while-revalidate).
const hasData = channelGroupsRef.current.length > 0 || agentsRef.current.length > 0;
if (!hasData) {
setLoading(true);
}
setError(null);
if (options?.forceAgentsRefresh) {
hasLoadedAgentsRef.current = false;
}
void ensureAgentsLoaded();
try {
const [channelsRes, agentsRes] = await Promise.all([
hostApiFetch<{ success: boolean; channels?: ChannelGroupItem[]; error?: string }>(
options?.probe ? '/api/channels/accounts?probe=1' : '/api/channels/accounts'
),
hostApiFetch<{ success: boolean; agents?: AgentItem[]; error?: string }>('/api/agents'),
]);
const channelsPath = configOnly
? '/api/channels/accounts?mode=config'
: options?.probe
? '/api/channels/accounts?probe=1'
: '/api/channels/accounts';
const channelsRes = await hostApiFetch<{ success: boolean; channels?: ChannelGroupItem[]; error?: string }>(
channelsPath
);
type ChannelsResponse = {
success: boolean;
@@ -203,23 +248,18 @@ export function Channels() {
throw new Error(channelsPayload.error || 'Failed to load channels');
}
if (!agentsRes.success) {
throw new Error(agentsRes.error || 'Failed to load agents');
}
setChannelGroups(channelsPayload.channels || []);
setAgents(agentsRes.agents || []);
setGatewayHealth(channelsPayload.gatewayHealth || DEFAULT_GATEWAY_HEALTH);
setDiagnosticsSnapshot(null);
setShowDiagnostics(false);
console.info(
`[channels-ui] fetch ok probe=${probe ? '1' : '0'} elapsedMs=${Date.now() - startedAt} view=${(channelsPayload.channels || []).map((item) => `${item.channelType}:${item.status}`).join(',')}`
`[channels-ui] fetch ok mode=${configOnly ? 'config' : 'runtime'} probe=${probe ? '1' : '0'} elapsedMs=${Date.now() - startedAt} view=${(channelsPayload.channels || []).map((item) => `${item.channelType}:${item.status}`).join(',')}`
);
} catch (fetchError) {
// Preserve previous data on error — don't clear channelGroups/agents.
setError(String(fetchError));
console.warn(
`[channels-ui] fetch fail probe=${probe ? '1' : '0'} elapsedMs=${Date.now() - startedAt} error=${String(fetchError)}`
`[channels-ui] fetch fail mode=${configOnly ? 'config' : 'runtime'} probe=${probe ? '1' : '0'} elapsedMs=${Date.now() - startedAt} error=${String(fetchError)}`
);
} finally {
fetchInFlightRef.current = false;
@@ -232,7 +272,7 @@ export function Channels() {
}
// Stable reference — reads state via refs, no deps needed.
}, []);
}, [ensureAgentsLoaded]);
const clearConvergenceRefreshTimers = useCallback(() => {
convergenceRefreshTimersRef.current.forEach((timerId) => {
@@ -261,6 +301,7 @@ export function Channels() {
}, [clearConvergenceRefreshTimers, fetchPageData]);
useEffect(() => {
void fetchPageData({ configOnly: true });
void fetchPageData();
}, [fetchPageData]);
@@ -329,7 +370,7 @@ export function Channels() {
const unsupportedGroups = displayedChannelTypes.filter((type) => !configuredTypes.includes(type));
const handleRefresh = () => {
void fetchPageData({ probe: true });
void fetchPageData({ probe: true, forceAgentsRefresh: true });
};
const fetchDiagnosticsSnapshot = useCallback(async (): Promise<GatewayDiagnosticSnapshot> => {