feat(gateway): enhance gateway readiness handling and batch sync configuration (#851)

Co-authored-by: paisley <8197966+su8su@users.noreply.github.com>
This commit is contained in:
Haze
2026-04-14 15:42:37 +08:00
committed by GitHub
Unverified
parent 758a8f8c94
commit 30bd8c08f9
14 changed files with 626 additions and 69 deletions

View File

@@ -89,6 +89,9 @@ export function Channels() {
const [existingAccountIdsForModal, setExistingAccountIdsForModal] = useState<string[]>([]);
const [initialConfigValuesForModal, setInitialConfigValuesForModal] = useState<Record<string, string> | undefined>(undefined);
const [deleteTarget, setDeleteTarget] = useState<DeleteTarget | null>(null);
const convergenceRefreshTimersRef = useRef<number[]>([]);
const fetchInFlightRef = useRef(false);
const queuedFetchOptionsRef = useRef<{ probe?: boolean } | null>(null);
const displayedChannelTypes = getPrimaryChannels();
const visibleChannelGroups = channelGroups;
@@ -104,7 +107,24 @@ export function Channels() {
const agentsRef = useRef(agents);
agentsRef.current = agents;
const fetchPageData = useCallback(async () => {
const mergeFetchOptions = (
base: { probe?: boolean } | null,
incoming: { probe?: boolean } | undefined,
): { probe?: boolean } => {
return {
probe: Boolean(base?.probe) || Boolean(incoming?.probe),
};
};
const fetchPageData = useCallback(async (options?: { probe?: boolean }) => {
if (fetchInFlightRef.current) {
queuedFetchOptionsRef.current = mergeFetchOptions(queuedFetchOptionsRef.current, options);
return;
}
fetchInFlightRef.current = true;
const startedAt = Date.now();
const probe = options?.probe === true;
console.info(`[channels-ui] fetch start 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) {
@@ -113,7 +133,9 @@ export function Channels() {
setError(null);
try {
const [channelsRes, agentsRes] = await Promise.all([
hostApiFetch<{ success: boolean; channels?: ChannelGroupItem[]; error?: string }>('/api/channels/accounts'),
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'),
]);
@@ -127,20 +149,64 @@ export function Channels() {
setChannelGroups(channelsRes.channels || []);
setAgents(agentsRes.agents || []);
console.info(
`[channels-ui] fetch ok probe=${probe ? '1' : '0'} elapsedMs=${Date.now() - startedAt} view=${(channelsRes.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)}`
);
} finally {
fetchInFlightRef.current = false;
setLoading(false);
const queued = queuedFetchOptionsRef.current;
if (queued) {
queuedFetchOptionsRef.current = null;
void fetchPageData(queued);
}
}
// Stable reference — reads state via refs, no deps needed.
}, []);
const clearConvergenceRefreshTimers = useCallback(() => {
convergenceRefreshTimersRef.current.forEach((timerId) => {
window.clearTimeout(timerId);
});
convergenceRefreshTimersRef.current = [];
}, []);
const scheduleConvergenceRefresh = useCallback(() => {
clearConvergenceRefreshTimers();
// Channel adapters can take time to reconnect after gateway restart.
// First few rounds use probe=true to force runtime connectivity checks,
// then fall back to cached pulls to reduce load.
[
{ delay: 1200, probe: true },
{ delay: 2600, probe: false },
{ delay: 4500, probe: false },
{ delay: 7000, probe: false },
{ delay: 10500, probe: false },
].forEach(({ delay, probe }) => {
const timerId = window.setTimeout(() => {
void fetchPageData({ probe });
}, delay);
convergenceRefreshTimersRef.current.push(timerId);
});
}, [clearConvergenceRefreshTimers, fetchPageData]);
useEffect(() => {
void fetchPageData();
}, [fetchPageData]);
useEffect(() => {
return () => {
clearConvergenceRefreshTimers();
};
}, [clearConvergenceRefreshTimers]);
useEffect(() => {
// Throttle channel-status events to avoid flooding fetchPageData during AI tasks.
let throttleTimer: ReturnType<typeof setTimeout> | null = null;
@@ -176,8 +242,9 @@ export function Channels() {
if (previousGatewayState !== 'running' && gatewayStatus.state === 'running') {
void fetchPageData();
scheduleConvergenceRefresh();
}
}, [fetchPageData, gatewayStatus.state]);
}, [fetchPageData, gatewayStatus.state, scheduleConvergenceRefresh]);
const configuredTypes = useMemo(
() => visibleChannelGroups.map((group) => group.channelType),
@@ -199,7 +266,7 @@ export function Channels() {
const unsupportedGroups = displayedChannelTypes.filter((type) => !configuredTypes.includes(type));
const handleRefresh = () => {
void fetchPageData();
void fetchPageData({ probe: true });
};
const handleBindAgent = async (channelType: string, accountId: string, agentId: string) => {
@@ -525,7 +592,8 @@ export function Channels() {
setInitialConfigValuesForModal(undefined);
}}
onChannelSaved={async () => {
await fetchPageData();
await fetchPageData({ probe: true });
scheduleConvergenceRefresh();
setShowConfigModal(false);
setSelectedChannelType(null);
setSelectedAccountId(undefined);