feat: add multi-gateway management with auto port detection

- Add GatewayManager for multi-profile gateway lifecycle management
- Auto-detect running gateways on startup via PID + health check
- Port conflict detection: check managed gateways, allocated ports, and
  system-level port availability (TCP bind test)
- Two-phase startup: sequential port resolution, parallel process launch
- Use `gateway start/restart` on normal systems, `gateway run --replace`
  on WSL/Docker
- Wait for health check before returning start/stop responses
- Add Gateways page with card-based layout showing profile status
- Reorganize sidebar navigation into collapsible groups
- Hide API server settings (now auto-managed by GatewayManager)
- Profile switch reloads page; Ctrl+C no longer stops gateways
- Remove redundant ensureApiServerConfig from index.ts and profiles.ts

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
ekko
2026-04-18 13:07:12 +08:00
parent 35481e452d
commit 4b6de351bd
15 changed files with 1170 additions and 467 deletions
+17 -77
View File
@@ -58,6 +58,7 @@ let isShuttingDown = false
// 👉 如果你有子进程,一定要存
let gatewayPid: number | null = null
let gatewayManager: any = null
export async function bootstrap() {
await mkdir(config.uploadDir, { recursive: true })
@@ -70,8 +71,7 @@ export async function bootstrap() {
console.log(`🔐 Auth enabled — token: ${authToken}`)
}
await ensureApiServerConfig()
await ensureGatewayRunning()
await initGatewayManager()
app.use(cors({ origin: config.corsOrigins }))
app.use(bodyParser())
@@ -115,7 +115,8 @@ export async function bootstrap() {
let gatewayOk = false
try {
const res = await fetch(`${config.upstream.replace(/\/$/, '')}/health`, {
const upstream = gatewayManager?.getUpstream() || config.upstream
const res = await fetch(`${upstream.replace(/\/$/, '')}/health`, {
signal: AbortSignal.timeout(5000),
})
gatewayOk = res.ok
@@ -191,13 +192,7 @@ function bindShutdown() {
})
}
// ✅ 2. 关闭子进程(如果有)
if (gatewayPid) {
try {
process.kill(gatewayPid)
console.log(`✓ gateway process killed: ${gatewayPid}`)
} catch { }
}
// gateway 是系统服务,不随 dev server 退出而停止
} catch (err) {
console.error('shutdown error:', err)
@@ -226,78 +221,23 @@ function bindShutdown() {
}
// ============================
// 你的原逻辑(基本不动)
// Gateway Manager
// ============================
async function ensureApiServerConfig() {
const { readFileSync, writeFileSync, existsSync, copyFileSync } = await import('fs')
const yaml = (await import('js-yaml')).default
const { getActiveConfigPath } = await import('./services/hermes/hermes-profile')
const configPath = getActiveConfigPath()
async function initGatewayManager() {
const { GatewayManager } = await import('./services/hermes/gateway-manager')
const { getActiveProfileName } = await import('./services/hermes/hermes-profile')
const { setGatewayManager } = await import('./routes/hermes/gateways')
const defaults: Record<string, any> = {
enabled: true,
host: '127.0.0.1',
port: 8642,
key: '',
cors_origins: '*',
}
const activeProfile = getActiveProfileName()
gatewayManager = new GatewayManager(activeProfile)
setGatewayManager(gatewayManager)
try {
if (!existsSync(configPath)) {
console.log('✗ config.yaml not found')
return
}
// Detect all running gateways
await gatewayManager.detectAllOnStartup()
const content = readFileSync(configPath, 'utf-8')
const cfg = yaml.load(content) as any || {}
if (!cfg.platforms) cfg.platforms = {}
if (!cfg.platforms.api_server) cfg.platforms.api_server = {}
cfg.platforms.api_server = defaults
copyFileSync(configPath, configPath + '.bak')
writeFileSync(configPath, yaml.dump(cfg), 'utf-8')
await restartGateway()
} catch (err: any) {
console.error('config error:', err.message)
}
}
async function ensureGatewayRunning() {
const upstream = config.upstream.replace(/\/$/, '')
const waitForGatewayReady = async (timeoutMs: number = 15000) => {
const deadline = Date.now() + timeoutMs
while (Date.now() < deadline) {
try {
const res = await fetch(`${upstream}/health`, { signal: AbortSignal.timeout(2000) })
if (res.ok) return true
} catch { }
await new Promise(r => setTimeout(r, 300))
}
return false
}
try {
const res = await fetch(`${upstream}/health`, { signal: AbortSignal.timeout(5000) })
if (res.ok) return
} catch { }
console.log('⚠ Gateway not running, starting...')
try {
// 👉 关键:保存 PID
gatewayPid = await startGatewayBackground()
if (await waitForGatewayReady()) {
console.log(`✓ Gateway started (PID: ${gatewayPid})`)
} else {
console.error('gateway start failed: timed out waiting for health')
}
} catch (err: any) {
console.error('gateway start failed:', err.message)
}
// Start all gateways that aren't running
await gatewayManager.startAll()
}
bootstrap()