[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:
@@ -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
|
||||
@@ -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}.`)
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user