b4a80aceeb
* fix: comprehensive Windows compatibility and gateway management improvements This commit addresses multiple Windows compatibility issues and improves gateway management across all platforms. ## Windows Compatibility Fixes - Add hermes-path.ts with cross-platform Hermes home/bin detection - Fix Windows native installation paths (%LOCALAPPDATA%\hermes) - Update terminal.ts to use PowerShell instead of /bin/bash on Windows - Fix upload.ts path construction to use path.join() for cross-platform paths - Fix download.ts to use isAbsolute() for Windows absolute path detection - Update auth.ts to skip file mode 0o600 on Windows (unsupported) - Add nodemon.json for cross-platform environment variable handling ## Gateway Management Improvements - Simplify gateway startup: all platforms use 'run' mode uniformly - Remove complex init system detection and platform-specific code paths - Improve PID file validation: use health check instead of port detection - Remove getPortByPid() method (too complex and error-prone) - Remove checkPortAvailable() TCP bind test (TIME_WAIT false positives) - Trust gateway --replace flag to handle real port conflicts - Add smart PID validation: check if stale process via health check - Fix port allocation to avoid incrementing when gateway restarts - Add allocatedPorts.clear() on each startAll() call - Add clearPidFile() method to clean up stale PID files ## Process Management - Remove detached:true and unref() from gateway spawn - Gateway processes now follow parent process lifecycle - Add process reference storage in ManagedGateway interface - Improve shutdown logic: call gatewayManager.stopAll() before exit - Fix Windows process killing: use process.kill(pid) for Windows - Remove PowerShell command for lock file cleanup (use Node.js fs.unlinkSync) ## Frontend Theme Fixes - Fix main.ts localStorage key mismatch (hermes_theme → hermes_brightness) - Add inline script in index.html to prevent FOUC (Flash of Unstyled Content) - Apply theme classes before Vue mount to avoid visual glitches ## Developer Experience - Fix nodemon windows-kill popup on Windows by removing signal config - Add delay and environment variables to nodemon.json - Add windowsHide: true to all child process spawns ## Breaking Changes - Gateway management now exclusively uses 'run' mode on all platforms - systemd/launchd integration removed (use --replace flag instead) This fix ensures hermes-web-ui works correctly on Windows native installations while maintaining compatibility with Linux/macOS/WSL2. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Fix gateway lifecycle port handling * fix: comprehensive Windows compatibility and gateway management improvements - Simplified hermes CLI binary resolution logic - Fixed Windows line ending compatibility in profile list parsing - Migrated gateway restart logic from CLI to GatewayManager - Added gateway restart to updateCredentials method - Removed unnecessary gateway restarts from provider operations - Fixed configuration preservation when switching profiles - Added nodemon quiet mode and legacy watch to reduce Windows popups Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * revert: change back to nodemon due to tsx compatibility issues - tsx has compatibility issues with Koa generator functions - Restored nodemon with simplified configuration - Added cross-env package for future Windows environment variable needs Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat: replace nodemon with ts-node-dev to eliminate Windows popup windows - Installed ts-node-dev as nodemon replacement - ts-node-dev has better Windows compatibility without console popups - Supports respawning, inspector debugging, and TypeScript compilation - Uses cross-env for Windows environment variable support - Removed nodemon.json configuration file (no longer needed) Benefits: - No more Windows console popup windows during development - Faster restart times compared to nodemon - Built-in TypeScript compilation without ts-node overhead Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix: improve log parsing and Windows compatibility for agent/error logs - Fixed Pino JSON log parsing bug where logger field incorrectly used obj.msg - Changed logger field to use obj.name to properly display log source - Added Windows line ending support (\r\n) for log file listing - Added support for 'error' log type in addition to 'errors' - Improved error message extraction from obj.err when available This fixes the missing agent and error logs issue on Windows. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Fix gateway health checks and shutdown ownership * Refine auth lock window and dev shutdown * fix: improve Hermes plugin discovery on Windows by fixing Python path resolution - Added support for Windows venv Scripts directory structure - Fixed Python executable path detection for hermes.exe in venv/Scripts/ - Added Windows LOCALAPPDATA hermes-agent directory to search paths - Improved cross-platform compatibility for plugin discovery This fixes the "No module named 'hermes_cli'" error on Windows by correctly locating the Python virtual environment that contains the Hermes modules. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * refactor: improve cross-platform compatibility for Hermes plugin discovery - Added platform detection to only add Windows-specific paths on Windows - Prevents potential issues on Unix/Linux/macOS systems - Ensures LOCALAPPDATA path is only used when available on Windows - Maintains existing behavior for all platforms This makes the Windows plugin discovery fix safer for cross-platform usage. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * chore: remove unused development dependencies - Removed nodemon (replaced by ts-node-dev) - Removed tsx (had compatibility issues with Koa) - Removed nodemon.json configuration file - Cleaned up development tools to only what's actually used This reduces dependency size and eliminates the windows-kill popup source that was part of nodemon. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * chore: remove memory system files - Removed MEMORY.md index file - Removed memory/ directory and windows-compatibility.md - Cleaned up unused memory persistence system Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix: resolve TypeScript compilation error in plugins.ts - Added type assertion 'as string[]' after filter(Boolean) - Fixes TS2769 error: No overload matches this call - Ensures type compatibility with hasHermesPluginModule function Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix: comprehensive Windows compatibility and gateway management improvements - Fix gateway detection after nodemon restart by adding health check-based detection - Prevent port conflicts by detecting already-running gateways without PID files - Switch to serial gateway startup to avoid lock file race conditions - Return to nodemon from ts-node-dev for development stability - Always stop gateways on shutdown to prevent orphan processes - Prevent project root config files from being committed to git - Fix syntax issues in plugins.ts Resolves issues where default profile gateway failed to start after nodemon restart and gateways were incorrectly marked as stopped despite running on correct ports. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat: comic theme multilingual fonts, sidebar collapse fix, plugin discovery for Termux, and cron history - Add Chinese (ZCOOL KuaiLe), Japanese (Zen Maru Gothic), Korean (Gaegu) handwritten fonts for Comic theme - Fix collapsed sidebar: hide language switch, stack theme icons vertically - Add hermes shebang parsing as fallback Python discovery for Termux - Remove cron source filter from history sessions - Add 0.5.17 changelog entries for all 8 locales Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix: tolerate duplicate YAML keys in config parsing (closes #628) Add `{ json: true }` to all 7 `yaml.load()` calls so duplicated mapping keys (e.g. multiple `mcp_servers:` blocks) no longer crash the API. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix: gateway ownership check requires PID file to prevent cross-profile port hijacking Remove fallback that assumed ownership of healthy gateways without PID verification. Now only claims a gateway if PID file exists and process is alive, preventing one profile from hijacking another's port. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
194 lines
6.9 KiB
TypeScript
194 lines
6.9 KiB
TypeScript
import Koa from 'koa'
|
||
import cors from '@koa/cors'
|
||
import bodyParser from '@koa/bodyparser'
|
||
import serve from 'koa-static'
|
||
import send from 'koa-send'
|
||
import os from 'os'
|
||
import { resolve } from 'path'
|
||
import { mkdir } from 'fs/promises'
|
||
import { readFileSync } from 'fs'
|
||
import { config } from './config'
|
||
import { getToken, requireAuth } from './services/auth'
|
||
import { initLoginLimiter } from './services/login-limiter'
|
||
import { initGatewayManager, getGatewayManagerInstance } from './services/gateway-bootstrap'
|
||
import { bindShutdown } from './services/shutdown'
|
||
import { setupTerminalWebSocket } from './routes/hermes/terminal'
|
||
import { startVersionCheck } from './routes/health'
|
||
import { registerRoutes } from './routes'
|
||
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/chat-run-socket'
|
||
import { logger } from './services/logger'
|
||
|
||
// Injected by esbuild at build time; fallback to reading package.json in dev mode
|
||
declare const __APP_VERSION__: string
|
||
const APP_VERSION = typeof __APP_VERSION__ !== 'undefined'
|
||
? __APP_VERSION__
|
||
: (() => { try { return JSON.parse(readFileSync(resolve(__dirname, '../../package.json'), 'utf-8')).version } catch { return 'dev' } })()
|
||
|
||
// Global error handlers
|
||
process.on('uncaughtException', (err) => {
|
||
console.error('FATAL: Uncaught exception')
|
||
console.error(err)
|
||
logger.fatal(err, 'Uncaught exception')
|
||
process.exit(1)
|
||
})
|
||
|
||
process.on('unhandledRejection', (reason) => {
|
||
console.error('FATAL: Unhandled rejection')
|
||
console.error(reason)
|
||
logger.error(reason, 'Unhandled rejection')
|
||
process.exit(1)
|
||
})
|
||
|
||
let server: any = null
|
||
let servers: any[] = []
|
||
let chatRunServer: any = null
|
||
|
||
interface ListenResult {
|
||
primary: any
|
||
servers: any[]
|
||
}
|
||
|
||
function listen(app: Koa, port: number, host: string): Promise<any> {
|
||
return new Promise((resolve, reject) => {
|
||
const s = app.listen(port, host)
|
||
s.once('listening', () => resolve(s))
|
||
s.once('error', reject)
|
||
})
|
||
}
|
||
|
||
async function listenWithFallback(app: Koa, port: number, host?: string): Promise<ListenResult> {
|
||
const bindHost = host || '0.0.0.0'
|
||
console.log(`[bootstrap] listening on ${bindHost}:${port}`)
|
||
const primary = await listen(app, port, bindHost)
|
||
return { primary, servers: [primary] }
|
||
}
|
||
|
||
/**
|
||
* 安全获取网络接口信息(兼容 Termux/proot 环境)
|
||
* 在 proot 环境中 os.networkInterfaces() 会抛出权限错误(errno 13)
|
||
*/
|
||
function safeNetworkInterfaces() {
|
||
try {
|
||
return os.networkInterfaces()
|
||
} catch {
|
||
return {}
|
||
}
|
||
}
|
||
|
||
export async function bootstrap() {
|
||
console.log(`hermes-web-ui v${APP_VERSION} starting...`)
|
||
await mkdir(config.uploadDir, { recursive: true })
|
||
await mkdir(config.dataDir, { recursive: true })
|
||
|
||
const authToken = await getToken()
|
||
await initLoginLimiter()
|
||
const app = new Koa()
|
||
|
||
await initGatewayManager()
|
||
console.log('[bootstrap] gateway manager initialized')
|
||
await new Promise(resolve => setTimeout(resolve, 1000))
|
||
// Initialize all web-ui SQLite tables
|
||
const { initAllStores } = await import('./db/hermes/init')
|
||
// Wait 1 second before initializing stores to ensure all resources are ready
|
||
initAllStores()
|
||
await new Promise(resolve => setTimeout(resolve, 1000))
|
||
console.log('[bootstrap] all stores initialized')
|
||
|
||
// Sync Hermes sessions from all profiles (only if local DB is empty)
|
||
const { syncAllHermesSessionsOnStartup } = await import('./services/hermes/session-sync')
|
||
await syncAllHermesSessionsOnStartup()
|
||
console.log('[bootstrap] Hermes session sync completed')
|
||
|
||
app.use(cors({ origin: config.corsOrigins }))
|
||
app.use(bodyParser())
|
||
console.log('[bootstrap] cors + bodyParser registered')
|
||
|
||
// Register all routes (handles auth internally)
|
||
const proxyMiddleware = registerRoutes(app, requireAuth(authToken))
|
||
app.use(proxyMiddleware)
|
||
console.log('[bootstrap] routes registered')
|
||
|
||
if (authToken) {
|
||
console.log(`Auth enabled — token: ${authToken}`)
|
||
logger.info('Auth enabled — token: %s', authToken)
|
||
}
|
||
|
||
// SPA fallback
|
||
const distDir = resolve(__dirname, '..', 'client')
|
||
app.use(serve(distDir))
|
||
app.use(async (ctx) => {
|
||
if (!ctx.path.startsWith('/api') &&
|
||
ctx.path !== '/health' &&
|
||
ctx.path !== '/upload' &&
|
||
ctx.path !== '/webhook') {
|
||
await send(ctx, 'index.html', { root: distDir })
|
||
}
|
||
})
|
||
console.log('[bootstrap] SPA fallback registered')
|
||
|
||
// Start server using the configured bind host. Default is IPv4 for WSL stability.
|
||
const listenResult = await listenWithFallback(app, config.port, config.host)
|
||
server = listenResult.primary
|
||
servers = listenResult.servers
|
||
console.log('[bootstrap] app.listen called')
|
||
|
||
setupTerminalWebSocket(servers)
|
||
console.log('[bootstrap] terminal websocket setup')
|
||
|
||
// Group chat Socket.IO (must be after server is created)
|
||
const groupChatServer = new GroupChatServer(servers)
|
||
setGroupChatServer(groupChatServer)
|
||
groupChatServer.setGatewayManager(getGatewayManagerInstance())
|
||
|
||
// Chat run Socket.IO — shares the same Server instance, just adds /chat-run namespace
|
||
chatRunServer = new ChatRunSocket(groupChatServer.getIO(), getGatewayManagerInstance())
|
||
setChatRunServer(chatRunServer)
|
||
chatRunServer.init()
|
||
|
||
// Session deleter — periodically drain pending session deletes
|
||
const { SessionDeleter } = await import('./services/hermes/session-deleter')
|
||
const sessionDeleter = SessionDeleter.getInstance()
|
||
const activeProfile = process.env.PROFILE || 'default'
|
||
sessionDeleter.start(activeProfile)
|
||
console.log('[bootstrap] session deleter started, profile=%s', activeProfile)
|
||
|
||
// Catch-all: destroy upgrade requests not handled by terminal or Socket.IO
|
||
servers.forEach((httpServer) => {
|
||
httpServer.on('upgrade', (req: any, socket: any) => {
|
||
const url = new URL(req.url || '', `http://${req.headers.host}`)
|
||
if (url.pathname !== '/api/hermes/terminal' && !url.pathname.startsWith('/socket.io/')) {
|
||
socket.destroy()
|
||
}
|
||
})
|
||
})
|
||
|
||
const interfaces = safeNetworkInterfaces()
|
||
const localIp = Object.values(interfaces).flat().find(i => i?.family === 'IPv4' && !i?.internal)?.address || 'localhost'
|
||
console.log(`Server: http://localhost:${config.port} (LAN: http://${localIp}:${config.port})`)
|
||
console.log(`Log: ~/.hermes-web-ui/logs/server.log`)
|
||
logger.info('Server: http://localhost:%d (LAN: http://%s:%d)', config.port, localIp, config.port)
|
||
|
||
// Restore group chat agents after server is ready.
|
||
groupChatServer.restoreWhenReady()
|
||
|
||
servers.forEach((httpServer) => {
|
||
httpServer.on('error', (err: any) => {
|
||
console.error('[bootstrap] server error:', err.code || err.message)
|
||
logger.error({ err }, 'Server error')
|
||
})
|
||
})
|
||
|
||
bindShutdown(servers, groupChatServer, chatRunServer)
|
||
startVersionCheck()
|
||
}
|
||
|
||
bootstrap().catch((error) => {
|
||
console.error('FATAL: Failed to start Hermes Web UI')
|
||
console.error(error)
|
||
logger.fatal(error, 'Fatal error during bootstrap')
|
||
process.exit(1)
|
||
})
|