From ad34fe387b0cf0d5b5a763442995397eb6abb437 Mon Sep 17 00:00:00 2001 From: 7418 <7418@openclaw.ai> Date: Sat, 28 Feb 2026 11:45:36 +0800 Subject: [PATCH] =?UTF-8?q?chore:=20=E7=A6=81=E7=94=A8=20native=20auto-upd?= =?UTF-8?q?ater=EF=BC=8C=E6=94=B9=E4=B8=BA=E5=BC=95=E5=AF=BC=E7=94=A8?= =?UTF-8?q?=E6=88=B7=E4=BB=8E=20GitHub=20=E4=B8=8B=E8=BD=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit macOS 上 ad-hoc 签名无法通过 ShipIt 的 kSecCSStrictValidate 校验, 导致热更新始终失败。在获取 Apple 开发者证书之前,暂时禁用 native auto-updater,改为 browser-mode 检查 GitHub Releases API: - electron/updater.ts: 注释掉 electron-updater 逻辑,initAutoUpdater 和 setUpdaterWindow 改为 no-op - electron/preload.ts: 注释掉 updater IPC bridge,前端不再暴露 window.electronAPI.updater - AppShell.tsx: isNativeUpdater 硬编码为 false,移除 native IPC 订阅 和 native check/download/quitAndInstall 逻辑;保留 browser-mode 的 GitHub API 检查(mount 时 + 每 8 小时),发现新版本时弹窗引导 用户跳转 GitHub Release 页面下载 设置页 UpdateCard 保持不变:「检查更新」按钮通过 /api/app/updates 查询 GitHub,有新版本时显示「查看发布」按钮跳转下载。 TODO: 获取 Apple 开发者证书后,取消注释恢复 native auto-updater。 Co-Authored-By: Claude Opus 4.6 --- electron/preload.ts | 23 +++-- electron/updater.ts | 147 ++++------------------------- src/components/layout/AppShell.tsx | 123 +++--------------------- 3 files changed, 45 insertions(+), 248 deletions(-) diff --git a/electron/preload.ts b/electron/preload.ts index 20f6803..7c97cc3 100644 --- a/electron/preload.ts +++ b/electron/preload.ts @@ -25,14 +25,17 @@ contextBridge.exposeInMainWorld('electronAPI', { return () => { ipcRenderer.removeListener('install:progress', listener); }; }, }, - updater: { - checkForUpdates: () => ipcRenderer.invoke('updater:check'), - downloadUpdate: () => ipcRenderer.invoke('updater:download'), - quitAndInstall: () => ipcRenderer.invoke('updater:quit-and-install'), - onStatus: (callback: (data: unknown) => void) => { - const listener = (_event: unknown, data: unknown) => callback(data); - ipcRenderer.on('updater:status', listener); - return () => { ipcRenderer.removeListener('updater:status', listener); }; - }, - }, + // Native auto-updater bridge — disabled (code signature issues on macOS). + // Users are directed to download from GitHub Releases. + // TODO: Re-enable after obtaining Apple Developer certificate. + // updater: { + // checkForUpdates: () => ipcRenderer.invoke('updater:check'), + // downloadUpdate: () => ipcRenderer.invoke('updater:download'), + // quitAndInstall: () => ipcRenderer.invoke('updater:quit-and-install'), + // onStatus: (callback: (data: unknown) => void) => { + // const listener = (_event: unknown, data: unknown) => callback(data); + // ipcRenderer.on('updater:status', listener); + // return () => { ipcRenderer.removeListener('updater:status', listener); }; + // }, + // }, }); diff --git a/electron/updater.ts b/electron/updater.ts index adf2e91..d13b2d0 100644 --- a/electron/updater.ts +++ b/electron/updater.ts @@ -1,132 +1,25 @@ -import { autoUpdater } from 'electron-updater'; +// ============================================================================= +// Native auto-updater (electron-updater) — DISABLED +// +// Temporarily disabled due to macOS code signature validation failures with +// ad-hoc signing. Users are directed to download from GitHub Releases instead. +// The browser-mode update check (via /api/app/updates) remains active in the +// frontend to notify users of new versions. +// +// TODO: Re-enable after obtaining an Apple Developer certificate for proper +// code signing, then uncomment this file and the calls in main.ts / preload.ts. +// ============================================================================= + +// import { autoUpdater } from 'electron-updater'; import type { BrowserWindow } from 'electron'; -import { ipcMain, session } from 'electron'; +// import { ipcMain, session } from 'electron'; -let mainWindow: BrowserWindow | null = null; - -function sendStatus(data: Record) { - mainWindow?.webContents.send('updater:status', data); +// eslint-disable-next-line @typescript-eslint/no-unused-vars +export function initAutoUpdater(_win: BrowserWindow) { + console.log('[updater] Native auto-updater is disabled. Users should download updates from GitHub Releases.'); } -/** - * Resolve system proxy for GitHub and inject into electron-updater - * so that VPN / proxy tools are respected during update downloads. - */ -async function configureProxy() { - try { - const proxy = await session.defaultSession.resolveProxy('https://github.com'); - // proxy returns "DIRECT" or "PROXY host:port" / "SOCKS5 host:port" etc. - if (proxy && proxy !== 'DIRECT') { - const match = proxy.match(/^(?:PROXY|HTTPS)\s+(.+)/i); - if (match) { - process.env.HTTPS_PROXY = `http://${match[1]}`; - console.log('[updater] Using system proxy:', process.env.HTTPS_PROXY); - } - const socksMatch = proxy.match(/^SOCKS5?\s+(.+)/i); - if (socksMatch) { - process.env.HTTPS_PROXY = `socks5://${socksMatch[1]}`; - console.log('[updater] Using system SOCKS proxy:', process.env.HTTPS_PROXY); - } - } - } catch (err) { - console.warn('[updater] Failed to resolve proxy:', err); - } -} - -export function initAutoUpdater(win: BrowserWindow) { - mainWindow = win; - - // Configuration — don't auto-download, let user trigger manually - autoUpdater.autoDownload = false; - autoUpdater.autoInstallOnAppQuit = true; - - // Resolve and apply system proxy for update downloads - configureProxy(); - - // --- Events --- - autoUpdater.on('checking-for-update', () => { - sendStatus({ status: 'checking' }); - }); - - autoUpdater.on('update-available', (info) => { - sendStatus({ - status: 'available', - info: { - version: info.version, - releaseNotes: info.releaseNotes, - releaseName: info.releaseName, - releaseDate: info.releaseDate, - }, - }); - }); - - autoUpdater.on('update-not-available', () => { - sendStatus({ status: 'not-available' }); - }); - - autoUpdater.on('download-progress', (progress) => { - sendStatus({ - status: 'downloading', - progress: { - percent: progress.percent, - bytesPerSecond: progress.bytesPerSecond, - transferred: progress.transferred, - total: progress.total, - }, - }); - }); - - autoUpdater.on('update-downloaded', (info) => { - sendStatus({ - status: 'downloaded', - info: { - version: info.version, - releaseNotes: info.releaseNotes, - releaseName: info.releaseName, - releaseDate: info.releaseDate, - }, - }); - }); - - autoUpdater.on('error', (err) => { - sendStatus({ status: 'error', error: err.message }); - }); - - // --- IPC handlers --- - ipcMain.handle('updater:check', async () => { - return autoUpdater.checkForUpdates(); - }); - - ipcMain.handle('updater:download', async () => { - try { - return await autoUpdater.downloadUpdate(); - } catch (err) { - const message = err instanceof Error ? err.message : String(err); - console.error('[updater] Download failed:', message); - sendStatus({ status: 'error', error: message }); - throw err; - } - }); - - ipcMain.handle('updater:quit-and-install', () => { - autoUpdater.quitAndInstall(); - }); - - // Initial check after 10 seconds - setTimeout(() => { - autoUpdater.checkForUpdates().catch((err) => { - console.warn('[updater] Initial check failed:', err.message); - }); - }, 10_000); - - // Periodic check every 4 hours - setInterval(() => { - autoUpdater.checkForUpdates().catch((err) => { - console.warn('[updater] Periodic check failed:', err.message); - }); - }, 4 * 60 * 60 * 1000); -} - -export function setUpdaterWindow(win: BrowserWindow) { - mainWindow = win; +// eslint-disable-next-line @typescript-eslint/no-unused-vars +export function setUpdaterWindow(_win: BrowserWindow) { + // no-op while native updater is disabled } diff --git a/src/components/layout/AppShell.tsx b/src/components/layout/AppShell.tsx index 33a3810..9f79fd1 100644 --- a/src/components/layout/AppShell.tsx +++ b/src/components/layout/AppShell.tsx @@ -368,9 +368,12 @@ export function AppShell({ children }: { children: React.ReactNode }) { const [checking, setChecking] = useState(false); const [showDialog, setShowDialog] = useState(false); - const isNativeUpdater = typeof window !== "undefined" && !!window.electronAPI?.updater; + // Native auto-updater disabled — always use browser-mode (GitHub API) check. + // Users are directed to download from GitHub Releases. + // TODO: Re-enable native updater after obtaining Apple Developer certificate. + const isNativeUpdater = false; - // --- Browser-mode update check (fallback) --- + // --- Browser-mode update check --- const checkForUpdatesBrowser = useCallback(async () => { setChecking(true); try { @@ -399,130 +402,28 @@ export function AppShell({ children }: { children: React.ReactNode }) { } }, []); - // --- Electron native updater check --- - const checkForUpdatesNative = useCallback(async () => { - setChecking(true); - setUpdateInfo((prev) => prev ? { ...prev, lastError: null } : prev); - try { - await window.electronAPI?.updater?.checkForUpdates(); - } catch { - setChecking(false); - } - }, []); - - const checkForUpdates = isNativeUpdater ? checkForUpdatesNative : checkForUpdatesBrowser; - - // Subscribe to native updater IPC events - useEffect(() => { - if (!isNativeUpdater) return; - - const unsubscribe = window.electronAPI!.updater!.onStatus((event) => { - switch (event.status) { - case 'checking': - setChecking(true); - break; - - case 'available': - setChecking(false); - setUpdateInfo((prev) => { - const releaseNotes = typeof event.info?.releaseNotes === 'string' - ? event.info.releaseNotes - : ''; - const newInfo: UpdateInfo = { - updateAvailable: true, - latestVersion: event.info?.version ?? '', - currentVersion: prev?.currentVersion ?? (process.env.NEXT_PUBLIC_APP_VERSION || '0.0.0'), - releaseName: event.info?.releaseName ?? `v${event.info?.version}`, - releaseNotes, - releaseUrl: `https://github.com/op7418/CodePilot/releases/tag/v${event.info?.version}`, - publishedAt: event.info?.releaseDate ?? '', - downloadProgress: prev?.downloadProgress ?? null, - readyToInstall: prev?.readyToInstall ?? false, - isNativeUpdate: true, - lastError: null, - }; - return newInfo; - }); - // Show dialog if not dismissed - if (event.info?.version) { - const dismissed = localStorage.getItem(DISMISSED_VERSION_KEY); - if (dismissed !== event.info.version) { - setShowDialog(true); - } - } - break; - - case 'not-available': - setChecking(false); - break; - - case 'downloading': - setUpdateInfo((prev) => prev ? { - ...prev, - downloadProgress: event.progress?.percent ?? null, - lastError: null, - } : prev); - break; - - case 'downloaded': - setUpdateInfo((prev) => { - if (!prev) return prev; - return { - ...prev, - readyToInstall: true, - downloadProgress: 100, - lastError: null, - }; - }); - break; - - case 'error': - setChecking(false); - // Reset download progress so the download button re-appears - setUpdateInfo((prev) => prev ? { - ...prev, - downloadProgress: null, - lastError: event.error ?? 'Update failed', - } : prev); - console.warn('[updater] Error:', event.error); - break; - } - }); - - return unsubscribe; - }, [isNativeUpdater]); + // Native updater check disabled — always use browser-mode check + const checkForUpdates = checkForUpdatesBrowser; // Browser mode: check on mount + every 8 hours useEffect(() => { - if (isNativeUpdater) return; checkForUpdatesBrowser(); const id = setInterval(checkForUpdatesBrowser, CHECK_INTERVAL); return () => clearInterval(id); - }, [isNativeUpdater, checkForUpdatesBrowser]); + }, [checkForUpdatesBrowser]); const dismissUpdate = useCallback(() => { setShowDialog(false); }, []); + // Native download/install disabled — no-ops while native updater is disabled. + // The UI shows "View Release" (opens GitHub) instead. const downloadUpdate = useCallback(async () => { - // Immediately show downloading state so user gets feedback - setUpdateInfo((prev) => prev ? { ...prev, downloadProgress: 0, lastError: null } : prev); - try { - await window.electronAPI?.updater?.downloadUpdate(); - } catch (err) { - console.warn('[updater] Download failed:', err); - const message = err instanceof Error ? err.message : String(err); - // Reset progress so the download button re-appears - setUpdateInfo((prev) => prev ? { - ...prev, - downloadProgress: null, - lastError: message, - } : prev); - } + // no-op: native updater disabled }, []); const quitAndInstall = useCallback(() => { - window.electronAPI?.updater?.quitAndInstall(); + // no-op: native updater disabled }, []); const updateContextValue = useMemo(