fix desktop preload build and rename app (#1150)

* fix desktop preload fetch typing

* rename desktop app to Hermes Studio

* rename desktop package to Hermes Studio

* update Hermes Studio desktop icons

* configure desktop signing and app id

* force desktop api calls to local server

* isolate desktop agent bridge ports

* bundle MCP support in desktop Python

* change desktop default port to 8748

* restore webui production port copy
This commit is contained in:
ekko
2026-05-30 15:59:52 +08:00
committed by GitHub
parent cbae8e8c3f
commit f7991572af
14 changed files with 89 additions and 25 deletions
+4 -4
View File
@@ -4,7 +4,7 @@ import { startWebUiServer, stopWebUiServer, getToken } from './webui-server'
import { hermesBinExists, hermesBin } from './paths'
import { initAutoUpdater } from './updater'
const PORT = Number(process.env.HERMES_DESKTOP_PORT) || 8648
const PORT = Number(process.env.HERMES_DESKTOP_PORT) || 8748
let mainWindow: BrowserWindow | null = null
let serverUrl: string | null = null
@@ -15,7 +15,7 @@ function createWindow() {
height: 820,
minWidth: 960,
minHeight: 600,
title: 'Hermes Desktop',
title: 'Hermes Studio',
backgroundColor: '#1a1a1a',
autoHideMenuBar: true,
webPreferences: {
@@ -46,7 +46,7 @@ function createWindow() {
}
function splashHtml(): string {
const html = `<!doctype html><html><head><meta charset="utf-8"><title>Hermes Desktop</title>
const html = `<!doctype html><html><head><meta charset="utf-8"><title>Hermes Studio</title>
<style>
html,body{margin:0;height:100%;background:#1a1a1a;color:#e5e5e5;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Helvetica,Arial,sans-serif;}
.wrap{display:flex;flex-direction:column;align-items:center;justify-content:center;height:100%;gap:24px}
@@ -57,7 +57,7 @@ function splashHtml(): string {
.label{font-size:14px;color:#999}
h1{font-weight:500;margin:0;font-size:18px}
</style></head><body><div class="wrap">
<h1>Hermes Desktop</h1>
<h1>Hermes Studio</h1>
<div class="row"><div class="dot"></div><div class="dot"></div><div class="dot"></div></div>
<div class="label">Starting local services…</div>
</div></body></html>`
+1 -1
View File
@@ -26,7 +26,7 @@ export function initAutoUpdater() {
const { response } = await dialog.showMessageBox({
type: 'info',
title: 'Update ready',
message: `Hermes Desktop ${info.version} is ready to install.`,
message: `Hermes Studio ${info.version} is ready to install.`,
detail: 'Restart now to apply the update, or it will be installed on next quit.',
buttons: ['Restart now', 'Later'],
defaultId: 0,
+43 -2
View File
@@ -1,11 +1,12 @@
import { ChildProcess, spawn } from 'node:child_process'
import { mkdirSync, readFileSync, writeFileSync, chmodSync, existsSync } from 'node:fs'
import { createServer } from 'node:net'
import { dirname, delimiter, join } from 'node:path'
import { randomBytes } from 'node:crypto'
import { app } from 'electron'
import { webuiServerEntry, webuiDir, hermesBin, webUiHome, hermesHome, tokenFile, pythonDir } from './paths'
const DEFAULT_PORT = 8648
const DEFAULT_PORT = 8748
const READY_TIMEOUT_MS = 30_000
let serverProc: ChildProcess | null = null
@@ -50,6 +51,43 @@ export function getServerUrl(port = DEFAULT_PORT): string {
return `http://127.0.0.1:${port}`
}
async function getFreeTcpPort(): Promise<number> {
return await new Promise((resolveFreePort, rejectFreePort) => {
const server = createServer()
server.unref()
server.once('error', rejectFreePort)
server.listen(0, '127.0.0.1', () => {
const address = server.address()
server.close(() => {
if (typeof address === 'object' && address?.port) {
resolveFreePort(address.port)
} else {
rejectFreePort(new Error('Unable to allocate local TCP port'))
}
})
})
})
}
async function canBindTcpPort(port: number): Promise<boolean> {
return await new Promise((resolveCanBind) => {
const server = createServer()
server.unref()
server.once('error', () => resolveCanBind(false))
server.listen(port, '127.0.0.1', () => {
server.close(() => resolveCanBind(true))
})
})
}
async function getFreeTcpPortInRange(min: number, max: number): Promise<number> {
for (let attempt = 0; attempt < 100; attempt += 1) {
const port = min + (randomBytes(2).readUInt16BE(0) % (max - min + 1))
if (await canBindTcpPort(port)) return port
}
return getFreeTcpPort()
}
export async function startWebUiServer(port = DEFAULT_PORT): Promise<string> {
ensureNativeModules()
const token = ensureToken()
@@ -71,6 +109,8 @@ export async function startWebUiServer(port = DEFAULT_PORT): Promise<string> {
const bundledPython = isWin
? join(pythonDir(), 'python.exe')
: join(pythonDir(), 'bin', 'python3')
const bridgePort = await getFreeTcpPort()
const workerPortBase = await getFreeTcpPortInRange(20000, 59000)
// Run via Electron's "run as Node" mode — Electron binary doubles as Node.
const env: NodeJS.ProcessEnv = {
@@ -85,10 +125,11 @@ export async function startWebUiServer(port = DEFAULT_PORT): Promise<string> {
// unix socket is rejected on macOS in some EDR/sandbox setups (silent
// SIGKILL of the bridge child within ~150ms). TCP on 127.0.0.1 works
// identically and avoids the issue cross-platform.
HERMES_AGENT_BRIDGE_ENDPOINT: 'tcp://127.0.0.1:18765',
HERMES_AGENT_BRIDGE_ENDPOINT: `tcp://127.0.0.1:${bridgePort}`,
// Force TCP for worker endpoints too (upstream #1106). Same EDR/sandbox
// reason as above — default ipc:// unix sockets in /tmp get killed.
HERMES_AGENT_BRIDGE_WORKER_TRANSPORT: 'tcp',
HERMES_AGENT_BRIDGE_WORKER_PORT_BASE: String(workerPortBase),
// And for preview-mode bridges spawned by the in-app update controller.
HERMES_WEB_UI_PREVIEW_AGENT_BRIDGE_TRANSPORT: 'tcp',
// Suppress the npm-registry update prompt (upstream #1105). hermes-web-ui