fix: default to 0.0.0.0 to fix WSL2 health check failure (#520)
PR #470 changed the default listen host to undefined, letting Node.js bind to IPv6 :: on systems that support it. This broke WSL2 where IPv6 dual-stack is unreliable — the server binds to :: but IPv4 127.0.0.1 connections fail, causing the health check to time out. Revert to 0.0.0.0 as the default. Users who need IPv6 can set BIND_HOST=:: explicitly. Fixes #518 Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -120,6 +120,15 @@ interface ManagedGateway {
|
||||
process?: ChildProcess
|
||||
}
|
||||
|
||||
function formatHostForUrl(host: string): string {
|
||||
if (host.startsWith('[') && host.endsWith(']')) return host
|
||||
return host.includes(':') ? `[${host}]` : host
|
||||
}
|
||||
|
||||
function buildHttpUrl(host: string, port: number): string {
|
||||
return `http://${formatHostForUrl(host)}:${port}`
|
||||
}
|
||||
|
||||
// ============================
|
||||
// GatewayManager
|
||||
// ============================
|
||||
@@ -360,7 +369,7 @@ export class GatewayManager {
|
||||
const gw = this.gateways.get(name)
|
||||
if (gw?.url) return gw.url
|
||||
const { port, host } = this.readProfilePort(name)
|
||||
return `http://${host}:${port}`
|
||||
return buildHttpUrl(host, port)
|
||||
}
|
||||
|
||||
/** 读取 profile 的 API_SERVER_KEY(从 .env 文件) */
|
||||
@@ -426,7 +435,7 @@ export class GatewayManager {
|
||||
async detectStatus(name: string): Promise<GatewayStatus> {
|
||||
const pid = this.readPidFile(name)
|
||||
const { port, host } = this.readProfilePort(name)
|
||||
const url = `http://${host}:${port}`
|
||||
const url = buildHttpUrl(host, port)
|
||||
|
||||
if (pid && this.isProcessAlive(pid) && await this.checkHealth(url)) {
|
||||
this.gateways.set(name, { pid, port, host, url })
|
||||
@@ -456,7 +465,7 @@ export class GatewayManager {
|
||||
async start(name: string): Promise<GatewayStatus> {
|
||||
const { port, host } = await this.resolvePort(name)
|
||||
const hermesHome = this.profileDir(name)
|
||||
const url = `http://${host}:${port}`
|
||||
const url = buildHttpUrl(host, port)
|
||||
|
||||
if (needsRunMode) {
|
||||
// WSL / Docker:无 systemd/launchd,用 "gateway run" 作为 detached 子进程
|
||||
@@ -534,7 +543,7 @@ export class GatewayManager {
|
||||
const gw = this.gateways.get(name)
|
||||
const url = gw?.url || (() => {
|
||||
const { port, host } = this.readProfilePort(name)
|
||||
return `http://${host}:${port}`
|
||||
return buildHttpUrl(host, port)
|
||||
})()
|
||||
|
||||
if (!needsRunMode) {
|
||||
|
||||
@@ -3,7 +3,6 @@ import type { Server as HttpServer } from 'http'
|
||||
import { getToken } from '../../../services/auth'
|
||||
import { logger } from '../../../services/logger'
|
||||
import { getDb } from '../../../db'
|
||||
import { GC_ROOMS_TABLE, GC_MESSAGES_TABLE, GC_ROOM_AGENTS_TABLE, GC_CONTEXT_SNAPSHOTS_TABLE, GC_ROOM_MEMBERS_TABLE, GC_PENDING_SESSION_DELETES_TABLE, GC_SESSION_PROFILES_TABLE } from '../../../db/hermes/schemas'
|
||||
import { AgentClients } from './agent-clients'
|
||||
import { ContextEngine } from '../context-engine/compressor'
|
||||
import { SessionDeleter } from '../session-deleter'
|
||||
@@ -39,74 +38,6 @@ interface Member {
|
||||
socketId: string
|
||||
}
|
||||
|
||||
// ─── SQLite Storage (global DB) ──────────────────────────────
|
||||
|
||||
const GC_PENDING_SESSION_DELETES_SCHEMA: Record<string, string> = {
|
||||
session_id: 'TEXT PRIMARY KEY',
|
||||
profile_name: 'TEXT NOT NULL',
|
||||
status: "TEXT NOT NULL DEFAULT 'pending'",
|
||||
attempt_count: 'INTEGER NOT NULL DEFAULT 0',
|
||||
last_error: 'TEXT',
|
||||
created_at: 'INTEGER NOT NULL',
|
||||
updated_at: 'INTEGER NOT NULL',
|
||||
next_attempt_at: 'INTEGER NOT NULL DEFAULT 0',
|
||||
}
|
||||
|
||||
const GC_SESSION_PROFILES_SCHEMA: Record<string, string> = {
|
||||
session_id: 'TEXT PRIMARY KEY',
|
||||
room_id: 'TEXT NOT NULL',
|
||||
agent_id: 'TEXT NOT NULL',
|
||||
profile_name: 'TEXT NOT NULL',
|
||||
created_at: 'INTEGER NOT NULL',
|
||||
}
|
||||
|
||||
const GC_ROOMS_SCHEMA: Record<string, string> = {
|
||||
id: 'TEXT PRIMARY KEY',
|
||||
name: 'TEXT NOT NULL',
|
||||
inviteCode: 'TEXT UNIQUE',
|
||||
triggerTokens: 'INTEGER NOT NULL DEFAULT 100000',
|
||||
maxHistoryTokens: 'INTEGER NOT NULL DEFAULT 32000',
|
||||
tailMessageCount: 'INTEGER NOT NULL DEFAULT 20',
|
||||
totalTokens: 'INTEGER NOT NULL DEFAULT 0',
|
||||
}
|
||||
|
||||
const GC_MESSAGES_SCHEMA: Record<string, string> = {
|
||||
id: 'TEXT PRIMARY KEY',
|
||||
roomId: 'TEXT NOT NULL',
|
||||
senderId: 'TEXT NOT NULL',
|
||||
senderName: 'TEXT NOT NULL',
|
||||
content: 'TEXT NOT NULL',
|
||||
timestamp: 'INTEGER NOT NULL',
|
||||
}
|
||||
|
||||
const GC_ROOM_AGENTS_SCHEMA: Record<string, string> = {
|
||||
id: 'TEXT PRIMARY KEY',
|
||||
roomId: 'TEXT NOT NULL',
|
||||
agentId: 'TEXT NOT NULL',
|
||||
profile: 'TEXT NOT NULL',
|
||||
name: 'TEXT NOT NULL',
|
||||
description: "TEXT NOT NULL DEFAULT ''",
|
||||
invited: 'INTEGER NOT NULL DEFAULT 0',
|
||||
}
|
||||
|
||||
const GC_CONTEXT_SNAPSHOTS_SCHEMA: Record<string, string> = {
|
||||
roomId: 'TEXT PRIMARY KEY',
|
||||
summary: 'TEXT NOT NULL DEFAULT \'\'',
|
||||
lastMessageId: 'TEXT NOT NULL',
|
||||
lastMessageTimestamp: 'INTEGER NOT NULL',
|
||||
updatedAt: 'INTEGER NOT NULL',
|
||||
}
|
||||
|
||||
const GC_ROOM_MEMBERS_SCHEMA: Record<string, string> = {
|
||||
id: 'TEXT PRIMARY KEY',
|
||||
roomId: 'TEXT NOT NULL',
|
||||
userId: 'TEXT NOT NULL',
|
||||
userName: 'TEXT NOT NULL',
|
||||
description: "TEXT NOT NULL DEFAULT ''",
|
||||
joinedAt: 'INTEGER NOT NULL',
|
||||
updatedAt: 'INTEGER NOT NULL',
|
||||
}
|
||||
|
||||
let _tablesEnsured = false
|
||||
|
||||
interface PendingSessionDelete {
|
||||
@@ -487,13 +418,15 @@ export class GroupChatServer {
|
||||
}
|
||||
}
|
||||
|
||||
constructor(httpServer: HttpServer) {
|
||||
constructor(httpServers: HttpServer | HttpServer[]) {
|
||||
this.storage = new ChatStorage()
|
||||
this.storage.init()
|
||||
const servers = Array.isArray(httpServers) ? httpServers : [httpServers]
|
||||
|
||||
this.io = new Server(httpServer, {
|
||||
this.io = new Server(servers[0], {
|
||||
cors: { origin: '*' }
|
||||
})
|
||||
servers.slice(1).forEach((httpServer) => this.io.attach(httpServer))
|
||||
this.nsp = this.io.of('/group-chat')
|
||||
this.nsp.use(this.authMiddleware.bind(this))
|
||||
this.nsp.on('connection', this.onConnection.bind(this))
|
||||
|
||||
@@ -8,6 +8,9 @@ export function bindShutdown(server: any, groupChatServer?: any, chatRunServer?:
|
||||
if (isShuttingDown) return
|
||||
isShuttingDown = true
|
||||
|
||||
// Force exit after 3s no matter what
|
||||
setTimeout(() => process.exit(0), 3000)
|
||||
|
||||
logger.info('Shutting down (%s)...', signal)
|
||||
|
||||
try {
|
||||
@@ -24,13 +27,16 @@ export function bindShutdown(server: any, groupChatServer?: any, chatRunServer?:
|
||||
logger.info('Socket.IO closed')
|
||||
}
|
||||
|
||||
if (server) {
|
||||
await new Promise<void>((resolve) => {
|
||||
server.close(() => {
|
||||
logger.info('HTTP server closed')
|
||||
resolve()
|
||||
const servers = Array.isArray(server) ? server : [server].filter(Boolean)
|
||||
if (servers.length) {
|
||||
await Promise.all(servers.map((httpServer) => (
|
||||
new Promise<void>((resolve) => {
|
||||
httpServer.close(() => {
|
||||
logger.info('HTTP server closed')
|
||||
resolve()
|
||||
})
|
||||
})
|
||||
})
|
||||
)))
|
||||
}
|
||||
} catch (err) {
|
||||
logger.error(err, 'Shutdown error')
|
||||
|
||||
Reference in New Issue
Block a user