feat: 支持 GitHub 主源与自定义策略更新

This commit is contained in:
ILoveBingLu
2026-04-01 23:41:45 +08:00
parent 87605b404a
commit a77e88907b
10 changed files with 614 additions and 64 deletions

View File

@@ -206,6 +206,52 @@ npm run mcp
安装版会附带 `ciphertalk-mcp.cmd` 伴随启动器,放在安装目录根部,可直接作为宿主的 `command` 使用。
### 强制更新清单
当前更新架构:
- **主更新源**GitHub Release安装包、`latest.yml`
- **策略补充源**`https://miyuapp.aiqji.com`
- **策略优先级**GitHub 优先,自定义源仅在 GitHub 策略不可用时作为回退
应用启动时会按以下顺序请求 `force-update.json`,用于判定:
1. `https://github.com/ILoveBingLu/CipherTalk/releases/latest/download/force-update.json`
2. `https://miyuapp.aiqji.com/force-update.json`
策略字段含义:
- 最低安全版本 `minimumSupportedVersion`
- 被封禁版本列表 `blockedVersions`
- 强制更新提示文案 `title` / `message`
可用以下命令在 `release/force-update.json` 生成清单:
```bash
FORCE_UPDATE_MIN_VERSION=2.2.15 npm run build:force-update-manifest
```
示例结构:
```json
{
"schemaVersion": 1,
"latestVersion": "2.2.15",
"minimumSupportedVersion": "2.2.14",
"blockedVersions": ["2.2.13"],
"title": "必须更新到最新版本",
"message": "当前版本存在安全风险,请立即更新。",
"releaseNotes": "修复关键安全问题",
"publishedAt": "2026-04-01T00:00:00.000Z"
}
```
发布要求:
- **GitHub Release 必须上传**:安装包、`latest.yml``force-update.json`
- **自定义源可上传**`force-update.json`
- 自定义源不再承担安装包和 `latest.yml` 分发
### v1 工具
- `health_check`

View File

@@ -3,6 +3,7 @@ import { join } from 'path'
import { readFileSync, existsSync, mkdirSync } from 'fs'
import { autoUpdater } from 'electron-updater'
import { DatabaseService } from './services/database'
import { appUpdateService } from './services/appUpdateService'
import { wechatDecryptService } from './services/decryptService'
import { ConfigService } from './services/config'
@@ -61,29 +62,6 @@ autoUpdater.autoDownload = false
autoUpdater.autoInstallOnAppQuit = true
autoUpdater.disableDifferentialDownload = true // 禁用差分更新,强制全量下载
/**
* 比较两个语义化版本号
* @param version1 版本1
* @param version2 版本2
* @returns version1 > version2 返回 true
*/
function isNewerVersion(version1: string, version2: string): boolean {
const v1Parts = version1.split('.').map(Number)
const v2Parts = version2.split('.').map(Number)
// 补齐版本号位数
const maxLength = Math.max(v1Parts.length, v2Parts.length)
while (v1Parts.length < maxLength) v1Parts.push(0)
while (v2Parts.length < maxLength) v2Parts.push(0)
for (let i = 0; i < maxLength; i++) {
if (v1Parts[i] > v2Parts[i]) return true
if (v1Parts[i] < v2Parts[i]) return false
}
return false // 版本相同
}
// 单例服务
let dbService: DatabaseService | null = null
@@ -237,6 +215,12 @@ function createWindow() {
// 监听窗口关闭事件
win.on('close', (event) => {
const updateInfo = appUpdateService.getCachedUpdateInfo()
if (updateInfo?.forceUpdate) {
app.isQuitting = true
return
}
// 如果是真正退出应用,不阻止
if (app.isQuitting) {
return
@@ -1297,25 +1281,20 @@ function registerIpcHandlers() {
})
ipcMain.handle('app:checkForUpdates', async () => {
try {
const result = await autoUpdater.checkForUpdates()
if (result && result.updateInfo) {
const currentVersion = app.getVersion()
const latestVersion = result.updateInfo.version
return appUpdateService.checkForUpdates()
})
// 使用语义化版本比较
if (isNewerVersion(latestVersion, currentVersion)) {
return {
hasUpdate: true,
version: latestVersion,
releaseNotes: result.updateInfo.releaseNotes as string || ''
}
}
}
return { hasUpdate: false }
} catch (error) {
console.error('检查更新失败:', error)
return { hasUpdate: false }
ipcMain.handle('app:getUpdateState', async () => {
return appUpdateService.getCachedUpdateInfo()
})
ipcMain.handle('app:getUpdateSourceInfo', async () => {
return {
primaryUpdateSource: 'github' as const,
githubRepository: appUpdateService.getGithubRepository(),
policySources: ['github', 'custom'] as const,
policyPrecedence: 'github' as const,
forceUpdatePolicyFallbackUrl: appUpdateService.getForceUpdatePolicyFallbackUrl()
}
})
@@ -3708,19 +3687,9 @@ function checkForUpdatesOnStartup() {
// 延迟3秒检测等待窗口完全加载
setTimeout(async () => {
try {
const result = await autoUpdater.checkForUpdates()
if (result && result.updateInfo) {
const currentVersion = app.getVersion()
const latestVersion = result.updateInfo.version
// 使用语义化版本比较
if (isNewerVersion(latestVersion, currentVersion) && mainWindow) {
// 通知渲染进程有新版本
mainWindow.webContents.send('app:updateAvailable', {
version: latestVersion,
releaseNotes: result.updateInfo.releaseNotes || ''
})
}
const result = await appUpdateService.checkForUpdates()
if (result.hasUpdate && mainWindow) {
mainWindow.webContents.send('app:updateAvailable', result)
}
} catch (error) {
console.error('启动时检查更新失败:', error)

View File

@@ -71,6 +71,8 @@ contextBridge.exposeInMainWorld('electronAPI', {
getDownloadsPath: () => ipcRenderer.invoke('app:getDownloadsPath'),
getVersion: () => ipcRenderer.invoke('app:getVersion'),
getMcpLaunchConfig: () => getMcpLaunchConfigSafe(),
getUpdateState: () => ipcRenderer.invoke('app:getUpdateState'),
getUpdateSourceInfo: () => ipcRenderer.invoke('app:getUpdateSourceInfo'),
checkForUpdates: () => ipcRenderer.invoke('app:checkForUpdates'),
downloadAndInstall: () => ipcRenderer.invoke('app:downloadAndInstall'),
getStartupDbConnected: () => ipcRenderer.invoke('app:getStartupDbConnected'),
@@ -79,7 +81,20 @@ contextBridge.exposeInMainWorld('electronAPI', {
ipcRenderer.on('app:downloadProgress', (_, progress) => callback(progress))
return () => ipcRenderer.removeAllListeners('app:downloadProgress')
},
onUpdateAvailable: (callback: (info: { version: string; releaseNotes: string }) => void) => {
onUpdateAvailable: (callback: (info: {
hasUpdate: boolean
forceUpdate: boolean
currentVersion: string
version?: string
releaseNotes?: string
title?: string
message?: string
minimumSupportedVersion?: string
reason?: 'minimum-version' | 'blocked-version'
checkedAt: number
updateSource: 'github' | 'custom' | 'none'
policySource: 'github' | 'custom' | 'none'
}) => void) => {
ipcRenderer.on('app:updateAvailable', (_, info) => callback(info))
return () => ipcRenderer.removeAllListeners('app:updateAvailable')
}

View File

@@ -0,0 +1,198 @@
import { app } from 'electron'
import { autoUpdater } from 'electron-updater'
const GITHUB_OWNER = 'ILoveBingLu'
const GITHUB_REPO = 'CipherTalk'
const GITHUB_FORCE_UPDATE_URL = `https://github.com/${GITHUB_OWNER}/${GITHUB_REPO}/releases/latest/download/force-update.json`
const FORCE_UPDATE_POLICY_FALLBACK_URL = 'https://miyuapp.aiqji.com'
export type ForceUpdateReason = 'minimum-version' | 'blocked-version'
export type AppUpdateSource = 'github' | 'custom' | 'none'
export interface ForceUpdateManifest {
schemaVersion: number
latestVersion?: string
minimumSupportedVersion?: string
blockedVersions?: string[]
title?: string
message?: string
releaseNotes?: string
publishedAt?: string
}
export interface AppUpdateInfo {
hasUpdate: boolean
forceUpdate: boolean
currentVersion: string
version?: string
releaseNotes?: string
title?: string
message?: string
minimumSupportedVersion?: string
reason?: ForceUpdateReason
checkedAt: number
updateSource: AppUpdateSource
policySource: AppUpdateSource
}
type ManifestLookupResult = {
manifest: ForceUpdateManifest | null
source: AppUpdateSource
}
function isNewerVersion(version1: string, version2: string): boolean {
const v1Parts = version1.split('.').map(Number)
const v2Parts = version2.split('.').map(Number)
const maxLength = Math.max(v1Parts.length, v2Parts.length)
while (v1Parts.length < maxLength) v1Parts.push(0)
while (v2Parts.length < maxLength) v2Parts.push(0)
for (let i = 0; i < maxLength; i++) {
if (v1Parts[i] > v2Parts[i]) return true
if (v1Parts[i] < v2Parts[i]) return false
}
return false
}
function isVersionEqual(version1: string, version2: string): boolean {
return !isNewerVersion(version1, version2) && !isNewerVersion(version2, version1)
}
function normalizeReleaseNotes(value: unknown): string {
if (!value) return ''
if (typeof value === 'string') return value
if (Array.isArray(value)) {
return value.map((item) => {
if (typeof item === 'string') return item
if (item && typeof item === 'object' && 'note' in item) {
return String((item as { note?: unknown }).note || '')
}
return String(item)
}).filter(Boolean).join('\n\n')
}
if (value && typeof value === 'object' && 'note' in value) {
return String((value as { note?: unknown }).note || '')
}
return String(value)
}
async function fetchManifestFromUrl(url: string): Promise<ForceUpdateManifest | null> {
try {
const response = await fetch(`${url}${url.includes('?') ? '&' : '?'}t=${Date.now()}`, {
cache: 'no-store',
headers: {
Accept: 'application/json'
}
})
if (!response.ok) return null
const data = await response.json() as ForceUpdateManifest
if (!data || typeof data !== 'object') return null
if (Number(data.schemaVersion || 0) < 1) return null
return data
} catch (error) {
console.warn('[AppUpdate] 获取策略文件失败:', url, error)
return null
}
}
async function resolveForceUpdateManifest(): Promise<ManifestLookupResult> {
const githubManifest = await fetchManifestFromUrl(GITHUB_FORCE_UPDATE_URL)
if (githubManifest) {
return { manifest: githubManifest, source: 'github' }
}
const fallbackUrl = `${FORCE_UPDATE_POLICY_FALLBACK_URL.replace(/\/+$/, '')}/force-update.json`
const customManifest = await fetchManifestFromUrl(fallbackUrl)
if (customManifest) {
return { manifest: customManifest, source: 'custom' }
}
return { manifest: null, source: 'none' }
}
class AppUpdateService {
private lastInfo: AppUpdateInfo | null = null
getCachedUpdateInfo(): AppUpdateInfo | null {
return this.lastInfo
}
getForceUpdatePolicyFallbackUrl(): string {
return FORCE_UPDATE_POLICY_FALLBACK_URL
}
getGithubRepository(): { owner: string; repo: string } {
return {
owner: GITHUB_OWNER,
repo: GITHUB_REPO
}
}
private buildInfo(payload: Partial<AppUpdateInfo>): AppUpdateInfo {
return {
hasUpdate: false,
forceUpdate: false,
currentVersion: app.getVersion(),
checkedAt: Date.now(),
updateSource: 'none',
policySource: 'none',
...payload
}
}
async checkForUpdates(): Promise<AppUpdateInfo> {
const currentVersion = app.getVersion()
let latestVersion: string | undefined
let releaseNotes = ''
let hasUpdate = false
let updateSource: AppUpdateSource = 'none'
try {
const result = await autoUpdater.checkForUpdates()
if (result?.updateInfo?.version) {
latestVersion = result.updateInfo.version
releaseNotes = normalizeReleaseNotes(result.updateInfo.releaseNotes)
hasUpdate = isNewerVersion(latestVersion, currentVersion)
updateSource = hasUpdate ? 'github' : 'none'
}
} catch (error) {
console.error('[AppUpdate] 检查 GitHub 更新失败:', error)
}
const { manifest, source: policySource } = await resolveForceUpdateManifest()
let forceUpdate = false
let reason: ForceUpdateReason | undefined
if (manifest?.minimumSupportedVersion && isNewerVersion(manifest.minimumSupportedVersion, currentVersion)) {
forceUpdate = true
reason = 'minimum-version'
} else if (manifest?.blockedVersions?.some((version) => isVersionEqual(currentVersion, version))) {
forceUpdate = true
reason = 'blocked-version'
}
const finalVersion = latestVersion || manifest?.latestVersion
const finalReleaseNotes = releaseNotes || manifest?.releaseNotes || ''
const info = this.buildInfo({
hasUpdate: hasUpdate || forceUpdate,
forceUpdate,
currentVersion,
version: finalVersion,
releaseNotes: finalReleaseNotes,
title: manifest?.title || (forceUpdate ? '必须更新到最新版本' : undefined),
message: manifest?.message,
minimumSupportedVersion: manifest?.minimumSupportedVersion,
reason,
updateSource,
policySource
})
this.lastInfo = info
return info
}
}
export const appUpdateService = new AppUpdateService()

View File

@@ -12,6 +12,7 @@
"build": "tsc && vite build && electron-builder && node scripts/add-size-to-yml.js",
"build:mcp": "tsc && vite build",
"build:pro": "node scripts/build-full.js",
"build:force-update-manifest": "node scripts/generate-force-update-manifest.js",
"mcp": "node scripts/mcp-runner.js",
"mcp:probe": "node scripts/mcp-probe.js",
"preview": "vite preview",
@@ -88,8 +89,9 @@
"output": "release"
},
"publish": {
"provider": "generic",
"url": "https://miyuapp.aiqji.com"
"provider": "github",
"owner": "ILoveBingLu",
"repo": "CipherTalk"
},
"win": {
"icon": "public/xinnian.ico",

View File

@@ -0,0 +1,33 @@
const fs = require('fs')
const path = require('path')
const rootDir = path.resolve(__dirname, '..')
const releaseDir = path.join(rootDir, 'release')
const pkg = require(path.join(rootDir, 'package.json'))
const parseList = (value) => {
if (!value) return []
return String(value)
.split(',')
.map((item) => item.trim())
.filter(Boolean)
}
const manifest = {
schemaVersion: 1,
latestVersion: pkg.version,
minimumSupportedVersion: process.env.FORCE_UPDATE_MIN_VERSION || undefined,
blockedVersions: parseList(process.env.FORCE_UPDATE_BLOCKED_VERSIONS),
title: process.env.FORCE_UPDATE_TITLE || '',
message: process.env.FORCE_UPDATE_MESSAGE || '',
releaseNotes: process.env.FORCE_UPDATE_RELEASE_NOTES || '',
publishedAt: new Date().toISOString()
}
if (!fs.existsSync(releaseDir)) {
fs.mkdirSync(releaseDir, { recursive: true })
}
const outputPath = path.join(releaseDir, 'force-update.json')
fs.writeFileSync(outputPath, `${JSON.stringify(manifest, null, 2)}\n`, 'utf8')
console.log(`✅ force-update.json 已生成: ${outputPath}`)

View File

@@ -88,6 +88,120 @@
}
}
.force-update-overlay {
position: fixed;
inset: 0;
z-index: 5000;
display: flex;
align-items: center;
justify-content: center;
padding: 24px;
background: rgba(6, 10, 16, 0.82);
backdrop-filter: blur(14px);
}
.force-update-card {
width: min(720px, 100%);
max-height: 85vh;
overflow: auto;
padding: 28px;
border-radius: 20px;
border: 1px solid rgba(255, 255, 255, 0.08);
background: var(--bg-secondary);
box-shadow: 0 24px 80px rgba(0, 0, 0, 0.35);
h2 {
margin: 16px 0 10px;
font-size: 28px;
font-weight: 700;
color: var(--text-primary);
}
.force-update-desc {
margin: 0 0 18px;
color: var(--text-secondary);
line-height: 1.7;
}
.force-update-meta {
display: grid;
gap: 8px;
margin-bottom: 18px;
padding: 14px 16px;
border-radius: 14px;
background: var(--bg-primary);
color: var(--text-primary);
font-size: 14px;
}
.force-update-notes {
margin-bottom: 18px;
padding: 14px 16px;
border-radius: 14px;
background: var(--bg-primary);
.force-update-notes-title {
margin-bottom: 10px;
font-weight: 600;
color: var(--text-primary);
}
pre {
margin: 0;
white-space: pre-wrap;
word-break: break-word;
color: var(--text-secondary);
font-family: inherit;
line-height: 1.6;
}
}
.force-update-progress {
margin-bottom: 18px;
}
.force-update-progress-label {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 10px;
color: var(--text-primary);
font-size: 14px;
font-weight: 600;
}
.force-update-progress-bar {
height: 10px;
border-radius: 999px;
background: var(--bg-primary);
overflow: hidden;
}
.force-update-progress-fill {
height: 100%;
border-radius: inherit;
background: linear-gradient(90deg, #ff7849 0%, #ff4747 100%);
}
.force-update-actions {
display: flex;
gap: 12px;
justify-content: flex-end;
}
}
.force-update-badge {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 8px 12px;
border-radius: 999px;
background: rgba(255, 92, 92, 0.12);
color: #ff5c5c;
font-size: 13px;
font-weight: 700;
}
// 独立聊天窗口容器
.chat-window-container {
height: 100vh;

View File

@@ -38,6 +38,21 @@ import { useAuthStore } from './stores/authStore'
import { X, Shield, Loader2 } from 'lucide-react'
import './App.scss'
type AppUpdateInfo = {
hasUpdate: boolean
forceUpdate: boolean
currentVersion: string
version?: string
releaseNotes?: string
title?: string
message?: string
minimumSupportedVersion?: string
reason?: 'minimum-version' | 'blocked-version'
checkedAt: number
updateSource: 'github' | 'custom' | 'none'
policySource: 'github' | 'custom' | 'none'
}
function App() {
const navigate = useNavigate()
const location = useLocation()
@@ -55,7 +70,7 @@ function App() {
const [showActivation, setShowActivation] = useState(false)
// 更新提示状态
const [updateInfo, setUpdateInfo] = useState<{ version: string; releaseNotes: string } | null>(null)
const [updateInfo, setUpdateInfo] = useState<AppUpdateInfo | null>(null)
const [downloadProgress, setDownloadProgress] = useState<number | null>(null)
// 加载主题配置
@@ -136,6 +151,15 @@ function App() {
// 监听启动时的更新通知
useEffect(() => {
let mounted = true
window.electronAPI.app.getUpdateState?.().then((info) => {
if (mounted && info?.hasUpdate) {
setUpdateInfo(info)
}
}).catch((error) => {
console.error('获取更新状态失败:', error)
})
const removeUpdateListener = window.electronAPI.app.onUpdateAvailable?.((info) => {
setUpdateInfo(info)
})
@@ -162,6 +186,7 @@ function App() {
})
return () => {
mounted = false
removeUpdateListener?.()
removeSessionsListener?.()
removeUpdateAvailableListener?.()
@@ -179,6 +204,7 @@ function App() {
}, [])
const dismissUpdate = () => {
if (updateInfo?.forceUpdate) return
setUpdateInfo(null)
}
@@ -466,12 +492,13 @@ function App() {
return (
<div className="app-container">
<TitleBar />
{updateInfo && (
{updateInfo && !updateInfo.forceUpdate && (
<div className="update-toast">
<div className="update-toast-icon">🎉</div>
<div className="update-toast-content">
<div className="update-toast-title"></div>
<div className="update-toast-version">v{updateInfo.version} </div>
<div className="update-toast-version">{updateInfo.updateSource === 'github' ? 'GitHub Release' : '未知'}</div>
</div>
<button className="update-toast-btn" onClick={() => {
window.electronAPI.app.downloadAndInstall()
@@ -484,6 +511,56 @@ function App() {
</button>
</div>
)}
{updateInfo?.forceUpdate && (
<div className="force-update-overlay">
<div className="force-update-card">
<div className="force-update-badge">
<Shield size={18} />
<span></span>
</div>
<h2>{updateInfo.title || '必须更新后才能继续使用'}</h2>
<p className="force-update-desc">
{updateInfo.message || '当前版本已被标记为需要立即升级,应用将限制继续使用,直到安装最新版本。'}
</p>
<div className="force-update-meta">
<div>v{updateInfo.currentVersion}</div>
{updateInfo.version && <div>v{updateInfo.version}</div>}
{updateInfo.minimumSupportedVersion && <div>v{updateInfo.minimumSupportedVersion}</div>}
<div>{updateInfo.updateSource === 'github' ? 'GitHub Release' : '未检测到普通更新源'}</div>
<div>{updateInfo.policySource === 'github' ? 'GitHub 策略源' : updateInfo.policySource === 'custom' ? '自定义策略源' : '无'}</div>
</div>
{updateInfo.releaseNotes && (
<div className="force-update-notes">
<div className="force-update-notes-title"></div>
<pre>{updateInfo.releaseNotes}</pre>
</div>
)}
{downloadProgress !== null && (
<div className="force-update-progress">
<div className="force-update-progress-label">
<Loader2 size={16} className="spin" />
<span>... {downloadProgress.toFixed(0)}%</span>
</div>
<div className="force-update-progress-bar">
<div className="force-update-progress-fill" style={{ width: `${downloadProgress}%` }} />
</div>
</div>
)}
<div className="force-update-actions">
<button className="btn btn-primary" onClick={() => window.electronAPI.app.downloadAndInstall()}>
</button>
<button className="btn btn-secondary" onClick={() => window.electronAPI.window.close()}>
退
</button>
</div>
</div>
</div>
)}
<Box
sx={{

View File

@@ -98,7 +98,30 @@ function SettingsPage() {
const [isDownloading, setIsDownloading] = useState(false)
const [downloadProgress, setDownloadProgress] = useState(0)
const [appVersion, setAppVersion] = useState('')
const [updateInfo, setUpdateInfo] = useState<{ hasUpdate: boolean; version?: string; releaseNotes?: string } | null>(null)
const [updateInfo, setUpdateInfo] = useState<{
hasUpdate: boolean
forceUpdate: boolean
currentVersion: string
version?: string
releaseNotes?: string
title?: string
message?: string
minimumSupportedVersion?: string
reason?: 'minimum-version' | 'blocked-version'
checkedAt: number
updateSource: 'github' | 'custom' | 'none'
policySource: 'github' | 'custom' | 'none'
} | null>(null)
const [updateSourceInfo, setUpdateSourceInfo] = useState<{
primaryUpdateSource: 'github'
githubRepository: {
owner: string
repo: string
}
policySources: Array<'github' | 'custom'>
policyPrecedence: 'github'
forceUpdatePolicyFallbackUrl: string
} | null>(null)
const [keyStatus, setKeyStatus] = useState('')
const [message, setMessage] = useState<{ text: string; success: boolean } | null>(null)
const [showDecryptKey, setShowDecryptKey] = useState(false)
@@ -466,7 +489,7 @@ function SettingsPage() {
const result = await window.electronAPI.app.checkForUpdates()
if (result.hasUpdate) {
setUpdateInfo(result)
showMessage(`发现新版本 ${result.version}`, true)
showMessage(result.forceUpdate ? `检测到强制更新 ${result.version}` : `发现新版本 ${result.version}`, true)
} else {
showMessage('当前已是最新版本', true)
}
@@ -2646,6 +2669,14 @@ function SettingsPage() {
}
}, [location.state])
useEffect(() => {
window.electronAPI.app.getUpdateSourceInfo?.().then((info) => {
setUpdateSourceInfo(info)
}).catch((error) => {
console.error('获取更新源信息失败:', error)
})
}, [])
const renderAboutTab = () => (
<div className="tab-content about-tab">
<div className="about-card">
@@ -2657,9 +2688,24 @@ function SettingsPage() {
<p className="about-version">v{appVersion || '...'}</p>
<div className="about-update">
{updateSourceInfo && (
<div className="update-hint" style={{ marginBottom: '10px' }}>
GitHub Release ({updateSourceInfo.githubRepository.owner}/{updateSourceInfo.githubRepository.repo})<br />
{updateSourceInfo.forceUpdatePolicyFallbackUrl}
</div>
)}
{updateInfo?.hasUpdate ? (
<>
<p className="update-hint"> v{updateInfo.version} </p>
<p className="update-hint">
{updateInfo.forceUpdate ? '检测到强制更新' : `新版本 v${updateInfo.version} 可用`}
</p>
<p className="update-hint">
{updateInfo.updateSource === 'github' ? 'GitHub Release' : '未知'} /
{updateInfo.policySource === 'github' ? 'GitHub' : updateInfo.policySource === 'custom' ? '自定义源' : '无'}
</p>
{updateInfo.forceUpdate && updateInfo.minimumSupportedVersion && (
<p className="update-hint">v{updateInfo.minimumSupportedVersion}</p>
)}
{isDownloading ? (
<div className="download-progress">
<div className="progress-bar">

View File

@@ -81,12 +81,62 @@ export interface ElectronAPI {
cwd: string
mode: 'dev' | 'packaged'
} | null>
checkForUpdates: () => Promise<{ hasUpdate: boolean; version?: string; releaseNotes?: string }>
getUpdateState: () => Promise<{
hasUpdate: boolean
forceUpdate: boolean
currentVersion: string
version?: string
releaseNotes?: string
title?: string
message?: string
minimumSupportedVersion?: string
reason?: 'minimum-version' | 'blocked-version'
checkedAt: number
updateSource: 'github' | 'custom' | 'none'
policySource: 'github' | 'custom' | 'none'
} | null>
getUpdateSourceInfo: () => Promise<{
primaryUpdateSource: 'github'
githubRepository: {
owner: string
repo: string
}
policySources: Array<'github' | 'custom'>
policyPrecedence: 'github'
forceUpdatePolicyFallbackUrl: string
}>
checkForUpdates: () => Promise<{
hasUpdate: boolean
forceUpdate: boolean
currentVersion: string
version?: string
releaseNotes?: string
title?: string
message?: string
minimumSupportedVersion?: string
reason?: 'minimum-version' | 'blocked-version'
checkedAt: number
updateSource: 'github' | 'custom' | 'none'
policySource: 'github' | 'custom' | 'none'
}>
downloadAndInstall: () => Promise<void>
getStartupDbConnected?: () => Promise<boolean>
setAppIcon: (iconName: string) => Promise<void>
onDownloadProgress: (callback: (progress: number) => void) => () => void
onUpdateAvailable: (callback: (info: { version: string; releaseNotes: string }) => void) => () => void
onUpdateAvailable: (callback: (info: {
hasUpdate: boolean
forceUpdate: boolean
currentVersion: string
version?: string
releaseNotes?: string
title?: string
message?: string
minimumSupportedVersion?: string
reason?: 'minimum-version' | 'blocked-version'
checkedAt: number
updateSource: 'github' | 'custom' | 'none'
policySource: 'github' | 'custom' | 'none'
}) => void) => () => void
}
httpApi: {
getStatus: () => Promise<{