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:
@@ -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)]"
|
||||
|
||||
@@ -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)",
|
||||
|
||||
@@ -67,6 +67,8 @@ export const zh = {
|
||||
*/
|
||||
checkNow: "检查更新",
|
||||
checking: "检查中…",
|
||||
/** 手动检查发现新版本时的成功提示;下方同一行即变为更新入口。 */
|
||||
foundNew: (v: string) => `发现新版本 v${v},点击下方更新入口即可安装`,
|
||||
upToDate: "已是最新版本",
|
||||
checkFailed: "检查更新失败,请稍后重试",
|
||||
checkDisabled: "更新检查已关闭(PENGUIN_UPDATE_CHECK=off)",
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user