From 2d30f27e146a557f864ab00ad52b10a38289e1f1 Mon Sep 17 00:00:00 2001 From: YuanShiJiLoong <2450829984@qq.com> Date: Wed, 25 Feb 2026 00:31:25 +0900 Subject: [PATCH] feat: add in-app update notification with GitHub/Gitee fallback --- README.md | 20 +++++++ README.zh.md | 22 ++++++++ app/api/check-update/route.js | 98 ++++++++++++++++++++++++++++++++++ app/components/UpdateBanner.js | 77 ++++++++++++++++++++++++++ app/globals.css | 76 ++++++++++++++++++++++++++ app/locales/en.json | 6 +++ app/locales/ru.json | 6 +++ app/locales/zh.json | 6 +++ app/page.js | 4 ++ 9 files changed, 315 insertions(+) create mode 100644 app/api/check-update/route.js create mode 100644 app/components/UpdateBanner.js diff --git a/README.md b/README.md index 95ef1e6..96e5706 100644 --- a/README.md +++ b/README.md @@ -123,6 +123,26 @@ npm start --- +## 🔄 Updating + +### Desktop Client Users + +Download the latest installer from the [Releases](https://github.com/YuanShiJiLoong/author/releases/latest) page and install it over your current version. Your data is stored in the browser/Electron profile and will not be lost. + +### Self-Deployed Users (Source) + +```bash +git pull origin main +npm install +npm run dev +``` + +### Vercel Users + +If you deployed via Vercel fork, just sync your fork with upstream on GitHub — Vercel will automatically redeploy. + +--- + ## ⚙️ AI Configuration Author supports multiple AI providers. Configure via **environment variables** or **in-app settings**: diff --git a/README.zh.md b/README.zh.md index df5c342..2ec4936 100644 --- a/README.zh.md +++ b/README.zh.md @@ -124,6 +124,28 @@ npm start --- +## 🔄 更新 + +### 桌面客户端用户 + +前往 [Releases](https://github.com/YuanShiJiLoong/author/releases/latest) 页面下载最新版本安装包,覆盖安装即可。你的数据存储在浏览器/Electron 用户配置中,不会丢失。 + +> 💬 无法访问 GitHub?[加入 QQ 交流群:1087016949](https://qm.qq.com/q/wjRDkotw0E),群文件中下载最新版本。 + +### 源码部署用户 + +```bash +git pull origin main +npm install +npm run dev +``` + +### Vercel 部署用户 + +如果你通过 Fork 部署到 Vercel,只需在 GitHub 上将你的 Fork 与上游同步(Sync fork),Vercel 会自动重新部署。 + +--- + ## ⚙️ AI 配置 Author 支持多种 AI 供应商,你可以通过 **环境变量** 或 **应用内设置** 来配置: diff --git a/app/api/check-update/route.js b/app/api/check-update/route.js new file mode 100644 index 0000000..94f4bf9 --- /dev/null +++ b/app/api/check-update/route.js @@ -0,0 +1,98 @@ +import { NextResponse } from 'next/server'; +import { readFileSync } from 'fs'; +import { join } from 'path'; + +/** + * 比较两个语义化版本号 + * 如果 a > b 返回 1,a < b 返回 -1,相等返回 0 + */ +function compareSemver(a, b) { + const pa = a.replace(/^v/, '').split('.').map(Number); + const pb = b.replace(/^v/, '').split('.').map(Number); + for (let i = 0; i < 3; i++) { + const na = pa[i] || 0; + const nb = pb[i] || 0; + if (na > nb) return 1; + if (na < nb) return -1; + } + return 0; +} + +/** + * 从 GitHub 或 Gitee 获取最新 Release 版本号 + * 优先 GitHub,超时/失败后回退到 Gitee + */ +async function fetchLatestVersion() { + const sources = [ + { + name: 'GitHub', + url: 'https://api.github.com/repos/YuanShiJiLoong/author/releases/latest', + headers: { 'Accept': 'application/vnd.github.v3+json', 'User-Agent': 'Author-App' }, + extract: (data) => data.tag_name, + }, + { + name: 'Gitee', + url: 'https://gitee.com/api/v5/repos/yuanshijilong/author/releases/latest', + headers: {}, + extract: (data) => data.tag_name, + }, + ]; + + for (const source of sources) { + try { + const res = await fetch(source.url, { + headers: source.headers, + signal: AbortSignal.timeout(5000), // 5 秒超时 + }); + if (!res.ok) continue; + const data = await res.json(); + const tag = source.extract(data); + if (tag) return tag; + } catch { + // 当前源失败,尝试下一个 + continue; + } + } + + return null; // 所有源都失败 +} + +export async function GET() { + try { + // 读取当前版本号 + let currentVersion; + try { + const pkgPath = join(process.cwd(), 'package.json'); + const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8')); + currentVersion = pkg.version; + } catch { + return NextResponse.json( + { error: '无法读取当前版本号' }, + { status: 500 } + ); + } + + // 获取最新版本 + const latestTag = await fetchLatestVersion(); + + if (!latestTag) { + return NextResponse.json( + { current: currentVersion, latest: null, hasUpdate: false }, + { headers: { 'Cache-Control': 'public, max-age=600' } } // 失败时缓存 10 分钟 + ); + } + + const latestVersion = latestTag.replace(/^v/, ''); + const hasUpdate = compareSemver(latestVersion, currentVersion) > 0; + + return NextResponse.json( + { current: currentVersion, latest: latestVersion, hasUpdate }, + { headers: { 'Cache-Control': 'public, max-age=3600' } } // 成功缓存 1 小时 + ); + } catch (err) { + return NextResponse.json( + { error: '检查更新失败', details: err.message }, + { status: 500 } + ); + } +} diff --git a/app/components/UpdateBanner.js b/app/components/UpdateBanner.js new file mode 100644 index 0000000..255db99 --- /dev/null +++ b/app/components/UpdateBanner.js @@ -0,0 +1,77 @@ +'use client'; + +import { useState, useEffect } from 'react'; +import { useI18n } from '../lib/useI18n'; + +export default function UpdateBanner() { + const { t } = useI18n(); + const [updateInfo, setUpdateInfo] = useState(null); + const [dismissed, setDismissed] = useState(false); + + useEffect(() => { + const checkUpdate = async () => { + try { + const res = await fetch('/api/check-update', { cache: 'no-store' }); + if (!res.ok) return; + const data = await res.json(); + + if (data.hasUpdate && data.latest) { + // 检查用户是否已经忽略过该版本 + const dismissedVersion = localStorage.getItem('author-update-dismissed'); + if (dismissedVersion === data.latest) return; + + setUpdateInfo(data); + } + } catch { + // 网络失败静默跳过 + } + }; + + // 延迟 3 秒检查,避免影响首屏加载 + const timer = setTimeout(checkUpdate, 3000); + return () => clearTimeout(timer); + }, []); + + const handleDismiss = () => { + setDismissed(true); + if (updateInfo?.latest) { + localStorage.setItem('author-update-dismissed', updateInfo.latest); + } + }; + + if (!updateInfo || dismissed) return null; + + const versionText = t('update.newVersion').replace('{version}', `v${updateInfo.latest}`); + + return ( +