fix linux desktop packaging paths (#1162)

Co-authored-by: xingzhi <chuzihao.czh@alibaba-inc.com>
This commit is contained in:
sir1st
2026-05-30 20:14:24 +08:00
committed by GitHub
parent 46bd7d0420
commit dcbf601e35
15 changed files with 46 additions and 3 deletions
+1
View File
@@ -31,6 +31,7 @@ should not duplicate server persistence rules.
- `HERMES_WEB_UI_HOME` and `HERMES_WEBUI_STATE_DIR` override Web UI state location. - `HERMES_WEB_UI_HOME` and `HERMES_WEBUI_STATE_DIR` override Web UI state location.
- Hermes Agent state lives under Hermes profile directories and must stay distinct from Web UI state. - Hermes Agent state lives under Hermes profile directories and must stay distinct from Web UI state.
- Uploads default to `config.uploadDir`, which is derived from the Web UI home unless `UPLOAD_DIR` is set. - Uploads default to `config.uploadDir`, which is derived from the Web UI home unless `UPLOAD_DIR` is set.
- Runtime data directories must also live under the Web UI home, not beside built `dist` assets.
- Profile-scoped Hermes data should use existing profile helpers instead of manually joining paths. - Profile-scoped Hermes data should use existing profile helpers instead of manually joining paths.
## Server Structure ## Server Structure
Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 821 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 179 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

+5
View File
@@ -21,6 +21,10 @@ files:
# Web UI source (built dist) and bundled Python live outside the asar. # Web UI source (built dist) and bundled Python live outside the asar.
# This package lives at packages/desktop, so ../.. is the hermes-web-ui repo root. # This package lives at packages/desktop, so ../.. is the hermes-web-ui repo root.
extraResources: extraResources:
- from: "build"
to: "build"
filter:
- "icon.png"
- from: "../.." - from: "../.."
to: "webui" to: "webui"
filter: filter:
@@ -57,6 +61,7 @@ win:
artifactName: "${productName}-${version}-${arch}.${ext}" artifactName: "${productName}-${version}-${arch}.${ext}"
linux: linux:
icon: build/icons
target: target:
- target: AppImage - target: AppImage
arch: [x64, arm64] arch: [x64, arm64]
+2 -1
View File
@@ -1,7 +1,7 @@
import { app, BrowserWindow, Menu, shell, ipcMain } from 'electron' import { app, BrowserWindow, Menu, shell, ipcMain } from 'electron'
import { join } from 'node:path' import { join } from 'node:path'
import { startWebUiServer, stopWebUiServer, getToken } from './webui-server' import { startWebUiServer, stopWebUiServer, getToken } from './webui-server'
import { hermesBinExists, hermesBin } from './paths' import { desktopIcon, hermesBinExists, hermesBin } from './paths'
import { initAutoUpdater } from './updater' import { initAutoUpdater } from './updater'
const PORT = Number(process.env.HERMES_DESKTOP_PORT) || 8748 const PORT = Number(process.env.HERMES_DESKTOP_PORT) || 8748
@@ -18,6 +18,7 @@ function createWindow() {
title: 'Hermes Studio', title: 'Hermes Studio',
backgroundColor: '#1a1a1a', backgroundColor: '#1a1a1a',
autoHideMenuBar: true, autoHideMenuBar: true,
...(process.platform === 'linux' ? { icon: desktopIcon() } : {}),
webPreferences: { webPreferences: {
preload: join(__dirname, '..', 'preload', 'index.js'), preload: join(__dirname, '..', 'preload', 'index.js'),
contextIsolation: true, contextIsolation: true,
+5
View File
@@ -40,6 +40,11 @@ export function hermesBinExists(): boolean {
return existsSync(hermesBin()) return existsSync(hermesBin())
} }
export function desktopIcon(): string {
if (app.isPackaged) return resolve(process.resourcesPath, 'build', 'icon.png')
return resolve(app.getAppPath(), 'build', 'icon.png')
}
export function webUiHome(): string { export function webUiHome(): string {
return process.env.HERMES_WEB_UI_HOME?.trim() || resolve(homedir(), '.hermes-web-ui') return process.env.HERMES_WEB_UI_HOME?.trim() || resolve(homedir(), '.hermes-web-ui')
} }
@@ -147,6 +147,7 @@ export async function startWebUiServer(port = DEFAULT_PORT): Promise<string> {
// macOS/Linux keep the standard ~/.hermes layout. // macOS/Linux keep the standard ~/.hermes layout.
HERMES_HOME: agentHome, HERMES_HOME: agentHome,
HERMES_WEB_UI_HOME: home, HERMES_WEB_UI_HOME: home,
HERMES_WEBUI_STATE_DIR: home,
AUTH_TOKEN: token, AUTH_TOKEN: token,
PORT: String(port), PORT: String(port),
// Prepend bundled Python's bin to PATH so any incidental `python` resolution lands on ours // Prepend bundled Python's bin to PATH so any incidental `python` resolution lands on ours
+6 -1
View File
@@ -14,6 +14,7 @@ import { homedir } from 'os'
* - HERMES_WEBUI_STATE_DIR: Compatibility alias for HERMES_WEB_UI_HOME. * - HERMES_WEBUI_STATE_DIR: Compatibility alias for HERMES_WEB_UI_HOME.
* Default: join(homedir(), '.hermes-web-ui'). * Default: join(homedir(), '.hermes-web-ui').
* - UPLOAD_DIR: Upload directory override. Default: join(HERMES_WEB_UI_HOME, 'upload'). * - UPLOAD_DIR: Upload directory override. Default: join(HERMES_WEB_UI_HOME, 'upload').
* - dataDir: Internal Web UI runtime data directory. Default: join(HERMES_WEB_UI_HOME, 'data').
* *
* Auth: * Auth:
* - AUTH_TOKEN: Explicit bearer token. If unset, Web UI stores an auto-generated token under HERMES_WEB_UI_HOME. * - AUTH_TOKEN: Explicit bearer token. If unset, Web UI stores an auto-generated token under HERMES_WEB_UI_HOME.
@@ -41,6 +42,10 @@ export function getWebUiHome(env: Record<string, string | undefined> = process.e
return appHome ? resolve(appHome) : join(homedir(), '.hermes-web-ui') return appHome ? resolve(appHome) : join(homedir(), '.hermes-web-ui')
} }
export function getWebUiDataDir(env: Record<string, string | undefined> = process.env): string {
return join(getWebUiHome(env), 'data')
}
const appHome = getWebUiHome() const appHome = getWebUiHome()
export const config = { export const config = {
@@ -49,6 +54,6 @@ export const config = {
host: getListenHost(), host: getListenHost(),
appHome, appHome,
uploadDir: process.env.UPLOAD_DIR || join(appHome, 'upload'), uploadDir: process.env.UPLOAD_DIR || join(appHome, 'upload'),
dataDir: resolve(__dirname, '..', 'data'), dataDir: getWebUiDataDir(),
corsOrigins: process.env.CORS_ORIGINS || '*', corsOrigins: process.env.CORS_ORIGINS || '*',
} }
+21
View File
@@ -42,6 +42,7 @@ for (const dir of [
'packages/client/src', 'packages/client/src',
'packages/server/src', 'packages/server/src',
'packages/desktop', 'packages/desktop',
'packages/desktop/build/icons',
'tests/client', 'tests/client',
'tests/server', 'tests/server',
'tests/e2e', 'tests/e2e',
@@ -50,6 +51,21 @@ for (const dir of [
requireDir(dir) requireDir(dir)
} }
for (const icon of [
'packages/desktop/build/icon.png',
'packages/desktop/build/icon.icns',
'packages/desktop/build/icon.ico',
'packages/desktop/build/icons/16x16.png',
'packages/desktop/build/icons/32x32.png',
'packages/desktop/build/icons/48x48.png',
'packages/desktop/build/icons/64x64.png',
'packages/desktop/build/icons/128x128.png',
'packages/desktop/build/icons/256x256.png',
'packages/desktop/build/icons/512x512.png',
]) {
requireFile(icon)
}
const agents = await readText('AGENTS.md') const agents = await readText('AGENTS.md')
const agentLines = agents.trimEnd().split(/\r?\n/) const agentLines = agents.trimEnd().split(/\r?\n/)
if (agentLines.length > 120) { if (agentLines.length > 120) {
@@ -101,10 +117,15 @@ if (!buildWorkflow.includes('npm run harness:check')) {
} }
const desktopReleaseWorkflow = await readText('.github/workflows/desktop-release.yml') const desktopReleaseWorkflow = await readText('.github/workflows/desktop-release.yml')
const electronBuilderConfig = await readText('packages/desktop/electron-builder.yml')
if (!desktopReleaseWorkflow.includes('files: ${{ matrix.artifact_files }}')) { if (!desktopReleaseWorkflow.includes('files: ${{ matrix.artifact_files }}')) {
fail('desktop-release.yml must upload matrix-specific artifact_files') fail('desktop-release.yml must upload matrix-specific artifact_files')
} }
if (!electronBuilderConfig.includes('icon: build/icons')) {
fail('electron-builder.yml must configure the Linux icon set')
}
for (const target of ['target_os: darwin', 'target_os: win32', 'target_os: linux']) { for (const target of ['target_os: darwin', 'target_os: win32', 'target_os: linux']) {
if (!desktopReleaseWorkflow.includes(target)) { if (!desktopReleaseWorkflow.includes(target)) {
fail(`desktop-release.yml is missing matrix target ${target}`) fail(`desktop-release.yml is missing matrix target ${target}`)
+5 -1
View File
@@ -1,7 +1,7 @@
import { describe, expect, it } from 'vitest' import { describe, expect, it } from 'vitest'
import { homedir } from 'os' import { homedir } from 'os'
import { join, resolve } from 'path' import { join, resolve } from 'path'
import { getListenHost, getWebUiHome } from '../../packages/server/src/config' import { getListenHost, getWebUiDataDir, getWebUiHome } from '../../packages/server/src/config'
describe('server config', () => { describe('server config', () => {
it('defaults to an IPv4 bind host', () => { it('defaults to an IPv4 bind host', () => {
@@ -27,4 +27,8 @@ describe('server config', () => {
it('uses HERMES_WEBUI_STATE_DIR as a compatibility alias', () => { it('uses HERMES_WEBUI_STATE_DIR as a compatibility alias', () => {
expect(getWebUiHome({ HERMES_WEBUI_STATE_DIR: ' ./tmp/hermes-state ' })).toBe(resolve('./tmp/hermes-state')) expect(getWebUiHome({ HERMES_WEBUI_STATE_DIR: ' ./tmp/hermes-state ' })).toBe(resolve('./tmp/hermes-state'))
}) })
it('keeps runtime data under the web-ui home', () => {
expect(getWebUiDataDir({ HERMES_WEB_UI_HOME: ' ./tmp/hermes-ui ' })).toBe(resolve('./tmp/hermes-ui/data'))
})
}) })