fix(webui): prevent redundant thread and media reloads (#5164)

This commit is contained in:
chengyongru
2026-07-30 10:25:22 +08:00
committed by GitHub
parent fc73d5ff39
commit 11fcd9cc5f
29 changed files with 1465 additions and 247 deletions
+52 -31
View File
@@ -633,7 +633,7 @@ export function SettingsView({
hostChromeInset = false,
}: SettingsViewProps) {
const { t } = useTranslation();
const { token } = useClient();
const { getToken, token } = useClient();
const pageVisible = usePageVisibility();
const remoteBrowserAccess =
typeof window !== "undefined" && !isLoopbackHost(window.location.hostname);
@@ -779,7 +779,7 @@ export function SettingsView({
const poll = async () => {
try {
const payload = await completeProviderOAuth(
token,
getToken(),
xaiOAuthFlow.provider,
xaiOAuthFlow.flow_id,
);
@@ -803,7 +803,7 @@ export function SettingsView({
cancelled = true;
if (timer !== null) window.clearTimeout(timer);
};
}, [applyPayload, closeXaiOAuthFlow, token, xaiOAuthFlow]);
}, [applyPayload, closeXaiOAuthFlow, getToken, xaiOAuthFlow]);
useEffect(() => {
if (!initialSettings || settings !== null) return;
@@ -815,7 +815,7 @@ export function SettingsView({
let cancelled = false;
const showLoading = settings === null;
if (showLoading) setLoading(true);
fetchSettings(token)
fetchSettings(getToken())
.then((payload) => {
if (!cancelled) {
applyPayload(payload);
@@ -831,30 +831,37 @@ export function SettingsView({
return () => {
cancelled = true;
};
}, [applyPayload, token]);
}, [applyPayload, getToken]);
const hasSettings = settings !== null;
useEffect(() => {
if (activeSection !== "overview" || !hasSettings || !pageVisible) return;
let cancelled = false;
const refresh = () => {
fetchSettingsUsage(token)
.then((usage) => {
if (cancelled) return;
let refreshing = false;
const refresh = async () => {
if (refreshing) return;
refreshing = true;
try {
const usage = await fetchSettingsUsage(getToken());
if (!cancelled) {
setSettings((current) => (current ? { ...current, usage } : current));
})
.catch(() => {});
}
} catch {
// Usage is best-effort telemetry; the settings snapshot remains usable.
} finally {
refreshing = false;
}
};
void refresh();
const interval = window.setInterval(refresh, 5000);
const onFocus = () => refresh();
const interval = window.setInterval(() => void refresh(), 5000);
const onFocus = () => void refresh();
window.addEventListener("focus", onFocus);
return () => {
cancelled = true;
window.clearInterval(interval);
window.removeEventListener("focus", onFocus);
};
}, [activeSection, hasSettings, pageVisible, token]);
}, [activeSection, getToken, hasSettings, pageVisible]);
useEffect(() => {
if (activeSection !== "apps") return;
@@ -863,7 +870,7 @@ export function SettingsView({
let retryCount = 0;
const loadCliApps = (showLoading: boolean) => {
if (showLoading) setCliAppsLoading(true);
fetchCliApps(token)
fetchCliApps(getToken())
.then((payload) => {
if (cancelled) return;
if (payload.catalog_refresh_pending && retryCount < CLI_APPS_REFRESH_MAX_RETRIES) {
@@ -889,15 +896,23 @@ export function SettingsView({
cancelled = true;
if (retry !== null) window.clearTimeout(retry);
};
}, [activeSection, token]);
}, [activeSection, getToken]);
useEffect(() => {
if (!["channels", "models", "browser", "runtime"].includes(activeSection)) return;
if (
!pageVisible
|| !["channels", "models", "browser", "runtime"].includes(activeSection)
) {
return;
}
let cancelled = false;
const refresh = async (showLoading = false) => {
let refreshing = false;
const refresh = async (showLoading = false): Promise<void> => {
if (refreshing) return;
refreshing = true;
if (showLoading) setNanobotFeaturesLoading(true);
try {
const payload = await fetchNanobotFeatures(token);
const payload = await fetchNanobotFeatures(getToken());
if (!cancelled) {
setNanobotFeatures(payload);
setNanobotFeaturesError(null);
@@ -906,6 +921,7 @@ export function SettingsView({
const message = (err as Error).message;
if (!cancelled && message !== "HTTP 404") setNanobotFeaturesError(message);
} finally {
refreshing = false;
if (!cancelled && showLoading) setNanobotFeaturesLoading(false);
}
};
@@ -926,13 +942,13 @@ export function SettingsView({
window.removeEventListener("focus", refreshOnFocus);
document.removeEventListener("visibilitychange", refreshOnFocus);
};
}, [activeSection, token]);
}, [activeSection, getToken, pageVisible]);
useEffect(() => {
if (activeSection !== "runtime") return;
let cancelled = false;
setApiServiceLoading(true);
fetchApiService(token)
fetchApiService(getToken())
.then((payload) => {
if (!cancelled) {
setApiService(payload);
@@ -948,13 +964,13 @@ export function SettingsView({
return () => {
cancelled = true;
};
}, [activeSection, token]);
}, [activeSection, getToken]);
useEffect(() => {
if (activeSection !== "apps") return;
let cancelled = false;
setMcpPresetsLoading(true);
fetchMcpPresets(token)
fetchMcpPresets(getToken())
.then((payload) => {
if (!cancelled) {
setMcpPresets(payload);
@@ -970,13 +986,13 @@ export function SettingsView({
return () => {
cancelled = true;
};
}, [activeSection, token]);
}, [activeSection, getToken]);
const refreshAutomations = useCallback(
async (showLoading = false) => {
if (showLoading) setAutomationsLoading(true);
try {
const payload = await fetchAutomations(token);
const payload = await fetchAutomations(getToken());
setAutomations(payload);
setAutomationsError(null);
} catch (err) {
@@ -985,23 +1001,26 @@ export function SettingsView({
if (showLoading) setAutomationsLoading(false);
}
},
[token],
[getToken],
);
useEffect(() => {
if (activeSection !== "automations" || !pageVisible) return;
let cancelled = false;
let refreshing = false;
const refresh = async (showLoading = false) => {
if (cancelled) return;
if (cancelled || refreshing) return;
refreshing = true;
if (showLoading) setAutomationsLoading(true);
try {
const payload = await fetchAutomations(token);
const payload = await fetchAutomations(getToken());
if (cancelled) return;
setAutomations(payload);
setAutomationsError(null);
} catch (err) {
if (!cancelled) setAutomationsError((err as Error).message);
} finally {
refreshing = false;
if (!cancelled && showLoading) setAutomationsLoading(false);
}
};
@@ -1014,7 +1033,7 @@ export function SettingsView({
window.clearInterval(interval);
window.removeEventListener("focus", refreshOnFocus);
};
}, [activeSection, pageVisible, token]);
}, [activeSection, getToken, pageVisible]);
useEffect(() => {
writeLocalPreferences(localPrefs);
@@ -8899,6 +8918,8 @@ function ModelIdPicker({
}) {
const { t } = useTranslation();
const tx = (key: string, fallback: string) => t(key, { defaultValue: fallback });
const tokenRef = useRef(token);
tokenRef.current = token;
const [open, setOpen] = useState(false);
const [query, setQuery] = useState("");
const [payload, setPayload] = useState<ProviderModelsPayload | null>(null);
@@ -8967,7 +8988,7 @@ function ModelIdPicker({
setPayload(null);
setError(null);
setLoading(true);
fetchProviderModels(token, effectiveProvider)
fetchProviderModels(tokenRef.current, effectiveProvider)
.then((nextPayload) => {
if (!cancelled) setPayload(nextPayload);
})
@@ -8980,7 +9001,7 @@ function ModelIdPicker({
return () => {
cancelled = true;
};
}, [effectiveProvider, open, shouldFetchModels, token]);
}, [effectiveProvider, open, shouldFetchModels]);
const selectModel = (model: string) => {
onChange(model);
@@ -302,7 +302,7 @@ function SkillDetailSheet({
open: boolean;
onOpenChange: (open: boolean) => void;
}) {
const { token } = useClient();
const { getToken } = useClient();
const { t } = useTranslation();
const [detail, setDetail] = useState<SkillDetail | null>(null);
const [loading, setLoading] = useState(false);
@@ -322,7 +322,7 @@ function SkillDetailSheet({
setActionError("");
setDeleteOpen(false);
setDescriptionExpanded(false);
fetchSkillDetail(token, skill.name)
fetchSkillDetail(getToken(), skill.name)
.then((payload) => {
if (!cancelled) setDetail(payload);
})
@@ -335,7 +335,7 @@ function SkillDetailSheet({
return () => {
cancelled = true;
};
}, [open, refreshKey, skill, token]);
}, [getToken, open, refreshKey, skill]);
if (!skill) return null;
@@ -354,7 +354,7 @@ function SkillDetailSheet({
setActionBusy(true);
setActionError("");
try {
const payload = await updateSkillEnabled(token, activeSkill.name, !enabled);
const payload = await updateSkillEnabled(getToken(), activeSkill.name, !enabled);
notifySkillsChanged(payload);
const updated = payload.skills.find((item) => item.name === activeSkill.name);
if (updated) {
@@ -378,7 +378,7 @@ function SkillDetailSheet({
setActionBusy(true);
setActionError("");
try {
const payload = await deleteSkill(token, activeSkill.name);
const payload = await deleteSkill(getToken(), activeSkill.name);
notifySkillsChanged(payload);
onOpenChange(false);
} catch (reason) {
@@ -45,7 +45,7 @@ export function SkillsMarketplace({
installing: string;
onInstallingChange: (skillId: string) => void;
}) {
const { token } = useClient();
const { getToken } = useClient();
const { t } = useTranslation();
const [query, setQuery] = useState("");
const [results, setResults] = useState<MarketplaceSkillSummary[]>([]);
@@ -78,7 +78,7 @@ export function SkillsMarketplace({
useEffect(() => {
let cancelled = false;
setTrendingLoading(true);
fetchTrendingMarketplaceSkills(token)
fetchTrendingMarketplaceSkills(getToken())
.then((payload) => {
if (cancelled) return;
setTrending(payload.skills);
@@ -92,7 +92,7 @@ export function SkillsMarketplace({
return () => {
cancelled = true;
};
}, [token]);
}, [getToken]);
useEffect(() => {
const skills = query.trim().length < 2 ? trending : results;
@@ -102,7 +102,7 @@ export function SkillsMarketplace({
if (!unresolved.length) return;
let cancelled = false;
fetchMarketplaceSkillTrends(token, unresolved.map((skill) => skill.id))
fetchMarketplaceSkillTrends(getToken(), unresolved.map((skill) => skill.id))
.then((payload) => {
if (!cancelled) {
setTrends((current) => ({ ...current, ...payload.trends }));
@@ -112,7 +112,7 @@ export function SkillsMarketplace({
return () => {
cancelled = true;
};
}, [query, results, token, trending, trends]);
}, [getToken, query, results, trending, trends]);
useEffect(() => {
const normalized = query.trim();
@@ -127,7 +127,7 @@ export function SkillsMarketplace({
const timer = window.setTimeout(() => {
setLoading(true);
setError("");
searchMarketplaceSkills(token, normalized)
searchMarketplaceSkills(getToken(), normalized)
.then((payload) => {
if (cancelled) return;
setResults(payload.skills);
@@ -152,7 +152,7 @@ export function SkillsMarketplace({
cancelled = true;
window.clearTimeout(timer);
};
}, [query, t, token]);
}, [getToken, query, t]);
const install = async (skill: MarketplaceSkillSummary) => {
setSelected(null);
@@ -160,7 +160,7 @@ export function SkillsMarketplace({
setError("");
try {
const payload = await installMarketplaceSkill(
token,
getToken(),
skill.provider,
skill.source,
skill.skill_id,
@@ -62,6 +62,8 @@ export function ChannelQrConnectFlow({
const [error, setError] = useState<string | null>(null);
const [handledRequestId, setHandledRequestId] = useState(0);
const pollInFlight = useRef(false);
const tokenRef = useRef(token);
tokenRef.current = token;
const startDomain = startOptions.domain;
const startInstanceId = startOptions.instanceId;
const startMode = startOptions.mode;
@@ -100,7 +102,11 @@ export function ChannelQrConnectFlow({
if (pollInFlight.current) return;
pollInFlight.current = true;
try {
const payload = await pollChannelConnect(token, channelName, connect.session_id);
const payload = await pollChannelConnect(
tokenRef.current,
channelName,
connect.session_id,
);
if (cancelled) return;
setConnect((current) => ({
...(current ?? payload),
@@ -129,13 +135,20 @@ export function ChannelQrConnectFlow({
window.clearTimeout(initial);
window.clearInterval(interval);
};
}, [channelName, connect?.interval_ms, connect?.session_id, connect?.status, onFeaturesUpdate, pageVisible, token]);
}, [
channelName,
connect?.interval_ms,
connect?.session_id,
connect?.status,
onFeaturesUpdate,
pageVisible,
]);
const start = useCallback(async (force = false) => {
setBusy(true);
setError(null);
try {
const payload = await startChannelConnect(token, channelName, {
const payload = await startChannelConnect(tokenRef.current, channelName, {
domain: startDomain,
instanceId: startInstanceId,
mode: startMode,
@@ -147,7 +160,7 @@ export function ChannelQrConnectFlow({
} finally {
setBusy(false);
}
}, [channelName, startDomain, startForce, startInstanceId, startMode, token]);
}, [channelName, startDomain, startForce, startInstanceId, startMode]);
useEffect(() => {
if (!connectRequestId || connectRequestId === handledRequestId) return;
@@ -162,7 +175,11 @@ export function ChannelQrConnectFlow({
}
setBusy(true);
try {
const payload = await cancelChannelConnect(token, channelName, connect.session_id);
const payload = await cancelChannelConnect(
tokenRef.current,
channelName,
connect.session_id,
);
setConnect(payload);
} catch (err) {
setError((err as Error).message);