diff --git a/README.md b/README.md index 4058970..8a5ad96 100644 --- a/README.md +++ b/README.md @@ -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` diff --git a/electron/main.ts b/electron/main.ts index f4641b4..24c2a70 100644 --- a/electron/main.ts +++ b/electron/main.ts @@ -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) diff --git a/electron/preload.ts b/electron/preload.ts index 423081c..1e18052 100644 --- a/electron/preload.ts +++ b/electron/preload.ts @@ -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') } diff --git a/electron/services/appUpdateService.ts b/electron/services/appUpdateService.ts new file mode 100644 index 0000000..ea5f032 --- /dev/null +++ b/electron/services/appUpdateService.ts @@ -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 { + 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 { + 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 { + return { + hasUpdate: false, + forceUpdate: false, + currentVersion: app.getVersion(), + checkedAt: Date.now(), + updateSource: 'none', + policySource: 'none', + ...payload + } + } + + async checkForUpdates(): Promise { + 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() diff --git a/package.json b/package.json index 9c75a17..faec754 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/scripts/generate-force-update-manifest.js b/scripts/generate-force-update-manifest.js new file mode 100644 index 0000000..3914746 --- /dev/null +++ b/scripts/generate-force-update-manifest.js @@ -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}`) diff --git a/src/App.scss b/src/App.scss index ee57a59..e917db1 100644 --- a/src/App.scss +++ b/src/App.scss @@ -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; diff --git a/src/App.tsx b/src/App.tsx index 6f50626..93fe709 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -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(null) const [downloadProgress, setDownloadProgress] = useState(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 (
- {updateInfo && ( + {updateInfo && !updateInfo.forceUpdate && (
🎉
发现新版本
v{updateInfo.version} 已发布
+
更新源:{updateInfo.updateSource === 'github' ? 'GitHub Release' : '未知'}
)} + {updateInfo?.forceUpdate && ( +
+
+
+ + 强制更新 +
+

{updateInfo.title || '必须更新后才能继续使用'}

+

+ {updateInfo.message || '当前版本已被标记为需要立即升级,应用将限制继续使用,直到安装最新版本。'} +

+ +
+
当前版本:v{updateInfo.currentVersion}
+ {updateInfo.version &&
目标版本:v{updateInfo.version}
} + {updateInfo.minimumSupportedVersion &&
最低安全版本:v{updateInfo.minimumSupportedVersion}
} +
更新来源:{updateInfo.updateSource === 'github' ? 'GitHub Release' : '未检测到普通更新源'}
+
策略来源:{updateInfo.policySource === 'github' ? 'GitHub 策略源' : updateInfo.policySource === 'custom' ? '自定义策略源' : '无'}
+
+ + {updateInfo.releaseNotes && ( +
+
更新说明
+
{updateInfo.releaseNotes}
+
+ )} + + {downloadProgress !== null && ( +
+
+ + 正在下载更新... {downloadProgress.toFixed(0)}% +
+
+
+
+
+ )} + +
+ + +
+
+
+ )} (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 = () => (
@@ -2657,9 +2688,24 @@ function SettingsPage() {

v{appVersion || '...'}

+ {updateSourceInfo && ( +
+ 主更新源:GitHub Release ({updateSourceInfo.githubRepository.owner}/{updateSourceInfo.githubRepository.repo})
+ 策略补充源:{updateSourceInfo.forceUpdatePolicyFallbackUrl} +
+ )} {updateInfo?.hasUpdate ? ( <> -

新版本 v{updateInfo.version} 可用

+

+ {updateInfo.forceUpdate ? '检测到强制更新' : `新版本 v${updateInfo.version} 可用`} +

+

+ 更新来源:{updateInfo.updateSource === 'github' ? 'GitHub Release' : '未知'} / 策略来源: + {updateInfo.policySource === 'github' ? 'GitHub' : updateInfo.policySource === 'custom' ? '自定义源' : '无'} +

+ {updateInfo.forceUpdate && updateInfo.minimumSupportedVersion && ( +

最低安全版本:v{updateInfo.minimumSupportedVersion}

+ )} {isDownloading ? (
diff --git a/src/types/electron.d.ts b/src/types/electron.d.ts index cea9479..a9b158e 100644 --- a/src/types/electron.d.ts +++ b/src/types/electron.d.ts @@ -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 getStartupDbConnected?: () => Promise setAppIcon: (iconName: string) => Promise 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<{