[codex] fix desktop tray icon and update checks (#1201)

* fix desktop tray icon and update errors

* fix brotlicffi error compatibility

* fix windows installer app shutdown

* fix desktop updater artifact names
This commit is contained in:
ekko
2026-06-01 12:49:03 +08:00
committed by GitHub
parent 022e18dc8f
commit b96bda4834
7 changed files with 108 additions and 15 deletions
+8
View File
@@ -0,0 +1,8 @@
!macro customInit
IfFileExists "$INSTDIR\Hermes Studio.exe" 0 hermesStudioStopDone
DetailPrint "Stopping Hermes Studio..."
nsExec::ExecToLog '"$INSTDIR\Hermes Studio.exe" --quit'
Sleep 5000
nsExec::ExecToLog 'taskkill.exe /IM "Hermes Studio.exe" /T /F'
hermesStudioStopDone:
!macroend
+5 -3
View File
@@ -29,6 +29,7 @@ extraResources:
to: "build"
filter:
- "icon.png"
- "icon.ico"
- "trayTemplate.png"
- from: "../.."
to: "webui"
@@ -60,13 +61,13 @@ mac:
hardenedRuntime: true
gatekeeperAssess: false
notarize: true
artifactName: "${productName}-${version}-${arch}.${ext}"
artifactName: "Hermes.Studio-${version}-${arch}.${ext}"
win:
target:
- target: nsis
arch: [x64]
artifactName: "${productName}-${version}-${arch}.${ext}"
artifactName: "Hermes.Studio-${version}-${arch}.${ext}"
linux:
icon: build/icons
@@ -76,9 +77,10 @@ linux:
- target: deb
arch: [x64] # fpm has no arm64 binary; deb only on x64
category: Development
artifactName: "${productName}-${version}-${arch}.${ext}"
artifactName: "Hermes.Studio-${version}-${arch}.${ext}"
nsis:
oneClick: false
allowToChangeInstallationDirectory: true
perMachine: false
include: build/installer.nsh
@@ -33,6 +33,7 @@ const sitePkgs = process.env.HERMES_AGENT_SITE_PACKAGES ?? (
)
const dtPath = join(sitePkgs, 'gateway', 'platforms', 'dingtalk.py')
const sitecustomizePath = join(sitePkgs, 'sitecustomize.py')
if (!existsSync(dtPath)) {
console.error(`dingtalk.py not found at ${dtPath} — is hermes-agent installed?`)
process.exit(1)
@@ -177,4 +178,31 @@ patch(
if (src !== before) {
writeFileSync(dtPath, src)
}
const brotlicffiCompatMarker = '# patch:brotlicffi-error-compat'
const brotlicffiCompat = `
${brotlicffiCompatMarker}
try:
import brotlicffi as _hermes_brotlicffi
if not hasattr(_hermes_brotlicffi, "error"):
_hermes_brotlicffi.error = (
getattr(_hermes_brotlicffi, "Error", None)
or getattr(_hermes_brotlicffi, "BrotliError", None)
or Exception
)
except Exception:
pass
`
const sitecustomize = existsSync(sitecustomizePath) ? readFileSync(sitecustomizePath, 'utf-8') : ''
if (sitecustomize.includes(brotlicffiCompatMarker)) {
console.log(' · brotlicffi-error-compat (already applied)')
skipped++
} else {
const nextSitecustomize = `${sitecustomize.replace(/\s*$/, '')}\n${brotlicffiCompat.trim()}\n`
writeFileSync(sitecustomizePath, nextSitecustomize)
console.log(' ✓ brotlicffi-error-compat')
applied++
}
console.log(`Done. Applied ${applied}, skipped ${skipped}.`)
+10
View File
@@ -24,6 +24,7 @@ type TranslationKey =
| 'update.later'
| 'update.failedTitle'
| 'update.failedMessage'
| 'update.noUpdateInfoMessage'
| 'update.packagedOnlyMessage'
| 'common.ok'
@@ -52,6 +53,7 @@ const translations: Record<DesktopLocale, Record<TranslationKey, string>> = {
'update.later': 'Later',
'update.failedTitle': 'Update check failed',
'update.failedMessage': 'Could not check for Hermes Studio updates.',
'update.noUpdateInfoMessage': 'Update information is not available for this platform yet.',
'update.packagedOnlyMessage': 'Automatic updates are only available in the packaged desktop app.',
'common.ok': 'OK',
},
@@ -77,6 +79,7 @@ const translations: Record<DesktopLocale, Record<TranslationKey, string>> = {
'update.later': '稍后',
'update.failedTitle': '检查更新失败',
'update.failedMessage': '无法检查 Hermes Studio 更新。',
'update.noUpdateInfoMessage': '当前平台的更新信息暂不可用。',
'update.packagedOnlyMessage': '自动更新仅在打包后的桌面应用中可用。',
'common.ok': '确定',
},
@@ -102,6 +105,7 @@ const translations: Record<DesktopLocale, Record<TranslationKey, string>> = {
'update.later': '稍後',
'update.failedTitle': '檢查更新失敗',
'update.failedMessage': '無法檢查 Hermes Studio 更新。',
'update.noUpdateInfoMessage': '目前平台的更新資訊暫不可用。',
'update.packagedOnlyMessage': '自動更新僅可在打包後的桌面應用中使用。',
'common.ok': '確定',
},
@@ -127,6 +131,7 @@ const translations: Record<DesktopLocale, Record<TranslationKey, string>> = {
'update.later': '後で',
'update.failedTitle': 'アップデート確認に失敗しました',
'update.failedMessage': 'Hermes Studio のアップデートを確認できませんでした。',
'update.noUpdateInfoMessage': 'このプラットフォームのアップデート情報はまだ利用できません。',
'update.packagedOnlyMessage': '自動アップデートはパッケージ版デスクトップアプリでのみ利用できます。',
'common.ok': 'OK',
},
@@ -152,6 +157,7 @@ const translations: Record<DesktopLocale, Record<TranslationKey, string>> = {
'update.later': '나중에',
'update.failedTitle': '업데이트 확인 실패',
'update.failedMessage': 'Hermes Studio 업데이트를 확인할 수 없습니다.',
'update.noUpdateInfoMessage': '이 플랫폼의 업데이트 정보를 아직 사용할 수 없습니다.',
'update.packagedOnlyMessage': '자동 업데이트는 패키징된 데스크톱 앱에서만 사용할 수 있습니다.',
'common.ok': '확인',
},
@@ -177,6 +183,7 @@ const translations: Record<DesktopLocale, Record<TranslationKey, string>> = {
'update.later': 'Plus tard',
'update.failedTitle': 'Echec de la recherche de mise a jour',
'update.failedMessage': 'Impossible de rechercher les mises a jour de Hermes Studio.',
'update.noUpdateInfoMessage': 'Les informations de mise a jour ne sont pas encore disponibles pour cette plateforme.',
'update.packagedOnlyMessage': 'Les mises a jour automatiques ne sont disponibles que dans l application de bureau packagee.',
'common.ok': 'OK',
},
@@ -202,6 +209,7 @@ const translations: Record<DesktopLocale, Record<TranslationKey, string>> = {
'update.later': 'Mas tarde',
'update.failedTitle': 'Error al buscar actualizaciones',
'update.failedMessage': 'No se pudieron buscar actualizaciones de Hermes Studio.',
'update.noUpdateInfoMessage': 'La informacion de actualizacion aun no esta disponible para esta plataforma.',
'update.packagedOnlyMessage': 'Las actualizaciones automaticas solo estan disponibles en la app de escritorio empaquetada.',
'common.ok': 'Aceptar',
},
@@ -227,6 +235,7 @@ const translations: Record<DesktopLocale, Record<TranslationKey, string>> = {
'update.later': 'Spater',
'update.failedTitle': 'Update-Prufung fehlgeschlagen',
'update.failedMessage': 'Updates fur Hermes Studio konnten nicht gepruft werden.',
'update.noUpdateInfoMessage': 'Update-Informationen sind fur diese Plattform noch nicht verfugbar.',
'update.packagedOnlyMessage': 'Automatische Updates sind nur in der paketierten Desktop-App verfugbar.',
'common.ok': 'OK',
},
@@ -252,6 +261,7 @@ const translations: Record<DesktopLocale, Record<TranslationKey, string>> = {
'update.later': 'Depois',
'update.failedTitle': 'Falha ao verificar atualizacoes',
'update.failedMessage': 'Nao foi possivel verificar atualizacoes do Hermes Studio.',
'update.noUpdateInfoMessage': 'As informacoes de atualizacao ainda nao estao disponiveis para esta plataforma.',
'update.packagedOnlyMessage': 'Atualizacoes automaticas estao disponiveis apenas no app desktop empacotado.',
'common.ok': 'OK',
},
+19 -5
View File
@@ -1,12 +1,13 @@
import { app, BrowserWindow, Menu, Tray, shell, ipcMain, nativeImage } from 'electron'
import { join } from 'node:path'
import { startWebUiServer, stopWebUiServer, getToken } from './webui-server'
import { desktopIcon, desktopTrayTemplateIcon, hermesBinExists, hermesBin } from './paths'
import { desktopIcon, desktopTrayTemplateIcon, desktopWindowsTrayIcon, hermesBinExists, hermesBin } from './paths'
import { checkForDesktopUpdates, initAutoUpdater } from './updater'
import { t } from './desktop-i18n'
const PORT = Number(process.env.HERMES_DESKTOP_PORT) || 8748
const START_HIDDEN = process.argv.includes('--hidden')
const QUIT_EXISTING = process.argv.includes('--quit')
let mainWindow: BrowserWindow | null = null
let serverUrl: string | null = null
@@ -90,10 +91,14 @@ function updateTrayMenu() {
function createTray() {
if (tray) return
const source = process.platform === 'darwin' ? desktopTrayTemplateIcon() : desktopIcon()
const source = process.platform === 'darwin'
? desktopTrayTemplateIcon()
: process.platform === 'win32'
? desktopWindowsTrayIcon()
: desktopIcon()
const icon = nativeImage.createFromPath(source).resize({
width: process.platform === 'darwin' ? 18 : 16,
height: process.platform === 'darwin' ? 18 : 16,
width: process.platform === 'darwin' ? 18 : process.platform === 'win32' ? 20 : 16,
height: process.platform === 'darwin' ? 18 : process.platform === 'win32' ? 20 : 16,
quality: 'best',
})
if (process.platform === 'darwin') {
@@ -205,11 +210,20 @@ const gotLock = app.requestSingleInstanceLock()
if (!gotLock) {
app.quit()
} else {
app.on('second-instance', () => {
app.on('second-instance', (_event, argv) => {
if (argv.includes('--quit')) {
quitApp()
return
}
showMainWindow()
})
app.whenReady().then(() => {
if (QUIT_EXISTING) {
quitApp()
return
}
// Drop the default File/Edit/View/Window menu on Windows/Linux. The web
// UI provides its own in-page controls, so the native menu bar is just
// visual clutter. macOS keeps a menu (system requirement) but Electron's
+5
View File
@@ -45,6 +45,11 @@ export function desktopIcon(): string {
return resolve(app.getAppPath(), 'build', 'icon.png')
}
export function desktopWindowsTrayIcon(): string {
if (app.isPackaged) return resolve(process.resourcesPath, 'build', 'icon.ico')
return resolve(app.getAppPath(), 'build', 'icon.ico')
}
export function desktopTrayTemplateIcon(): string {
if (app.isPackaged) return resolve(process.resourcesPath, 'build', 'trayTemplate.png')
return resolve(app.getAppPath(), 'build', 'trayTemplate.png')
+33 -7
View File
@@ -13,6 +13,13 @@ interface GitHubRelease {
tag_name?: string
}
class MissingUpdateInfoError extends Error {
constructor(public readonly url: string) {
super(`Update information is not available at ${url}`)
this.name = 'MissingUpdateInfoError'
}
}
interface AutoUpdaterOptions {
beforeQuitAndInstall?: () => void
}
@@ -31,14 +38,34 @@ async function getLatestReleaseTag(): Promise<string> {
const release = await res.json() as GitHubRelease
const tag = release.tag_name?.trim()
if (!tag) throw new Error('Latest release response did not include a tag')
return tag.startsWith('v') ? tag : `v${tag}`
return tag
}
function updateManifestFile(): string {
if (process.platform === 'darwin') return 'latest-mac.yml'
if (process.platform === 'win32') return 'latest.yml'
return 'latest-linux.yml'
}
async function assertUpdateManifestExists(feedUrl: string): Promise<void> {
const manifestUrl = `${feedUrl}/${updateManifestFile()}`
const res = await fetch(manifestUrl, {
method: 'HEAD',
headers: {
'User-Agent': `Hermes-Studio/${app.getVersion()}`,
},
})
if (res.status === 404) throw new MissingUpdateInfoError(manifestUrl)
if (!res.ok) throw new Error(`Update feed returned ${res.status}`)
}
async function configureFeedFromLatestRelease(): Promise<void> {
const tag = await getLatestReleaseTag()
const feedUrl = `${CLOUDFLARE_DOWNLOAD_BASE_URL}/${tag}`
await assertUpdateManifestExists(feedUrl)
autoUpdater.setFeedURL({
provider: 'generic',
url: `${CLOUDFLARE_DOWNLOAD_BASE_URL}/${tag}`,
url: feedUrl,
})
}
@@ -54,12 +81,11 @@ function showUpToDate(info?: UpdateInfo) {
}
function showUpdateCheckFailed(err: unknown) {
const detail = err instanceof Error ? err.message : String(err)
const isMissingUpdateInfo = err instanceof MissingUpdateInfoError
dialog.showMessageBox({
type: 'error',
title: t('update.failedTitle'),
message: t('update.failedMessage'),
detail,
type: isMissingUpdateInfo ? 'info' : 'error',
title: isMissingUpdateInfo ? t('update.upToDateTitle') : t('update.failedTitle'),
message: isMissingUpdateInfo ? t('update.noUpdateInfoMessage') : t('update.failedMessage'),
buttons: [t('common.ok')],
}).catch(() => undefined)
}