+
{/* 背景层 */}
@@ -85,7 +104,19 @@ export function VersionCard({
/>
v{VERSION}
- {/* OOBE 模式:仅显示查看亮点按钮 */}
+ {/* 构建来源(CI 构建时写入;本地开发不显示) */}
+ {(BUILD_COMMIT || BUILD_ID !== "local") && (
+
+ {BUILD_COMMIT && `commit ${BUILD_COMMIT}`}
+ {BUILD_COMMIT && BUILD_ID !== "local" && " · "}
+ {BUILD_ID !== "local" && `#${BUILD_ID}`}
+
+ )}
+
+ {/* 更新日志页内不显示任何按钮(更新操作由页面底部遮罩负责) */}
+ {!isOnUpdatePage && (
+ <>
+ {/* OOBE 模式:仅显示查看亮点按钮 */}
{oobe ? (
查看亮点
@@ -94,7 +125,7 @@ export function VersionCard({
{effectiveState === "latest" && (
<>
-
+
{checking ? "检查中..." : "检查更新"}
查看亮点
@@ -118,6 +149,8 @@ export function VersionCard({
)}
)}
+ >
+ )}
);
diff --git a/src/lib/buildInfo.ts b/src/lib/buildInfo.ts
new file mode 100644
index 0000000..076dda7
--- /dev/null
+++ b/src/lib/buildInfo.ts
@@ -0,0 +1,3 @@
+// 构建元数据:由 scripts/gen-build-info.js 自动生成(CI 覆盖;本地开发为默认值)
+export const BUILD_COMMIT: string = "";
+export const BUILD_ID: string = "local";
diff --git a/src/pages/setting/general/about.tsx b/src/pages/setting/general/about.tsx
index 5f9bb23..25c65a2 100644
--- a/src/pages/setting/general/about.tsx
+++ b/src/pages/setting/general/about.tsx
@@ -1,6 +1,7 @@
import { useState, useEffect } from "react";
import { VersionCard } from "@/components/VersionCard";
import { BUILD_MODE } from "@/lib/mode";
+import { BUILD_COMMIT, BUILD_ID } from "@/lib/buildInfo";
import { ExternalLink, GitFork, RotateCcw } from "lucide-react";
import { Link } from "@heroui/react";
import { SettingCard, SettingRow, PageHeader, SectionTitle } from "@/components/setting";
@@ -70,6 +71,14 @@ export function AboutSetting() {
{modeLabels[BUILD_MODE] ?? BUILD_MODE}
+
+
+
+ {BUILD_COMMIT ? `commit ${BUILD_COMMIT}` : "本地构建"}
+ {BUILD_ID !== "local" ? ` · #${BUILD_ID}` : ""}
+
+
+
Node.js
diff --git a/src/pages/update/index.tsx b/src/pages/update/index.tsx
new file mode 100644
index 0000000..449287f
--- /dev/null
+++ b/src/pages/update/index.tsx
@@ -0,0 +1,300 @@
+import { useCallback, useEffect, useState } from "react";
+import { VersionCard } from "@/components/VersionCard";
+import { SectionTitle, SettingCard } from "@/components/setting";
+import { Progress } from "@/components/ui/progress";
+import { BUILD_MODE } from "@/lib/mode";
+import {
+ cancelUpdate,
+ checkForUpdates,
+ downloadUpdate,
+ getReleaseNotes,
+ getUpdateState,
+ onUpdateStatus,
+ pauseUpdate,
+ quitAndInstall,
+ resumeUpdate,
+ type ReleaseNotesResult,
+ type UpdateStatusPayload,
+} from "@/api/update";
+import { ExternalLink, Loader2, RefreshCw } from "lucide-react";
+import { Button, Link } from "@heroui/react";
+import { toast } from "sonner";
+import ReactMarkdown from "react-markdown";
+import remarkGfm from "remark-gfm";
+import rehypeRaw from "rehype-raw";
+
+const GITHUB_RELEASES = "https://github.com/dream-pep/koring-launcher/releases";
+
+/** 主按钮颜色随构建模式:dev 橙 / beta 绿 / run 蓝(与 VersionCard 一致) */
+const MODE_BUTTON_COLORS: Record = {
+ dev: { bg: "#F59E0B", hover: "#D97706" },
+ beta: { bg: "#10B981", hover: "#059669" },
+ run: { bg: "#3b82f6", hover: "#2563eb" },
+};
+
+const modeColors = MODE_BUTTON_COLORS[BUILD_MODE] ?? MODE_BUTTON_COLORS.run;
+
+/** 主题 accent 为灰色系,内联覆盖按钮 CSS 变量 */
+const BUTTON_STYLE = {
+ "--button-bg": modeColors.bg,
+ "--button-bg-hover": modeColors.hover,
+ "--button-bg-pressed": modeColors.hover,
+ "--button-fg": "#ffffff",
+} as React.CSSProperties;
+
+function formatMB(bytes?: number): string {
+ if (!bytes || bytes <= 0) return "0 MB";
+ return `${(bytes / 1024 / 1024).toFixed(1)} MB`;
+}
+
+/**
+ * 更新日志(独立页面,不走设置 layout):
+ * - 顶栏由路由切换为「返回 + 更新日志」(routeStore 的 titleInBar)
+ * - 顶部 VersionCard(此页面不显示按钮,更新操作由底部遮罩负责)
+ * - 发布说明:默认当前版本;检测到可用更新后自动切到最新版本
+ * - 底部遮罩:检查更新 → 下载(进度条 + 暂停/继续/取消)→ 安装更新
+ * - 下载/安装进度由主进程写入 Koring.yml(update 段)
+ */
+export function UpdatePage() {
+ // 发布说明:notesTag 为空 = 当前版本;有可用更新后切到对应 tag
+ const [notes, setNotes] = useState(null);
+ const [notesLoading, setNotesLoading] = useState(true);
+ const [notesError, setNotesError] = useState(null);
+ const [notesTag, setNotesTag] = useState(undefined);
+
+ // 更新状态:事件驱动,主进程为唯一真相源
+ const [status, setStatus] = useState(null);
+
+ useEffect(() => {
+ const unsub = onUpdateStatus(setStatus);
+ getUpdateState().then(setStatus).catch(() => {});
+ return unsub;
+ }, []);
+
+ const loadNotes = useCallback(async (tag?: string) => {
+ setNotesLoading(true);
+ setNotesError(null);
+ try {
+ setNotes(await getReleaseNotes(tag));
+ } catch (e) {
+ setNotesError(e instanceof Error ? e.message : String(e));
+ } finally {
+ setNotesLoading(false);
+ }
+ }, []);
+
+ useEffect(() => {
+ loadNotes(notesTag);
+ }, [notesTag, loadNotes]);
+
+ // 检测到可用更新 → 发布说明切到最新版本(退出重进自动回到当前版本)
+ useEffect(() => {
+ if (status?.state === "available" && status.version && notesTag !== `v${status.version}`) {
+ setNotesTag(`v${status.version}`);
+ }
+ }, [status, notesTag]);
+
+ const st = status?.state ?? "idle";
+ const pct = status?.percent ?? 0;
+ const isDownloading = st === "downloading";
+ const isPaused = st === "paused";
+
+ const handleCheck = async () => {
+ try {
+ await checkForUpdates(true);
+ } catch (e) {
+ toast.error(e instanceof Error ? e.message : String(e));
+ }
+ };
+
+ const handleDownload = async () => {
+ try {
+ await downloadUpdate();
+ } catch (e) {
+ toast.error(e instanceof Error ? e.message : String(e));
+ }
+ };
+
+ const handlePause = async () => {
+ try {
+ await pauseUpdate();
+ } catch (e) {
+ toast.error(e instanceof Error ? e.message : String(e));
+ }
+ };
+
+ const handleResume = async () => {
+ try {
+ await resumeUpdate();
+ } catch (e) {
+ toast.error(e instanceof Error ? e.message : String(e));
+ }
+ };
+
+ const handleCancel = async () => {
+ try {
+ await cancelUpdate();
+ } catch (e) {
+ toast.error(e instanceof Error ? e.message : String(e));
+ }
+ };
+
+ const handleInstall = async () => {
+ try {
+ await quitAndInstall();
+ } catch (e) {
+ toast.error(e instanceof Error ? e.message : String(e));
+ }
+ };
+
+ const openGithub = () => {
+ window.electronAPI?.openExternal(GITHUB_RELEASES);
+ };
+
+ const primaryAction =
+ st === "available" ? handleDownload : st === "downloaded" ? handleInstall : handleCheck;
+
+ const statusText =
+ st === "checking"
+ ? "正在检查更新..."
+ : st === "available"
+ ? `发现新版本 v${status?.version}`
+ : st === "downloading"
+ ? `正在下载 ${pct.toFixed(0)}% · ${formatMB(status?.transferred)} / ${formatMB(status?.total)}${status?.bytesPerSecond ? ` · ${formatMB(status.bytesPerSecond)}/s` : ""}`
+ : st === "paused"
+ ? `下载已暂停(${pct.toFixed(0)}%)`
+ : st === "downloaded"
+ ? "更新已下载完成"
+ : st === "installing"
+ ? "正在安装更新,应用即将重启..."
+ : st === "error"
+ ? `更新失败:${status?.error ?? "未知错误"}`
+ : "";
+
+ return (
+
+
+
+
+
+
更新内容
+
+ {notesLoading ? (
+
+
+
+ 正在获取发布说明...
+
+
+ ) : notesError ? (
+
+
+
获取发布说明失败:{notesError}
+
+
+
+ ) : notes ? (
+
+
+
+ 版本 v{notes.version}
+ {notesTag ? "(最新)" : "(当前)"}
+
+ · 来源:{notes.source === "github" ? "GitHub" : `加速源 ${notes.source}`}
+
+
+
+
+
+
+
+ {notes.notes}
+
+
+
+ ) : (
+
+
+
+ 未获取到发布说明(GitHub 与加速源均不可用,或该版本尚未发布)
+
+
+ 打开 GitHub Releases
+
+
+
+
+ )}
+
+
+
+ {/* 底部遮罩:fixed 吸附底部,样式与顶栏一致;驱动整个更新流程 */}
+
+
+ {statusText && (
+
{statusText}
+ )}
+
+ {isDownloading || isPaused ? (
+
+
+
+
+
+
+
+ ) : (
+
+ )}
+
+
+
+ );
+}
diff --git a/src/stores/configStore.ts b/src/stores/configStore.ts
index 0250a5f..b54f8d7 100644
--- a/src/stores/configStore.ts
+++ b/src/stores/configStore.ts
@@ -59,6 +59,7 @@ const DEFAULT_CONFIG: AppConfig = {
download: { fileSource: "mirror", versionSource: "mirror", threads: 16, speedLimit: 0 },
network: { securityId: { enabled: false, authUrl: "" } },
ui: { showInstanceTitle: true, showTaskButton: true },
+ update: { state: "idle", version: "", percent: 0, transferred: 0, total: 0, source: "github", error: "" },
instances: [],
};
diff --git a/src/stores/routeStore.ts b/src/stores/routeStore.ts
index c475789..13eaa50 100644
--- a/src/stores/routeStore.ts
+++ b/src/stores/routeStore.ts
@@ -9,6 +9,7 @@ export type RouteKey =
| "setting/login"
| "gallery"
| "task-queue"
+ | "update"
| "oobe"
| "oobe/language"
| "oobe/agreement"
@@ -51,6 +52,7 @@ export const allRoutes: RouteItem[] = [
...routes,
{ key: "setting/login", label: "登录", path: "/setting/login", hidden: true, backable: true },
{ key: "task-queue", label: "任务队列", path: "/task-queue", hidden: true },
+ { key: "update", label: "更新日志", path: "/update", hidden: true, backable: true },
{ key: "oobe", label: "OOBE", path: "/oobe", hidden: true },
{ key: "oobe/language", label: "语言设置", path: "/oobe/language", hidden: true },
{ key: "oobe/agreement", label: "同意协议", path: "/oobe/agreement", hidden: true },
diff --git a/src/stores/updateStore.ts b/src/stores/updateStore.ts
index f71f650..0cae6d1 100644
--- a/src/stores/updateStore.ts
+++ b/src/stores/updateStore.ts
@@ -1,51 +1,157 @@
import { create } from "zustand";
import {
checkForUpdates,
- downloadAndInstall,
- type DownloadProgress,
+ downloadUpdate,
+ getUpdateState,
+ onUpdateStatus,
+ quitAndInstall,
+ resumeUpdate,
+ type UpdateStatusPayload,
} from "../api/update";
-interface UpdateState {
+/**
+ * 更新 store(与主进程 electron/updater.ts 的状态机联动):
+ * - 模块加载即订阅 update:status 事件 + 拉取一次状态快照
+ * - check() / install() 触发主进程操作,状态由事件驱动更新
+ */
+
+interface UpdateProgress {
+ percent: number;
+ transferred: number;
+ total: number;
+ bytesPerSecond: number;
+}
+
+interface UpdateStoreState {
checking: boolean;
downloading: boolean;
installed: boolean;
- progress: DownloadProgress | null;
+ progress: UpdateProgress | null;
update: { version: string; releaseNotes?: string } | null;
+ currentVersion: string;
+ source: string;
error: string | null;
check: () => Promise;
install: () => Promise;
reset: () => void;
}
-export const useUpdateStore = create((set, get) => ({
- checking: false,
- downloading: false,
- installed: false,
- progress: null,
- update: null,
- error: null,
+type Setter = (partial: Partial) => void;
- check: async () => {
- set({ checking: true, error: null });
- try {
- const update = await checkForUpdates();
- set({ update, checking: false });
- } catch (e: any) {
- set({ error: e.message ?? String(e), checking: false });
- }
- },
+function applyStatus(set: Setter, status: UpdateStatusPayload): void {
+ const next: Partial = {
+ currentVersion: status.currentVersion ?? "",
+ source: status.source ?? "github",
+ };
- install: async () => {
- set({ downloading: true, error: null, progress: null });
- try {
- await downloadAndInstall((progress) => {
- set({ progress });
- });
- set({ downloading: false, installed: true });
- } catch (e: any) {
- set({ error: e.message ?? String(e), downloading: false });
- }
- },
+ switch (status.state) {
+ case "checking":
+ next.checking = true;
+ next.error = null;
+ break;
+ case "available":
+ next.checking = false;
+ next.update = { version: status.version ?? "", releaseNotes: undefined };
+ next.error = null;
+ break;
+ case "not-available":
+ next.checking = false;
+ next.update = null;
+ next.error = null;
+ break;
+ case "downloading":
+ next.downloading = true;
+ next.progress = {
+ percent: status.percent ?? 0,
+ transferred: status.transferred ?? 0,
+ total: status.total ?? 0,
+ bytesPerSecond: status.bytesPerSecond ?? 0,
+ };
+ next.error = null;
+ break;
+ case "paused":
+ // 下载已暂停:保持 downloading 标记(VersionCard 按钮点击即继续)
+ next.downloading = true;
+ next.progress = {
+ percent: status.percent ?? 0,
+ transferred: status.transferred ?? 0,
+ total: status.total ?? 0,
+ bytesPerSecond: 0,
+ };
+ next.error = null;
+ break;
+ case "downloaded":
+ case "installing":
+ next.downloading = false;
+ next.installed = true;
+ next.update = { version: status.version ?? "", releaseNotes: undefined };
+ next.error = null;
+ break;
+ case "error":
+ next.checking = false;
+ next.downloading = false;
+ next.error = status.error ?? "更新失败";
+ break;
+ default:
+ break;
+ }
- reset: () => set({ update: null, error: null, progress: null, installed: false }),
-}));
+ set(next);
+}
+
+export const useUpdateStore = create((set, get) => {
+ // 模块加载即订阅(VersionCard 等组件引入本 store 后生效)
+ onUpdateStatus((status) => applyStatus(set, status));
+ getUpdateState()
+ .then((status) => applyStatus(set, status))
+ .catch((e) => console.error("[update] 获取状态失败:", e));
+
+ return {
+ checking: false,
+ downloading: false,
+ installed: false,
+ progress: null,
+ update: null,
+ currentVersion: "",
+ source: "github",
+ error: null,
+
+ check: async () => {
+ set({ checking: true, error: null });
+ try {
+ await checkForUpdates(true);
+ } catch (e) {
+ set({ error: e instanceof Error ? e.message : String(e), checking: false });
+ }
+ },
+
+ install: async () => {
+ set({ downloading: true, error: null, progress: null });
+ try {
+ // 智能分派:暂停→继续;已下载→安装;否则→开始下载
+ const state = get().installed ? "downloaded" : get().update ? "available" : "idle";
+ if (state === "downloaded") {
+ await quitAndInstall();
+ return;
+ }
+ if (get().downloading && get().progress) {
+ await resumeUpdate();
+ return;
+ }
+ await downloadUpdate();
+ } catch (e) {
+ set({ error: e instanceof Error ? e.message : String(e), downloading: false });
+ }
+ },
+
+ reset: () =>
+ set({
+ update: null,
+ error: null,
+ progress: null,
+ installed: false,
+ checking: false,
+ downloading: false,
+ }),
+ };
+});
diff --git a/src/types/electron.d.ts b/src/types/electron.d.ts
index d76a20a..1f0693a 100644
--- a/src/types/electron.d.ts
+++ b/src/types/electron.d.ts
@@ -23,6 +23,17 @@ interface ElectronAPI {
// Config reset
resetConfig: () => Promise;
+
+ // Auto-update
+ checkForUpdates: (manual?: boolean) => Promise;
+ downloadUpdate: () => Promise;
+ pauseUpdate: () => Promise;
+ resumeUpdate: () => Promise;
+ cancelUpdate: () => Promise;
+ quitAndInstall: () => Promise;
+ getUpdateState: () => Promise;
+ getReleaseNotes: (tag?: string) => Promise;
+ onUpdateStatus: (callback: (data: unknown) => void) => () => void;
}
declare global {