mirror of
https://github.com/HKUDS/nanobot.git
synced 2026-06-16 15:54:10 +00:00
refactor(webui): replace real-time polling with click-to-check version updates
- Remove background PyPI polling loop and WebSocket broadcast - Remove UpdateBanner from ThreadHeader (keep main page clean) - Add on-demand version check endpoint (GET /api/settings/version-check) - Add 'About' section in Settings > Overview with check-for-updates button - Design: no auto-fetch, user initiates check explicitly via button click
This commit is contained in:
parent
c00371c761
commit
4255656089
@ -34,9 +34,18 @@ from nanobot.webui.workspaces import (
|
|||||||
write_webui_default_access_mode,
|
write_webui_default_access_mode,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
from nanobot import __version__
|
||||||
|
|
||||||
QueryParams = dict[str, list[str]]
|
QueryParams = dict[str, list[str]]
|
||||||
RuntimeSurface = Literal["browser", "native"]
|
RuntimeSurface = Literal["browser", "native"]
|
||||||
|
|
||||||
|
|
||||||
|
def _version_payload() -> dict[str, Any]:
|
||||||
|
"""Return version info for the settings payload."""
|
||||||
|
return {
|
||||||
|
"current": __version__,
|
||||||
|
}
|
||||||
|
|
||||||
_RUNTIME_CAPABILITIES = {
|
_RUNTIME_CAPABILITIES = {
|
||||||
"can_restart_engine": False,
|
"can_restart_engine": False,
|
||||||
"can_pick_folder": False,
|
"can_pick_folder": False,
|
||||||
@ -805,6 +814,7 @@ def settings_payload(
|
|||||||
"exec_path_append_set": bool(exec_config.path_append),
|
"exec_path_append_set": bool(exec_config.path_append),
|
||||||
},
|
},
|
||||||
"requires_restart": requires_restart,
|
"requires_restart": requires_restart,
|
||||||
|
"version": _version_payload(),
|
||||||
}
|
}
|
||||||
return decorate_settings_payload(
|
return decorate_settings_payload(
|
||||||
payload,
|
payload,
|
||||||
|
|||||||
@ -36,6 +36,7 @@ from nanobot.webui.settings_api import (
|
|||||||
update_transcription_settings,
|
update_transcription_settings,
|
||||||
update_web_search_settings,
|
update_web_search_settings,
|
||||||
)
|
)
|
||||||
|
from nanobot.webui.version_check import check_for_update
|
||||||
|
|
||||||
QueryParams = dict[str, list[str]]
|
QueryParams = dict[str, list[str]]
|
||||||
|
|
||||||
@ -117,6 +118,8 @@ class WebUISettingsRouter:
|
|||||||
return await self._handle_settings_cli_apps_action(request, "test")
|
return await self._handle_settings_cli_apps_action(request, "test")
|
||||||
if path == "/api/settings/mcp-presets":
|
if path == "/api/settings/mcp-presets":
|
||||||
return await self._handle_settings_mcp_presets(request)
|
return await self._handle_settings_mcp_presets(request)
|
||||||
|
if path == "/api/settings/version-check":
|
||||||
|
return await self._handle_settings_version_check(request)
|
||||||
mcp_action = _MCP_PRESET_ACTIONS_BY_PATH.get(path)
|
mcp_action = _MCP_PRESET_ACTIONS_BY_PATH.get(path)
|
||||||
if mcp_action is not None:
|
if mcp_action is not None:
|
||||||
return await self._handle_settings_mcp_presets(request, mcp_action)
|
return await self._handle_settings_mcp_presets(request, mcp_action)
|
||||||
@ -347,3 +350,15 @@ class WebUISettingsRouter:
|
|||||||
if action is None:
|
if action is None:
|
||||||
return self._json_response(payload)
|
return self._json_response(payload)
|
||||||
return self._json_response(self._with_restart_state(payload, section="runtime"))
|
return self._json_response(self._with_restart_state(payload, section="runtime"))
|
||||||
|
|
||||||
|
async def _handle_settings_version_check(self, request: WsRequest) -> Response:
|
||||||
|
if not self._authorized(request):
|
||||||
|
return self._unauthorized()
|
||||||
|
try:
|
||||||
|
update_info = await asyncio.to_thread(check_for_update)
|
||||||
|
except Exception:
|
||||||
|
self.logger.exception("version check failed")
|
||||||
|
return self._error_response(500, "version check failed")
|
||||||
|
return self._json_response({
|
||||||
|
"updateAvailable": update_info,
|
||||||
|
})
|
||||||
|
|||||||
51
nanobot/webui/version_check.py
Normal file
51
nanobot/webui/version_check.py
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
"""On-demand version checker for nanobot-ai releases.
|
||||||
|
|
||||||
|
Checks PyPI for newer versions when explicitly requested (no background polling).
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import logging
|
||||||
|
import time
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
import httpx
|
||||||
|
|
||||||
|
from nanobot import __version__
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
_PYPI_URL = "https://pypi.org/pypi/nanobot-ai/json"
|
||||||
|
_CACHE_TTL_S = 300 # 5 minutes cache to avoid hammering PyPI
|
||||||
|
|
||||||
|
_cache: tuple[float, str | None] = (0.0, None)
|
||||||
|
|
||||||
|
|
||||||
|
def check_for_update() -> dict[str, Any] | None:
|
||||||
|
"""Check PyPI for a newer version. Returns update info dict or None if up-to-date.
|
||||||
|
|
||||||
|
Uses a short cache to avoid repeated requests within the TTL window.
|
||||||
|
This is a blocking call — invoke from a thread or background task.
|
||||||
|
"""
|
||||||
|
global _cache
|
||||||
|
now = time.monotonic()
|
||||||
|
cached_at, cached_val = _cache
|
||||||
|
if now - cached_at < _CACHE_TTL_S and cached_val is not None:
|
||||||
|
latest = cached_val
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
resp = httpx.get(_PYPI_URL, timeout=5.0, follow_redirects=True)
|
||||||
|
resp.raise_for_status()
|
||||||
|
latest = resp.json().get("info", {}).get("version")
|
||||||
|
except Exception:
|
||||||
|
logger.debug("PyPI version check failed", exc_info=True)
|
||||||
|
return None
|
||||||
|
_cache = (now, latest)
|
||||||
|
|
||||||
|
if not latest or latest == __version__:
|
||||||
|
return None
|
||||||
|
return {
|
||||||
|
"currentVersion": __version__,
|
||||||
|
"latestVersion": latest,
|
||||||
|
"pypiUrl": "https://pypi.org/project/nanobot-ai/",
|
||||||
|
}
|
||||||
@ -10,6 +10,7 @@ import {
|
|||||||
} from "react";
|
} from "react";
|
||||||
import {
|
import {
|
||||||
Activity,
|
Activity,
|
||||||
|
ArrowUpCircle,
|
||||||
Bot,
|
Bot,
|
||||||
Brain,
|
Brain,
|
||||||
Check,
|
Check,
|
||||||
@ -22,6 +23,7 @@ import {
|
|||||||
Database,
|
Database,
|
||||||
Eye,
|
Eye,
|
||||||
EyeOff,
|
EyeOff,
|
||||||
|
ExternalLink,
|
||||||
Gem,
|
Gem,
|
||||||
Globe2,
|
Globe2,
|
||||||
Grid3X3,
|
Grid3X3,
|
||||||
@ -75,6 +77,7 @@ import {
|
|||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Textarea } from "@/components/ui/textarea";
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
import {
|
import {
|
||||||
|
checkVersion,
|
||||||
createModelConfiguration,
|
createModelConfiguration,
|
||||||
fetchSettings,
|
fetchSettings,
|
||||||
fetchSettingsUsage,
|
fetchSettingsUsage,
|
||||||
@ -1852,6 +1855,104 @@ function OverviewSettings({
|
|||||||
/>
|
/>
|
||||||
</SettingsGroup>
|
</SettingsGroup>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<SettingsSectionTitle>{tx("settings.sections.about", "About")}</SettingsSectionTitle>
|
||||||
|
<SettingsGroup>
|
||||||
|
<VersionCheckRow currentVersion={settings.version?.current} />
|
||||||
|
</SettingsGroup>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function VersionCheckRow({ currentVersion }: { currentVersion?: string }) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const tx = (key: string, fallback: string) => t(key, { defaultValue: fallback });
|
||||||
|
const { token } = useClient();
|
||||||
|
const [checking, setChecking] = useState(false);
|
||||||
|
const [result, setResult] = useState<
|
||||||
|
| { type: "up-to-date" }
|
||||||
|
| { type: "update"; latestVersion: string; pypiUrl?: string }
|
||||||
|
| { type: "error"; message: string }
|
||||||
|
| null
|
||||||
|
>(null);
|
||||||
|
|
||||||
|
const handleCheck = async () => {
|
||||||
|
setChecking(true);
|
||||||
|
setResult(null);
|
||||||
|
try {
|
||||||
|
const res = await checkVersion(token);
|
||||||
|
if (res.updateAvailable) {
|
||||||
|
setResult({
|
||||||
|
type: "update",
|
||||||
|
latestVersion: res.updateAvailable.latestVersion,
|
||||||
|
pypiUrl: res.updateAvailable.pypiUrl,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
setResult({ type: "up-to-date" });
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
setResult({ type: "error", message: (err as Error).message });
|
||||||
|
} finally {
|
||||||
|
setChecking(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex min-h-[62px] flex-col gap-3 px-4 py-3.5 sm:flex-row sm:items-center sm:justify-between sm:px-5">
|
||||||
|
<div className="min-w-0">
|
||||||
|
<div className="text-[14px] font-medium leading-5 text-foreground">
|
||||||
|
{tx("settings.about.version", "Version")}
|
||||||
|
</div>
|
||||||
|
<div className="mt-0.5 text-[12px] leading-5 text-muted-foreground">
|
||||||
|
{currentVersion ? `v${currentVersion}` : "nanobot"}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex shrink-0 flex-col items-end gap-2">
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => void handleCheck()}
|
||||||
|
disabled={checking}
|
||||||
|
className="rounded-full"
|
||||||
|
>
|
||||||
|
{checking ? (
|
||||||
|
<Loader2 className="mr-1.5 h-3.5 w-3.5 animate-spin" aria-hidden />
|
||||||
|
) : (
|
||||||
|
<ArrowUpCircle className="mr-1.5 h-3.5 w-3.5" aria-hidden />
|
||||||
|
)}
|
||||||
|
{checking
|
||||||
|
? tx("settings.about.checking", "Checking...")
|
||||||
|
: tx("settings.about.checkForUpdates", "Check for updates")}
|
||||||
|
</Button>
|
||||||
|
{result?.type === "up-to-date" ? (
|
||||||
|
<span className="inline-flex items-center gap-1.5 text-[12px] text-emerald-600 dark:text-emerald-300">
|
||||||
|
<Check className="h-3 w-3" aria-hidden />
|
||||||
|
{tx("settings.about.upToDate", "You're up to date")}
|
||||||
|
</span>
|
||||||
|
) : null}
|
||||||
|
{result?.type === "update" ? (
|
||||||
|
<span className="inline-flex items-center gap-1.5 text-[12px] text-blue-600 dark:text-blue-300">
|
||||||
|
<ArrowUpCircle className="h-3 w-3" aria-hidden />
|
||||||
|
{tx("settings.about.updateAvailable", "Update available")}{result.latestVersion && ` v${result.latestVersion}`}
|
||||||
|
{result.pypiUrl ? (
|
||||||
|
<a
|
||||||
|
href={result.pypiUrl}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="inline-flex items-center gap-0.5 underline-offset-2 hover:underline"
|
||||||
|
>
|
||||||
|
PyPI
|
||||||
|
<ExternalLink className="h-2.5 w-2.5" aria-hidden />
|
||||||
|
</a>
|
||||||
|
) : null}
|
||||||
|
</span>
|
||||||
|
) : null}
|
||||||
|
{result?.type === "error" ? (
|
||||||
|
<span className="text-[12px] text-destructive">{result.message}</span>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -229,6 +229,26 @@ export async function fetchSettingsUsage(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface VersionCheckResult {
|
||||||
|
updateAvailable: {
|
||||||
|
currentVersion: string;
|
||||||
|
latestVersion: string;
|
||||||
|
pypiUrl?: string;
|
||||||
|
} | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function checkVersion(
|
||||||
|
token: string,
|
||||||
|
base: string = "",
|
||||||
|
): Promise<VersionCheckResult> {
|
||||||
|
return request<VersionCheckResult>(
|
||||||
|
`${base}/api/settings/version-check`,
|
||||||
|
token,
|
||||||
|
undefined,
|
||||||
|
10_000,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export async function fetchWorkspaces(
|
export async function fetchWorkspaces(
|
||||||
token: string,
|
token: string,
|
||||||
base: string = "",
|
base: string = "",
|
||||||
|
|||||||
@ -485,6 +485,9 @@ export interface SettingsPayload {
|
|||||||
};
|
};
|
||||||
requires_restart: boolean;
|
requires_restart: boolean;
|
||||||
restart_required_sections?: Array<"runtime" | "browser" | "image">;
|
restart_required_sections?: Array<"runtime" | "browser" | "image">;
|
||||||
|
version?: {
|
||||||
|
current: string;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AppPackageRef {
|
export interface AppPackageRef {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user