Files
Hermes-ui/server/src/index.ts
T
ekkoandClaude Opus 4.6 66cc9a6f1e fix: generate token on start, include token in URL, reset api_server config
- Pre-generate auth token before server start and pass via AUTH_TOKEN env var
- Append token to startup URL for auto-login
- Reset api_server config values to defaults on startup

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-15 08:28:36 +08:00

241 lines
6.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 { resolve } from 'path'
import { mkdir } from 'fs/promises'
import { config } from './config'
import { proxyRoutes } from './routes/proxy'
import { uploadRoutes } from './routes/upload'
import { sessionRoutes } from './routes/sessions'
import { webhookRoutes } from './routes/webhook'
import { logRoutes } from './routes/logs'
import { fsRoutes } from './routes/filesystem'
import { configRoutes } from './routes/config'
import { weixinRoutes } from './routes/weixin'
import * as hermesCli from './services/hermes-cli'
import { getToken, authMiddleware } from './services/auth'
const app = new Koa()
const { restartGateway, startGateway, startGatewayBackground, getVersion } = hermesCli
let server: any = null
let isShuttingDown = false
// 👉 如果你有子进程,一定要存
let gatewayPid: number | null = null
export async function bootstrap() {
await mkdir(config.uploadDir, { recursive: true })
await mkdir(config.dataDir, { recursive: true })
// Auth (after mkdir so data dir exists)
const authToken = await getToken()
if (authToken) {
app.use(await authMiddleware(authToken))
console.log(`🔐 Auth enabled — token: ${authToken}`)
}
await ensureApiServerConfig()
await ensureGatewayRunning()
app.use(cors({ origin: config.corsOrigins }))
app.use(bodyParser())
app.use(webhookRoutes.routes())
app.use(logRoutes.routes())
app.use(uploadRoutes.routes())
app.use(sessionRoutes.routes())
app.use(fsRoutes.routes())
app.use(configRoutes.routes())
app.use(weixinRoutes.routes())
// health
app.use(async (ctx, next) => {
if (ctx.path === '/health') {
const raw = await getVersion()
const version = raw.split('\n')[0].replace('Hermes Agent ', '') || ''
let gatewayOk = false
try {
const res = await fetch(`${config.upstream.replace(/\/$/, '')}/health`, {
signal: AbortSignal.timeout(5000),
})
gatewayOk = res.ok
} catch { }
ctx.body = {
status: gatewayOk ? 'ok' : 'error',
platform: 'hermes-agent',
version,
gateway: gatewayOk ? 'running' : 'stopped',
}
return
}
await next()
})
app.use(proxyRoutes.routes())
// SPA
const distDir = resolve(__dirname, '..')
app.use(serve(distDir))
app.use(async (ctx) => {
if (!ctx.path.startsWith('/api') &&
!ctx.path.startsWith('/v1') &&
ctx.path !== '/health' &&
ctx.path !== '/upload' &&
ctx.path !== '/webhook') {
await send(ctx, 'index.html', { root: distDir })
}
})
// 🚀 启动服务
server = app.listen(config.port, '0.0.0.0')
server.on('listening', () => {
console.log(`➜ Server: http://localhost:${config.port}`)
console.log(`➜ Upstream: ${config.upstream}`)
})
server.on('error', (err: any) => {
console.error('Server error:', err.message)
})
// 👇 绑定退出信号
bindShutdown()
}
// ============================
// ✅ 统一关闭逻辑(核心)
// ============================
function bindShutdown() {
const shutdown = async (signal: string) => {
if (isShuttingDown) return
isShuttingDown = true
console.log(`\n[${signal}] shutting down...`)
try {
// ✅ 1. 关闭 HTTP server
if (server) {
await new Promise<void>((resolve) => {
server.close(() => {
console.log('✓ http server closed')
resolve()
})
})
}
// ✅ 2. 关闭子进程(如果有)
if (gatewayPid) {
try {
process.kill(gatewayPid)
console.log(`✓ gateway process killed: ${gatewayPid}`)
} catch { }
}
} catch (err) {
console.error('shutdown error:', err)
}
process.exit(0)
}
// 👉 nodemon 专用(必须 once
process.once('SIGUSR2', shutdown)
// 👉 正常退出
process.on('SIGINT', shutdown)
process.on('SIGTERM', shutdown)
// 👉 防止异常退出没处理
process.on('uncaughtException', (err) => {
console.error('uncaughtException:', err)
shutdown('uncaughtException')
})
process.on('unhandledRejection', (err) => {
console.error('unhandledRejection:', err)
shutdown('unhandledRejection')
})
}
// ============================
// 你的原逻辑(基本不动)
// ============================
async function ensureApiServerConfig() {
const { homedir } = await import('os')
const { readFileSync, writeFileSync, existsSync, copyFileSync } = await import('fs')
const yaml = (await import('js-yaml')).default
const configPath = resolve(homedir(), '.hermes/config.yaml')
const defaults: Record<string, any> = {
enabled: true,
host: '127.0.0.1',
port: 8642,
key: '',
cors_origins: '*',
}
try {
if (!existsSync(configPath)) {
console.log('✗ config.yaml not found')
return
}
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 = {}
const api = cfg.platforms.api_server
let changed = false
for (const [k, v] of Object.entries(defaults)) {
if (api[k] != null && api[k] !== v) {
api[k] = v
changed = true
}
}
if (!changed) return
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(/\/$/, '')
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()
await new Promise(r => setTimeout(r, 3000))
const res = await fetch(`${upstream}/health`, { signal: AbortSignal.timeout(5000) })
if (res.ok) {
console.log(`✓ Gateway started (PID: ${gatewayPid})`)
}
} catch (err: any) {
console.error('gateway start failed:', err.message)
}
}
bootstrap()