feat(installer,web): one canonical bundle per target; robust in-place upgrades; update-check feedback (#142)

Co-authored-by: Yaowei Zheng <hiyouga@buaa.edu.cn>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Laodouuu
2026-08-03 12:19:59 +08:00
committed by GitHub
parent 740f101242
commit 84949882ab
24 changed files with 1081 additions and 465 deletions
+17 -11
View File
@@ -63,7 +63,7 @@ import { clearDraft, sessionDraftKey } from "../../features/chat/draft-cache";
import { CreateProjectDialog, ProjectSettingsDialog } from "./project-dialogs";
import { ChangePasswordDialog } from "../account/change-password-dialog";
import { UpdateDialog } from "../account/update-dialog";
import { forceUpdateCheck, useVersionInfo } from "../../lib/use-version-info";
import { forceUpdateCheck, updateCheckOutcome, useVersionInfo } from "../../lib/use-version-info";
function Icon({ d, size = 16 }: { d: string; size?: number }) {
return (
@@ -244,20 +244,20 @@ export function Sidebar({
/**
* Manual update check (owner request): forces a lookup past the server's TTL cache and
* pushes the result into the shared version-info store, so the reminder rows, badge,
* and dot appear immediately when a newer release is found — that visible change is
* the notification then. A toast fires only when nothing changes visibly (#54, one
* notification per action): up to date, checks disabled, or a failed lookup (the
* check is fail-soft — failure arrives as the `error` field, not an exception; the
* catch handles our own server being unreachable).
* and dot appear immediately when a newer release is found. Every outcome also toasts —
* up to date, found (naming the release; the row below turns into the update entry),
* checks disabled, and a failed lookup (the check is fail-soft — failure arrives as the
* `error` field, not an exception; the catch handles our own server being unreachable).
*/
const runUpdateCheck = async () => {
if (updateChecking) return;
setUpdateChecking(true);
try {
const res = await forceUpdateCheck();
if (res.disabled === true) toastInfo(S.update.checkDisabled);
else if (res.error !== undefined) toastError(S.update.checkFailed);
else if (!res.updateAvailable) toastSuccess(S.update.upToDate);
const outcome = updateCheckOutcome(await forceUpdateCheck());
if (outcome.kind === "disabled") toastInfo(S.update.checkDisabled);
else if (outcome.kind === "failed") toastError(S.update.checkFailed);
else if (outcome.kind === "found") toastSuccess(S.update.foundNew(outcome.latestVersion));
else toastSuccess(S.update.upToDate);
} catch (e) {
toastError(apiErrorText(e));
} finally {
@@ -1099,7 +1099,13 @@ export function Sidebar({
className={`${menuItemClass} flex items-center justify-between gap-2 disabled:cursor-default disabled:opacity-60`}
>
<span className="flex min-w-0 items-center gap-2">
{newVersion !== null && (
{updateChecking && (
<span
aria-hidden
className="inline-block h-3 w-3 shrink-0 animate-spin rounded-full border-[1.5px] border-current border-t-transparent opacity-70"
/>
)}
{!updateChecking && newVersion !== null && (
<span
aria-hidden
className="h-2 w-2 shrink-0 rounded-full bg-[var(--accent-bg)]"
+2
View File
@@ -67,6 +67,8 @@ export const en: Strings = {
*/
checkNow: "Check for updates",
checking: "Checking…",
/** Success toast when the manual check finds a newer release; the row below turns into the update entry. */
foundNew: (v: string) => `New version v${v} found — use the update entry below to install`,
upToDate: "You're on the latest version",
checkFailed: "Update check failed — try again later",
checkDisabled: "Update checks are disabled (PENGUIN_UPDATE_CHECK=off)",
+2
View File
@@ -67,6 +67,8 @@ export const zh = {
*/
checkNow: "检查更新",
checking: "检查中…",
/** 手动检查发现新版本时的成功提示;下方同一行即变为更新入口。 */
foundNew: (v: string) => `发现新版本 v${v},点击下方更新入口即可安装`,
upToDate: "已是最新版本",
checkFailed: "检查更新失败,请稍后重试",
checkDisabled: "更新检查已关闭(PENGUIN_UPDATE_CHECK=off)",
+22
View File
@@ -28,6 +28,28 @@ let updatePromise: Promise<UpdateCheckResponse> | null = null;
*/
const listeners = new Set<() => void>();
/** How one manual update check ended, for user feedback — exactly one notice per outcome. */
export type UpdateCheckOutcome =
| { kind: "disabled" }
| { kind: "failed" }
| { kind: "up-to-date" }
| { kind: "found"; latestVersion: string };
/**
* Classifies a manual check result. Order matters: `disabled` means no lookup ran, `error`
* means the lookup ran and failed (the response is fail-soft, not an exception), and only a
* result that names the newer release counts as `found` — updateAvailable without a version
* would leave the row and the toast with nothing to show.
*/
export function updateCheckOutcome(res: UpdateCheckResponse): UpdateCheckOutcome {
if (res.disabled === true) return { kind: "disabled" };
if (res.error !== undefined) return { kind: "failed" };
if (res.updateAvailable && res.latestVersion !== null) {
return { kind: "found", latestVersion: res.latestVersion };
}
return { kind: "up-to-date" };
}
export interface VersionInfo {
version: VersionResponse | null;
update: UpdateCheckResponse | null;