Phase 004: remove the desktop shell, website tooling, and bundled hermes CLI

- Drop all desktop/Electron branches, scripts, and UI (title bar, pets,
  desktop bridge); LAN endpoint kind is now web/custom only
- Replace the bundled hermes CLI with user-side installation: add
  GET /api/hermes/cli-status plus a HermesMissingView onboarding guide
- Remove the version-management UI (snapshots handled by git) and prune
  dead website/mcu/claude scripts and lockfile entries
This commit is contained in:
2026-10-16 15:33:56 +08:00
parent 8c1ddf415f
commit 4701bf0ad0
49 changed files with 354 additions and 2403 deletions
+3 -101
View File
@@ -1,5 +1,4 @@
import Koa from 'koa'
import type { Context } from 'koa'
import cors from '@koa/cors'
import bodyParser from '@koa/bodyparser'
import serve from 'koa-static'
@@ -19,7 +18,7 @@ import { setGroupChatServer } from './routes/hermes/group-chat'
import { setChatRunServer } from './routes/hermes/chat-run'
import { GroupChatServer } from './services/hermes/group-chat'
import { ChatRunSocket } from './services/hermes/run-chat'
import { getAgentBridgeManager, startAgentBridgeManager } from './services/hermes/agent-bridge'
import { startAgentBridgeManager } from './services/hermes/agent-bridge'
import { HermesSkillInjector } from './services/hermes/skill-injector'
import { injectBundledMcpServer } from './services/hermes/studio-mcp-autoinject'
import { ensureProfileGatewaysRunning } from './services/hermes/gateway-autostart'
@@ -33,7 +32,6 @@ import { logger } from './services/logger'
import { createStaticCompressionMiddleware } from './middleware/static-compression'
import { requireUserJwt, resolveUserProfile } from './middleware/user-auth'
import { createCorsOriginResolver, securityHeaders } from './security'
import type { ShutdownHandler } from './services/shutdown'
// Injected by esbuild at build time; fallback to reading package.json in dev mode
declare const __APP_VERSION__: string
@@ -61,7 +59,6 @@ let chatRunServer: any = null
let workflowSocketServer: WorkflowSocketServer | null = null
let petStateSocketServer: PetStateSocketServer | null = null
let agentBridgeManager: any = null
let desktopShutdownHandler: ShutdownHandler | null = null
interface ListenResult {
primary: any
@@ -101,65 +98,6 @@ function safeNetworkInterfaces() {
}
}
function isDesktopRuntime(): boolean {
return String(process.env.HERMES_DESKTOP || '').trim().toLowerCase() === 'true'
}
function isLoopbackAddress(address?: string | null): boolean {
if (!address) return false
return address === '127.0.0.1'
|| address === '::1'
|| address === '::ffff:127.0.0.1'
|| address.startsWith('::ffff:127.')
}
function bearerToken(ctx: Context): string {
const header = ctx.get('authorization')
const match = header.match(/^Bearer\s+(.+)$/i)
return match?.[1]?.trim() || ''
}
function registerDesktopShutdownRoute(app: Koa): void {
app.use(async (ctx, next) => {
if (ctx.method !== 'POST' || ctx.path !== '/api/desktop/shutdown') {
await next()
return
}
if (!isDesktopRuntime()) {
ctx.status = 404
ctx.body = { error: 'not_found' }
return
}
const remoteAddress = ctx.req.socket.remoteAddress
if (!isLoopbackAddress(remoteAddress)) {
ctx.status = 403
ctx.body = { error: 'forbidden' }
return
}
const expectedToken = String(process.env.AUTH_TOKEN || '').trim()
if (!expectedToken || bearerToken(ctx) !== expectedToken) {
ctx.status = 401
ctx.body = { error: 'unauthorized' }
return
}
if (!desktopShutdownHandler) {
ctx.status = 503
ctx.body = { error: 'shutdown_not_ready' }
return
}
ctx.status = 202
ctx.body = { ok: true }
setTimeout(() => {
void desktopShutdownHandler?.('desktop-api')
}, 50).unref?.()
})
}
function envFlagEnabled(name: string): boolean {
const value = String(process.env[name] || '').trim().toLowerCase()
return ['1', 'true', 'yes', 'on'].includes(value)
@@ -194,33 +132,6 @@ async function startRuntimeServicesBeforeListen(): Promise<void> {
}
}
function startRuntimeServicesAfterListen(): void {
if (gatewayAutostartDisabled()) {
console.log('[bootstrap] profile gateway check disabled by HERMES_WEB_UI_DISABLE_GATEWAY_AUTOSTART')
} else {
void (async () => {
try {
await ensureProfileGatewaysRunning()
console.log('[bootstrap] profile gateways checked')
} catch (err) {
logger.warn(err, '[bootstrap] failed to ensure profile gateways')
console.warn('[bootstrap] failed to ensure profile gateways:', err instanceof Error ? err.message : err)
}
})()
}
void (async () => {
try {
agentBridgeManager = await startAgentBridgeManager()
console.log('[bootstrap] agent bridge started')
} catch (err) {
logger.warn(err, '[bootstrap] agent bridge failed to start')
console.warn('[bootstrap] agent bridge failed to start:', err instanceof Error ? err.message : err)
return
}
})()
}
function startLanDiscovery(): void {
const discoverySocket = startLanDiscoveryResponder({ httpPort: config.port })
let initialScanStarted = false
@@ -278,9 +189,7 @@ export async function bootstrap() {
console.warn('[bootstrap] failed to inject bundled MCP server:', err instanceof Error ? err.message : err)
}
if (!isDesktopRuntime()) {
await startRuntimeServicesBeforeListen()
}
await startRuntimeServicesBeforeListen()
const app = new Koa()
// Initialize all web-ui SQLite tables
@@ -301,8 +210,6 @@ export async function bootstrap() {
}))
console.log('[bootstrap] cors + bodyParser registered')
registerDesktopShutdownRoute(app)
// Register all routes (handles auth internally)
registerRoutes(app, [requireUserJwt, resolveUserProfile])
console.log('[bootstrap] routes registered')
@@ -379,11 +286,6 @@ export async function bootstrap() {
startLanDiscovery()
refreshConfiguredProviderModelCatalogsInBackground('bootstrap')
if (isDesktopRuntime()) {
agentBridgeManager = getAgentBridgeManager()
startRuntimeServicesAfterListen()
}
// Restore group chat agents after server is ready.
groupChatServer.restoreWhenReady()
@@ -394,7 +296,7 @@ export async function bootstrap() {
})
})
desktopShutdownHandler = bindShutdown(servers, groupChatServer, chatRunServer, agentBridgeManager)
bindShutdown(servers, groupChatServer, chatRunServer, agentBridgeManager)
startVersionCheck()
}