refactor(desktop): 替换 Electrobun 为 WebUI 作为桌面窗口方案

Electrobun 太不稳定,改用 webui-dev/webui(轻量 C 库,~300KB)通过
系统浏览器或 WebView 提供桌面窗口。已验证 bun:ffi 加载和
bun build --compile 均正常工作。

- 移除 electrobun 依赖和配置
- 添加 @webui-dev/bun-webui 依赖
- 重写桌面入口为 WebUI 窗口方案
- 移除 Conveyor 打包工具(mise.toml)
This commit is contained in:
2026-02-08 04:15:34 +08:00
parent 41d97ca312
commit e8e473b357
9 changed files with 51 additions and 445 deletions

View File

@@ -1,35 +1,15 @@
import { createServer } from 'node:net'
import { dirname, join } from 'node:path'
import { BrowserWindow, PATHS } from 'electrobun/bun'
import { WebUI } from '@webui-dev/bun-webui'
const DEV_SERVER_URL = 'http://localhost:3000'
const SERVER_READY_TIMEOUT_MS = 5_000
const MAX_SPAWN_RETRIES = 3
const SERVER_READY_TIMEOUT_MS = 10_000
const isDev = (): boolean => {
const env = process.env.ELECTROBUN_BUILD_ENV
return !env || env === 'dev'
}
const getServerEntryPath = (): string => {
return join(PATHS.VIEWS_FOLDER, '..', 'server-output', 'server', 'index.mjs')
}
const getFreePort = (): Promise<number> => {
return new Promise((resolve, reject) => {
const srv = createServer()
srv.unref()
srv.once('error', reject)
srv.listen({ port: 0, host: '127.0.0.1', exclusive: true }, () => {
const addr = srv.address()
if (addr && typeof addr === 'object') {
const port = addr.port
srv.close((err) => (err ? reject(err) : resolve(port)))
} else {
srv.close(() => reject(new Error('Unexpected address() result')))
}
})
})
const isServerReady = async (url: string): Promise<boolean> => {
try {
const response = await fetch(url, { method: 'HEAD' })
return response.ok
} catch {
return false
}
}
const waitForServer = async (
@@ -38,126 +18,35 @@ const waitForServer = async (
): Promise<boolean> => {
const start = Date.now()
while (Date.now() - start < timeoutMs) {
try {
const response = await fetch(url, { method: 'HEAD' })
if (response.ok) return true
} catch (_) {
// Server not up yet, retry after sleep
}
if (await isServerReady(url)) return true
await Bun.sleep(100)
}
return false
}
const spawnServer = async (): Promise<{
process: ReturnType<typeof Bun.spawn>
url: string
}> => {
const serverEntryPath = getServerEntryPath()
const serverDir = dirname(serverEntryPath)
for (let attempt = 1; attempt <= MAX_SPAWN_RETRIES; attempt++) {
const port = await getFreePort()
const url = `http://127.0.0.1:${port}`
const serverProc = Bun.spawn([process.execPath, serverEntryPath], {
cwd: serverDir,
env: {
...process.env,
PORT: String(port),
HOST: '127.0.0.1',
},
stdio: ['ignore', 'inherit', 'inherit'],
})
const ready = await Promise.race([
waitForServer(url),
serverProc.exited.then((code) => {
throw new Error(`Server exited with code ${code} before becoming ready`)
}),
])
if (ready) {
return { process: serverProc, url }
}
serverProc.kill()
await serverProc.exited
console.warn(
`Server failed to become ready on port ${port} (attempt ${attempt}/${MAX_SPAWN_RETRIES})`,
)
}
throw new Error(`Server failed to start after ${MAX_SPAWN_RETRIES} attempts`)
}
const main = async () => {
console.log('Starting Furtherverse Desktop...')
let serverUrl: string
let serverProcess: ReturnType<typeof Bun.spawn> | null = null
if (isDev()) {
console.log('Dev mode: waiting for external server at', DEV_SERVER_URL)
const ready = await waitForServer(DEV_SERVER_URL)
if (!ready) {
console.error(
'Dev server not responding. Make sure to run: bun dev in apps/server',
)
process.exit(1)
}
console.log('Dev server ready!')
serverUrl = DEV_SERVER_URL
} else {
console.log('Production mode: starting embedded server...')
try {
const server = await spawnServer()
serverProcess = server.process
serverUrl = server.url
console.log('Server ready at', serverUrl)
} catch (err) {
console.error('Failed to start embedded server:', err)
process.exit(1)
}
console.log('Waiting for server at', DEV_SERVER_URL)
const ready = await waitForServer(DEV_SERVER_URL)
if (!ready) {
console.error(
'Dev server not responding. Run `bun dev` in apps/server first.',
)
process.exit(1)
}
new BrowserWindow({
title: 'Furtherverse',
url: serverUrl,
frame: {
x: 100,
y: 100,
width: 1200,
height: 800,
},
renderer: 'cef',
})
console.log('Opening window at', DEV_SERVER_URL)
if (serverProcess) {
const cleanup = () => {
if (serverProcess) {
serverProcess.kill()
serverProcess = null
}
}
const win = new WebUI()
win.setSize(1200, 800)
win.setCenter()
await win.show(DEV_SERVER_URL)
process.on('exit', cleanup)
process.on('SIGTERM', () => {
cleanup()
process.exit(0)
})
process.on('SIGINT', () => {
cleanup()
process.exit(0)
})
console.log('Window opened. Waiting for close...')
await WebUI.wait()
serverProcess.exited.then((code) => {
if (serverProcess) {
console.error(`Server exited unexpectedly with code ${code}`)
process.exit(1)
}
})
}
process.exit(0)
}
main().catch((error) => {