feat(console): 第5按钮·更新公告——dock最右侧新增铃铛按钮(反选切换),大磁片页:当前版本/检查更新/自动检查开关/新版本横幅+忽略此版本/公告正文(changelog.ts);更新检查核心抽到updateCheck.ts(键与设置面板复用)
This commit is contained in:
@@ -0,0 +1,28 @@
|
||||
/** 更新公告正文(v0.3.0 定稿,用户审过;呈现时按纯文本保留换行,不做 md 渲染) */
|
||||
export const CHANGELOG_VERSION = 'v0.3.0'
|
||||
export const CHANGELOG_V030 = `更新公告 · v0.3.0
|
||||
|
||||
🛠️ 一、控制室 UI 调整
|
||||
|
||||
【1.1 主题切换与界面优化】控制台支持深色 / 浅色两套完整主题,并在浅色模式下完成全套适配(纯色背景自动切换为浅色、网格线转为灰色、任务进行中的卡片光效改用高饱和蓝色,确保白色背景下依然清晰醒目)。界面顶部四个零散按钮整合为底部中央的「玻璃操作台」,图标改为细线描边风格并支持悬停放大;主题按钮的图标会随当前选择自动切换(月亮 / 太阳 / 电脑)。
|
||||
|
||||
【1.2 瓷片形态切换】项目卡片支持「方块」与「圆圈」两种形状,一键切换;切换时卡片自动平滑过渡,圆形模式下内容自动调整为居中排版。
|
||||
|
||||
【1.3 背景系统扩至三种可选】——单色背景、柔光背景、自定义背景,每种都有独立面板:
|
||||
· 单色背景:提供色相、饱和度、明度调节滑杆,支持直接输入数值与一键恢复默认;默认颜色随当前主题自动匹配(深色=深蓝黑,浅色=米白)。
|
||||
· 柔光背景:同样可调节光晕的色相、饱和度与明度,让整体氛围色调随心而变。
|
||||
· 自定义背景:支持照片与视频统一入库管理——可上传多张媒体、缩略图带类型角标、任意一张可单独删除;每张媒体会独立记住你的调色设置,选中即自动恢复;上传的文件以原始画质存储(不再压缩降清晰度);视频作为背景时自动循环播放,并加入首尾帧渐变交接,消除循环时的顿挫感。另有:背景网格线开关与透明度滑杆、卡片贴片模糊滑杆;所有滑杆悬停时会显示各自的功能说明。
|
||||
|
||||
【1.4 每行项目数】自由调整控制台每行显示的瓷片数量(1–5 张),拖拽滑杆即时生效并配平滑动画。
|
||||
|
||||
【1.5 更新公告】控制台新增独立的更新入口,随时查看本版本的完整说明、一键检查更新、开启自动检查更新开关;发现新版本时可选择「忽略此版本」。
|
||||
|
||||
🚀 二、项目 UX 优化
|
||||
|
||||
【2.1 标签折叠】窗口标题栏新增折叠按键,可将窗格标题与标签栏一键收起/展开,界面结构更紧凑、内容区域更完整(收起后仍可从窗口右上角一键恢复)。
|
||||
|
||||
【2.2 新增标注功能】每个窗口折叠键旁新增「标注」按钮——点击后鼠标变为蓝色小泡光标;按住左键拖拽可画框选定区域(点按即为单点定位),随后写下你的要求(如「把这里放大一些」)并确认;系统将自动把「窗口编号 + 精确坐标 + 框选内容 + 你的要求」打包填入聊天输入框(不会自动发送)。接收的 AI 会按这份标注直接定位并作答——能查看画面时先核实再回答,无法查看时也只追问一条最关键的信息,不再泛泛猜测。
|
||||
|
||||
🎀 三、其他
|
||||
|
||||
【3.1 文字与细节打磨】全局字体与字号统一优化、下拉面板玻璃化与对齐、菜单点选后保持打开便于连续预览、按钮描边与悬停反馈等交互细节;同时修复了多项体验问题(照片上传清晰度、背景网格线在照片模式下不生效、浅色主题下工作状态光效不可见等)。`
|
||||
@@ -185,6 +185,16 @@ export const zh = {
|
||||
'annot.placeholder': '对这里提要求…(Enter 确认,Shift+Enter 换行,Esc 取消)',
|
||||
'annot.ok': '确认:注入对话框输入框(不发送)',
|
||||
'annot.cancel': '取消',
|
||||
'annot.updateTitle': '更新公告',
|
||||
'annot.curVer': '当前版本',
|
||||
'annot.checkNow': '检查更新',
|
||||
'annot.checking': '检查中…',
|
||||
'annot.autoCheck': '自动检查更新',
|
||||
'annot.newVer': '发现新版本',
|
||||
'annot.gotoRelease': '查看发布页',
|
||||
'annot.skipVer': '忽略此版本',
|
||||
'annot.latest': '已是最新版本',
|
||||
'annot.checkFail': '检查失败,请稍后再试',
|
||||
'pane.expand': '展开窗格',
|
||||
'pane.jobsTitle': '后台任务',
|
||||
'pane.subagents': '子代理',
|
||||
@@ -394,6 +404,16 @@ export const en = {
|
||||
'annot.placeholder': 'Describe what to change here… (Enter to confirm, Shift+Enter newline, Esc cancel)',
|
||||
'annot.ok': 'Confirm: put into chat input (not sent)',
|
||||
'annot.cancel': 'Cancel',
|
||||
'annot.updateTitle': 'Changelog',
|
||||
'annot.curVer': 'Current version',
|
||||
'annot.checkNow': 'Check now',
|
||||
'annot.checking': 'Checking…',
|
||||
'annot.autoCheck': 'Auto check',
|
||||
'annot.newVer': 'New version',
|
||||
'annot.gotoRelease': 'View release',
|
||||
'annot.skipVer': 'Ignore this version',
|
||||
'annot.latest': 'Up to date',
|
||||
'annot.checkFail': 'Check failed — try again later',
|
||||
'pane.expand': 'Expand pane',
|
||||
'pane.jobsTitle': 'Background jobs',
|
||||
'pane.subagents': 'Subagents',
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { Fragment, useCallback, useEffect, useLayoutEffect, useRef, useState, type CSSProperties } from 'react'
|
||||
import { parentPathOf } from './pathutil'
|
||||
import { CHANGELOG_V030 } from './changelog'
|
||||
import { LOCAL_VERSION, checkUpdate, getAutoCheck, readCache, setAutoCheck as storeAutoCheck, setSkipVersion, type UpdateInfo, type UpdateStatus } from './updateCheck'
|
||||
import { Terminal } from 'xterm'
|
||||
|
||||
import MarkdownIt from 'markdown-it'
|
||||
@@ -1415,6 +1417,11 @@ function ConsolePane() {
|
||||
const [photoGrid, setPhotoGridState] = useState<boolean>(() => splitEnv?.console?.getPhotoGrid?.() ?? true)
|
||||
const [gridOpacity, setGridOpacityState] = useState<number>(() => splitEnv?.console?.getGridOpacity?.() ?? 8)
|
||||
const [cardBlur, setCardBlurState] = useState<number>(() => splitEnv?.console?.getCardBlur?.() ?? 0)
|
||||
const [annOpen, setAnnOpen] = useState(false)
|
||||
const [updStatus, setUpdStatus] = useState<UpdateStatus>(() => readCache().status)
|
||||
const [updInfo, setUpdInfo] = useState<UpdateInfo | null>(() => readCache().info)
|
||||
const [autoCheckOn, setAutoCheckOn] = useState<boolean>(() => getAutoCheck())
|
||||
const updBusyRef = useRef(false)
|
||||
const photoUrlRef = useRef<string>('')
|
||||
const [openMenu, setOpenMenu] = useState<string | null>(null)
|
||||
const [bgEdit, setBgEdit] = useState<'plain' | 'glow' | 'photo' | null>(null)
|
||||
@@ -1514,6 +1521,27 @@ function ConsolePane() {
|
||||
setCardBlurState(v)
|
||||
splitEnv?.console?.setCardBlur?.(v)
|
||||
}
|
||||
const runUpdateCheck = async (force: boolean) => {
|
||||
if (updBusyRef.current) return
|
||||
updBusyRef.current = true
|
||||
setUpdStatus('checking')
|
||||
try {
|
||||
const r = await checkUpdate(force)
|
||||
setUpdStatus(r.status)
|
||||
setUpdInfo(r.info)
|
||||
} finally {
|
||||
updBusyRef.current = false
|
||||
}
|
||||
}
|
||||
const onSkipVersion = () => {
|
||||
if (updInfo) { setSkipVersion(updInfo.latest); setUpdInfo(null); setUpdStatus('uptodate') }
|
||||
}
|
||||
const onAutoCheckToggle = () => {
|
||||
const next = !autoCheckOn
|
||||
setAutoCheckOn(next)
|
||||
storeAutoCheck(next)
|
||||
if (next) void runUpdateCheck(false)
|
||||
}
|
||||
/** 在媒体库中选中某条 */
|
||||
const selectPhoto = (p: { id: string; kind: 'photo' | 'video'; url: string }) => {
|
||||
setPhotoIdLocal(p.id)
|
||||
@@ -1605,6 +1633,8 @@ function ConsolePane() {
|
||||
useEffect(() => {
|
||||
splitEnv?.console?.refreshPreviews?.()
|
||||
}, [])
|
||||
// 自动检查更新(节流一天;手动按钮始终 force)
|
||||
useEffect(() => { if (getAutoCheck()) void runUpdateCheck(false) }, [])
|
||||
// 背景照片库:IndexedDB 异步加载记录 → 对象 URL;无记录则空
|
||||
useEffect(() => {
|
||||
let alive = true
|
||||
@@ -1684,6 +1714,26 @@ function ConsolePane() {
|
||||
{bg === 'glow' && (<><i className="dsh-wt_blob dsh-wt_blob1" /><i className="dsh-wt_blob dsh-wt_blob2" /><i className="dsh-wt_blob dsh-wt_blob3" /><i className="dsh-wt_blob dsh-wt_blob4" /></>)}</span>
|
||||
{openMenu !== null && <div className="dsh-wt_dropMask" onClick={() => setOpenMenu(null)} />}
|
||||
<div className="dsh-wt_consoleScroll">
|
||||
{annOpen ? (
|
||||
<div className="dsh-wt_announce">
|
||||
<div className="dsh-wt_announceHead">
|
||||
<span className="dsh-wt_announceVer">{T('annot.curVer')} v{LOCAL_VERSION}</span>
|
||||
<button type="button" className="dsh-wt_announceBtn" disabled={updStatus === 'checking'} onClick={() => runUpdateCheck(true)}>{updStatus === 'checking' ? T('annot.checking') : T('annot.checkNow')}</button>
|
||||
<span className="dsh-wt_announceAuto">{T('annot.autoCheck')}</span>
|
||||
<button type="button" className={'dsh-wt_switch' + (autoCheckOn ? ' dsh-wt_switchOn' : '')} aria-pressed={autoCheckOn} aria-label={T('annot.autoCheck')} onClick={onAutoCheckToggle}><span className="dsh-wt_switchKnob" /></button>
|
||||
</div>
|
||||
{updInfo && (
|
||||
<div className="dsh-wt_announceUpdate">
|
||||
<span className="dsh-wt_announceNewVer">{T('annot.newVer')} v{updInfo.latest}</span>
|
||||
<button type="button" className="dsh-wt_gridToggle dsh-wt_gridToggleOn" onClick={() => { try { window.open(updInfo.url, '_blank') } catch {} }}>{T('annot.gotoRelease')}</button>
|
||||
<button type="button" className="dsh-wt_announceSkip" onClick={onSkipVersion}>{T('annot.skipVer')}</button>
|
||||
</div>
|
||||
)}
|
||||
{updStatus === 'failed' && <div className="dsh-wt_announceStatus">{T('annot.checkFail')}</div>}
|
||||
{updStatus === 'uptodate' && !updInfo && <div className="dsh-wt_announceStatus">{T('annot.latest')}</div>}
|
||||
<div className="dsh-wt_announceBody">{CHANGELOG_V030}</div>
|
||||
</div>
|
||||
) : (
|
||||
<div ref={gridRef} className="dsh-wt_consoleGrid" style={{ ['--wt-cols' as any]: cols }}>
|
||||
{cards.map((c) => (
|
||||
<div
|
||||
@@ -1729,7 +1779,8 @@ function ConsolePane() {
|
||||
<span className="dsh-wt_consoleAddLabel">{T('console.addProject')}</span>
|
||||
</div>
|
||||
{cards.length === 0 && <div className="dsh-wt_consoleEmpty">{T('console.empty')}</div>}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="dsh-wt_consoleDockWrap">
|
||||
<div className="dsh-wt_consoleDock">
|
||||
@@ -1737,6 +1788,7 @@ function ConsolePane() {
|
||||
<button type="button" className={'dsh-wt_dockBtn' + (openMenu === 'shape' ? ' dsh-wt_dockBtnOn' : '')} title={T('console.shapeLabel')} aria-label={T('console.shapeLabel')} onClick={() => setOpenMenu(openMenu === 'shape' ? null : 'shape')}><svg width="20" height="20" viewBox="0 0 16 16" aria-hidden><rect x="2.2" y="2.2" width="7" height="7" rx="1.5" fill="none" stroke="currentColor" strokeWidth="0.9" /><circle cx="10.6" cy="10.6" r="3.8" fill="none" stroke="currentColor" strokeWidth="0.9" /></svg></button>
|
||||
<button type="button" className={'dsh-wt_dockBtn' + (openMenu === 'bg' ? ' dsh-wt_dockBtnOn' : '')} title={T('console.bgLabel')} aria-label={T('console.bgLabel')} onClick={() => { setOpenMenu(openMenu === 'bg' ? null : 'bg'); setBgEdit(null) }}><svg width="18" height="18" viewBox="0 0 16 16" aria-hidden><rect x="2.2" y="3.2" width="11.6" height="9.6" rx="1.6" fill="none" stroke="currentColor" strokeWidth="0.9" /><circle cx="5.9" cy="6.7" r="1.05" fill="none" stroke="currentColor" strokeWidth="0.8" /><path d="M3.4 11.4l3-3 2.3 2.3 1.9-1.9 3 2.6" fill="none" stroke="currentColor" strokeWidth="0.9" /></svg></button>
|
||||
<button type="button" className={'dsh-wt_dockBtn' + (openMenu === 'cols' ? ' dsh-wt_dockBtnOn' : '')} title={T('console.colsLabel')} aria-label={T('console.colsLabel')} onClick={() => setOpenMenu(openMenu === 'cols' ? null : 'cols')}><svg width="20" height="20" viewBox="0 0 16 16" aria-hidden><rect x="2.2" y="3.4" width="2.7" height="9.2" rx="0.9" fill="none" stroke="currentColor" strokeWidth="0.9" /><rect x="6.65" y="3.4" width="2.7" height="9.2" rx="0.9" fill="none" stroke="currentColor" strokeWidth="0.9" /><rect x="11.1" y="3.4" width="2.7" height="9.2" rx="0.9" fill="none" stroke="currentColor" strokeWidth="0.9" /></svg></button>
|
||||
<button type="button" className={'dsh-wt_dockBtn' + (annOpen ? ' dsh-wt_dockBtnOn' : '')} title={T('annot.updateTitle')} aria-label={T('annot.updateTitle')} onClick={() => { setOpenMenu(null); setAnnOpen((v) => !v) }}><svg width="18" height="18" viewBox="0 0 16 16" aria-hidden><path d="M2.6 7c0-2.9 2.4-4.9 5.4-4.9s5.4 2 5.4 4.9v2.9l1.2 1.7H1.4l1.2-1.7z" fill="none" stroke="currentColor" strokeWidth="0.9" strokeLinejoin="round" /><path d="M6.2 12.9c.3.7 1 1.2 1.8 1.2s1.5-.5 1.8-1.2" fill="none" stroke="currentColor" strokeWidth="0.9" strokeLinecap="round" /></svg>{updInfo && <span className="dsh-wt_dockBadge" aria-hidden />}</button>
|
||||
</div>
|
||||
{openMenu === 'theme' && (
|
||||
<div className="dsh-wt_drop">
|
||||
|
||||
@@ -428,10 +428,26 @@ export const css = xtermCss + '\n' + [
|
||||
'.dsh-wt_consoleGrid{position:relative;z-index:1}',
|
||||
// 底部中央玻璃 dock:4 个 ghost 按钮(图标常显,hover 才浮现圆角矩形)+ 统一下拉面板(宽度 = dock 宽 150px)
|
||||
'.dsh-wt_consoleScroll{flex:1;min-height:0;overflow:auto;padding-bottom:10px}',
|
||||
'.dsh-wt_announce{width:100%;max-width:856px;margin:0 auto;display:flex;flex-direction:column;gap:12px;padding:26px;border-radius:16px;background:linear-gradient(135deg,rgba(255,255,255,.22) 0%,rgba(255,255,255,.07) 30%,rgba(255,255,255,.01) 60%,rgba(0,0,0,.07) 100%),rgba(18,23,32,.24);border:1px solid transparent;box-shadow:0 10px 28px rgba(0,0,0,.28),inset 0 1px 0 rgba(255,255,255,.13);box-sizing:border-box;backdrop-filter:blur(var(--wt-cardBlur,0px)) saturate(1.15)}',
|
||||
'.dsh-wt_console[data-wt-theme=light] .dsh-wt_announce{background:linear-gradient(135deg,rgba(255,255,255,1) 0%,rgba(255,255,255,.65) 30%,rgba(255,255,255,.12) 60%,rgba(31,41,55,.06) 100%),rgba(255,255,255,.34);box-shadow:0 10px 28px rgba(31,41,55,.18),inset 0 1px 0 rgba(255,255,255,1)}',
|
||||
'.dsh-wt_announceHead{display:flex;align-items:center;gap:10px;flex-wrap:wrap;padding-bottom:10px;border-bottom:1px solid var(--wt-border)}',
|
||||
'.dsh-wt_announceVer{font-size:15px;font-weight:700;color:var(--wt-text)}',
|
||||
'.dsh-wt_announceBtn{flex:none;padding:4px 12px;border:none;border-radius:7px;background:var(--wt-chip);color:var(--wt-text2);font:inherit;font-size:12px;line-height:17px;cursor:pointer;transition:background .15s ease,color .15s ease}',
|
||||
'.dsh-wt_announceBtn:hover{background:rgba(79,142,247,.22);color:var(--wt-text)}',
|
||||
'.dsh-wt_announceBtn:disabled{opacity:.6;cursor:default}',
|
||||
'.dsh-wt_announceAuto{flex:none;font-size:12px;line-height:17px;color:var(--wt-text2);margin-left:auto}',
|
||||
'.dsh-wt_announceUpdate{display:flex;align-items:center;gap:10px;flex-wrap:wrap;padding:8px 10px;border-radius:8px;background:rgba(79,142,247,.12)}',
|
||||
'.dsh-wt_announceNewVer{font-size:12px;line-height:17px;color:#7eb3ff;font-weight:600}',
|
||||
'.dsh-wt_announceSkip{flex:none;padding:3px 10px;border:none;border-radius:6px;background:transparent;color:var(--wt-text3);font:inherit;font-size:11px;line-height:16px;cursor:pointer}',
|
||||
'.dsh-wt_announceSkip:hover{background:rgba(244,63,94,.14);color:#f87171}',
|
||||
'.dsh-wt_announceStatus{font-size:11px;line-height:16px;color:var(--wt-text3)}',
|
||||
'.dsh-wt_announceBody{white-space:pre-wrap;font-size:13px;line-height:1.75;color:var(--wt-text2);user-select:text}',
|
||||
'.dsh-wt_consoleDockWrap{position:relative;z-index:12;flex:none;display:flex;justify-content:center;padding-top:12px}',
|
||||
'.dsh-wt_consoleDock{display:flex;align-items:center;gap:4px;padding:5px;border-radius:12px;background:rgba(255,255,255,.07);border:1px solid rgba(255,255,255,.16);box-shadow:0 8px 24px rgba(0,0,0,.35);backdrop-filter:blur(14px) saturate(1.3)}',
|
||||
'.dsh-wt_console[data-wt-theme=light] .dsh-wt_consoleDock{background:rgba(255,255,255,.62);border-color:rgba(27,31,36,.12);box-shadow:0 8px 24px rgba(31,41,55,.12)}',
|
||||
'.dsh-wt_dockBtn{width:32px;height:32px;padding:0;border:none;border-radius:9px;background:transparent;color:var(--wt-text);cursor:pointer;display:inline-flex;align-items:center;justify-content:center;transition:background .15s ease}',
|
||||
'.dsh-wt_dockBtn{position:relative;width:32px;height:32px;padding:0;border:none;border-radius:9px;background:transparent;color:var(--wt-text);cursor:pointer;display:inline-flex;align-items:center;justify-content:center;transition:background .15s ease}',
|
||||
'.dsh-wt_dockBadge{position:absolute;top:3px;right:3px;width:7px;height:7px;border-radius:50%;background:#f5a623;box-shadow:0 0 6px rgba(245,166,35,.95);animation:wtAnnPulse 1.6s ease-in-out infinite}',
|
||||
'@keyframes wtAnnPulse{0%,100%{opacity:1}50%{opacity:.3}}',
|
||||
'.dsh-wt_dockBtn svg{transition:transform .15s ease}',
|
||||
'.dsh-wt_dockBtn:hover svg{transform:scale(1.12)}',
|
||||
'.dsh-wt_dockBtn:hover{background:rgba(255,255,255,.14)}',
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
/** 更新检查核心(控制室公告页使用;设置面板保留自身实现,后续版本统一去重)。键与设置面板共用。 */
|
||||
declare const __WT_VERSION__: string
|
||||
export const LOCAL_VERSION = typeof __WT_VERSION__ === 'undefined' ? 'dev' : __WT_VERSION__
|
||||
const UPDATE_REPO = 'Aisland-SJL/dsh-worktable'
|
||||
|
||||
export type UpdateStatus = 'idle' | 'checking' | 'uptodate' | 'failed'
|
||||
export type UpdateInfo = { latest: string; notes: string; url: string }
|
||||
|
||||
const K_UPDATE_CHECK = 'dsh.worktable.updateCheck.v1'
|
||||
const K_LAST_CHECK = 'dsh.worktable.lastUpdateCheck.v1'
|
||||
const K_SKIP = 'dsh.worktable.skipVersion.v1'
|
||||
const K_CACHE = 'dsh.worktable.updateCache.v1'
|
||||
|
||||
export function cmpVer(a: string, b: string): number {
|
||||
const pa = String(a).split('.').map((x) => parseInt(x, 10) || 0)
|
||||
const pb = String(b).split('.').map((x) => parseInt(x, 10) || 0)
|
||||
for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
|
||||
const d = (pa[i] || 0) - (pb[i] || 0)
|
||||
if (d !== 0) return d > 0 ? 1 : -1
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
export function getSkipVersion(): string {
|
||||
try { return localStorage.getItem(K_SKIP) ?? '' } catch { return '' }
|
||||
}
|
||||
export function setSkipVersion(v: string) {
|
||||
try { localStorage.setItem(K_SKIP, v) } catch {}
|
||||
}
|
||||
export function getAutoCheck(): boolean {
|
||||
try { return localStorage.getItem(K_UPDATE_CHECK) !== '0' } catch { return true }
|
||||
}
|
||||
export function setAutoCheck(on: boolean) {
|
||||
try { localStorage.setItem(K_UPDATE_CHECK, on ? '1' : '0') } catch {}
|
||||
}
|
||||
export function readCache(): { status: UpdateStatus; info: UpdateInfo | null } {
|
||||
try {
|
||||
const raw = localStorage.getItem(K_CACHE)
|
||||
if (raw) {
|
||||
const c = JSON.parse(raw)
|
||||
if (c && typeof c.status === 'string') {
|
||||
return { status: c.status === 'checking' ? 'idle' : c.status, info: c.info ?? null }
|
||||
}
|
||||
}
|
||||
} catch {}
|
||||
return { status: 'idle', info: null }
|
||||
}
|
||||
export function writeCache(status: UpdateStatus, info: UpdateInfo | null) {
|
||||
try { localStorage.setItem(K_CACHE, JSON.stringify({ status, info })) } catch {}
|
||||
}
|
||||
|
||||
/** 检查更新:节流一天一次(force 绕过);8s 超时 × 3 重试;防重入由调用方保证 */
|
||||
export async function checkUpdate(force = false): Promise<{ status: UpdateStatus; info: UpdateInfo | null }> {
|
||||
const last = Number(localStorage.getItem(K_LAST_CHECK) ?? '0')
|
||||
if (!force && Date.now() - last < 24 * 3600 * 1000) return readCache()
|
||||
try { localStorage.setItem(K_LAST_CHECK, String(Date.now())) } catch {}
|
||||
let d: { tag_name?: string; body?: string; html_url?: string } | null = null
|
||||
for (let attempt = 0; attempt < 3 && !d; attempt++) {
|
||||
const ctrl = new AbortController()
|
||||
const timer = setTimeout(() => ctrl.abort(), 8000)
|
||||
try {
|
||||
const r = await fetch('https://api.github.com/repos/' + UPDATE_REPO + '/releases/latest', { headers: { Accept: 'application/vnd.github+json' }, cache: 'no-store', signal: ctrl.signal })
|
||||
if (r.ok) d = (await r.json()) as { tag_name?: string; body?: string; html_url?: string }
|
||||
else if (r.status === 403 || r.status === 404) break
|
||||
} catch { /* 网络抖动/超时:下一轮重试 */ }
|
||||
finally { clearTimeout(timer) }
|
||||
}
|
||||
if (!d) { writeCache('failed', null); return { status: 'failed', info: null } }
|
||||
const tag = (d.tag_name ?? '').replace(/^v/, '')
|
||||
if (!tag || cmpVer(tag, LOCAL_VERSION) <= 0) { writeCache('uptodate', null); return { status: 'uptodate', info: null } }
|
||||
if (getSkipVersion() === tag) { writeCache('uptodate', null); return { status: 'uptodate', info: null } }
|
||||
const info: UpdateInfo = { latest: tag, notes: (d.body ?? '').slice(0, 800), url: d.html_url ?? '' }
|
||||
writeCache('uptodate', info)
|
||||
return { status: 'uptodate', info }
|
||||
}
|
||||
Reference in New Issue
Block a user