mirror of
https://github.com/claude-code-best/claude-code.git
synced 2026-06-22 16:25:51 +00:00
style: 完成所有文件的lint
This commit is contained in:
@@ -1,31 +1,31 @@
|
||||
import type { WSContext } from "hono/ws";
|
||||
import { getEventBus } from "./event-bus";
|
||||
import type { SessionEvent } from "./event-bus";
|
||||
import { publishSessionEvent } from "../services/transport";
|
||||
import { log, error as logError } from "../logger";
|
||||
import { toClientPayload } from "./client-payload";
|
||||
import { config } from "../config";
|
||||
import type { WSContext } from 'hono/ws'
|
||||
import { getEventBus } from './event-bus'
|
||||
import type { SessionEvent } from './event-bus'
|
||||
import { publishSessionEvent } from '../services/transport'
|
||||
import { log, error as logError } from '../logger'
|
||||
import { toClientPayload } from './client-payload'
|
||||
import { config } from '../config'
|
||||
|
||||
// Per-connection cleanup, keyed by sessionId (only one WS per session)
|
||||
interface CleanupEntry {
|
||||
unsub: () => void;
|
||||
keepalive: ReturnType<typeof setInterval>;
|
||||
ws: WSContext;
|
||||
openTime: number;
|
||||
lastClientActivity: number;
|
||||
unsub: () => void
|
||||
keepalive: ReturnType<typeof setInterval>
|
||||
ws: WSContext
|
||||
openTime: number
|
||||
lastClientActivity: number
|
||||
}
|
||||
const cleanupBySession = new Map<string, CleanupEntry>();
|
||||
const cleanupBySession = new Map<string, CleanupEntry>()
|
||||
|
||||
// Track all active WS connections for graceful shutdown
|
||||
const activeConnections = new Set<WSContext>();
|
||||
const activeConnections = new Set<WSContext>()
|
||||
|
||||
// Server-side keepalive interval (configurable via RCS_WS_KEEPALIVE_INTERVAL).
|
||||
// Sends data frames to keep reverse proxies from closing idle connections.
|
||||
const SERVER_KEEPALIVE_INTERVAL_MS = (config.wsKeepaliveInterval || 20) * 1000;
|
||||
const SERVER_KEEPALIVE_INTERVAL_MS = (config.wsKeepaliveInterval || 20) * 1000
|
||||
|
||||
// If no client data received within this threshold, the connection is
|
||||
// considered dead. Set to 3x keepalive to tolerate one missed interval.
|
||||
const CLIENT_ACTIVITY_TIMEOUT_MS = SERVER_KEEPALIVE_INTERVAL_MS * 3;
|
||||
const CLIENT_ACTIVITY_TIMEOUT_MS = SERVER_KEEPALIVE_INTERVAL_MS * 3
|
||||
|
||||
/**
|
||||
* Convert internal EventBus event -> SDK message for bridge client.
|
||||
@@ -33,36 +33,36 @@ const CLIENT_ACTIVITY_TIMEOUT_MS = SERVER_KEEPALIVE_INTERVAL_MS * 3;
|
||||
function toSDKMessage(event: SessionEvent): string {
|
||||
// NDJSON format: each message MUST end with \n so the child process's
|
||||
// line-based parser can split messages correctly.
|
||||
return JSON.stringify(toClientPayload(event)) + "\n";
|
||||
return JSON.stringify(toClientPayload(event)) + '\n'
|
||||
}
|
||||
|
||||
/** Called from onOpen — subscribes to event bus, forwards outbound events to bridge WS */
|
||||
export function handleWebSocketOpen(ws: WSContext, sessionId: string) {
|
||||
const openTime = Date.now();
|
||||
const lastClientActivity = Date.now();
|
||||
log(`[RC-DEBUG] [WS] Open session=${sessionId}`);
|
||||
activeConnections.add(ws);
|
||||
const openTime = Date.now()
|
||||
const lastClientActivity = Date.now()
|
||||
log(`[RC-DEBUG] [WS] Open session=${sessionId}`)
|
||||
activeConnections.add(ws)
|
||||
|
||||
// If there's an existing connection for this session, clean it up first
|
||||
const existing = cleanupBySession.get(sessionId);
|
||||
const existing = cleanupBySession.get(sessionId)
|
||||
if (existing) {
|
||||
log(`[WS] Replacing existing connection for session=${sessionId}`);
|
||||
existing.unsub();
|
||||
clearInterval(existing.keepalive);
|
||||
activeConnections.delete(existing.ws);
|
||||
log(`[WS] Replacing existing connection for session=${sessionId}`)
|
||||
existing.unsub()
|
||||
clearInterval(existing.keepalive)
|
||||
activeConnections.delete(existing.ws)
|
||||
}
|
||||
|
||||
const bus = getEventBus(sessionId);
|
||||
const bus = getEventBus(sessionId)
|
||||
|
||||
// Replay ALL events (inbound + outbound) so the bridge can reconstruct
|
||||
// the full conversation history — assistant replies are inbound events.
|
||||
const missed = bus.getEventsSince(0);
|
||||
const missed = bus.getEventsSince(0)
|
||||
if (missed.length > 0) {
|
||||
log(`[WS] Replaying ${missed.length} missed event(s)`);
|
||||
log(`[WS] Replaying ${missed.length} missed event(s)`)
|
||||
for (const event of missed) {
|
||||
if (ws.readyState !== 1) break;
|
||||
if (ws.readyState !== 1) break
|
||||
try {
|
||||
ws.send(toSDKMessage(event));
|
||||
ws.send(toSDKMessage(event))
|
||||
} catch {
|
||||
// ignore send errors during replay
|
||||
}
|
||||
@@ -70,75 +70,96 @@ export function handleWebSocketOpen(ws: WSContext, sessionId: string) {
|
||||
}
|
||||
|
||||
const unsub = bus.subscribe((event: SessionEvent) => {
|
||||
if (ws.readyState !== 1) return;
|
||||
if (event.direction !== "outbound") return;
|
||||
if (ws.readyState !== 1) return
|
||||
if (event.direction !== 'outbound') return
|
||||
try {
|
||||
const sdkMsg = toSDKMessage(event);
|
||||
log(`[RC-DEBUG] [WS] -> bridge (outbound): type=${event.type} len=${sdkMsg.length} msg=${sdkMsg.slice(0, 300)}`);
|
||||
ws.send(sdkMsg);
|
||||
const sdkMsg = toSDKMessage(event)
|
||||
log(
|
||||
`[RC-DEBUG] [WS] -> bridge (outbound): type=${event.type} len=${sdkMsg.length} msg=${sdkMsg.slice(0, 300)}`,
|
||||
)
|
||||
ws.send(sdkMsg)
|
||||
} catch (err) {
|
||||
logError("[RC-DEBUG] [WS] send error:", err);
|
||||
logError('[RC-DEBUG] [WS] send error:', err)
|
||||
}
|
||||
});
|
||||
})
|
||||
|
||||
const keepalive = setInterval(() => {
|
||||
if (ws.readyState !== 1) {
|
||||
clearInterval(keepalive);
|
||||
return;
|
||||
clearInterval(keepalive)
|
||||
return
|
||||
}
|
||||
// Check if client is still alive — close if no data received for too long
|
||||
const silenceMs = Date.now() - lastClientActivity;
|
||||
const silenceMs = Date.now() - lastClientActivity
|
||||
if (silenceMs > CLIENT_ACTIVITY_TIMEOUT_MS) {
|
||||
log(`[WS] Client inactive for ${Math.round(silenceMs / 1000)}s on session=${sessionId}, closing dead connection`);
|
||||
log(
|
||||
`[WS] Client inactive for ${Math.round(silenceMs / 1000)}s on session=${sessionId}, closing dead connection`,
|
||||
)
|
||||
try {
|
||||
ws.close(1000, "client inactive");
|
||||
ws.close(1000, 'client inactive')
|
||||
} catch {
|
||||
clearInterval(keepalive);
|
||||
clearInterval(keepalive)
|
||||
}
|
||||
return;
|
||||
return
|
||||
}
|
||||
try {
|
||||
ws.send('{"type":"keep_alive"}\n');
|
||||
ws.send('{"type":"keep_alive"}\n')
|
||||
} catch {
|
||||
clearInterval(keepalive);
|
||||
clearInterval(keepalive)
|
||||
}
|
||||
}, SERVER_KEEPALIVE_INTERVAL_MS);
|
||||
}, SERVER_KEEPALIVE_INTERVAL_MS)
|
||||
|
||||
cleanupBySession.set(sessionId, { unsub, keepalive, ws, openTime, lastClientActivity });
|
||||
cleanupBySession.set(sessionId, {
|
||||
unsub,
|
||||
keepalive,
|
||||
ws,
|
||||
openTime,
|
||||
lastClientActivity,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Called from onMessage — bridge sends newline-delimited JSON.
|
||||
*/
|
||||
export function handleWebSocketMessage(ws: WSContext, sessionId: string, data: string) {
|
||||
export function handleWebSocketMessage(
|
||||
ws: WSContext,
|
||||
sessionId: string,
|
||||
data: string,
|
||||
) {
|
||||
// Track client activity for dead-connection detection
|
||||
const entry = cleanupBySession.get(sessionId);
|
||||
const entry = cleanupBySession.get(sessionId)
|
||||
if (entry) {
|
||||
entry.lastClientActivity = Date.now();
|
||||
entry.lastClientActivity = Date.now()
|
||||
}
|
||||
const lines = data.split("\n").filter((l) => l.trim());
|
||||
const lines = data.split('\n').filter(l => l.trim())
|
||||
for (const line of lines) {
|
||||
try {
|
||||
ingestBridgeMessage(sessionId, JSON.parse(line));
|
||||
ingestBridgeMessage(sessionId, JSON.parse(line))
|
||||
} catch (err) {
|
||||
logError("[WS] parse error:", err);
|
||||
logError('[WS] parse error:', err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Called from onClose — unsubscribes from event bus */
|
||||
export function handleWebSocketClose(ws: WSContext, sessionId: string, code?: number, reason?: string) {
|
||||
activeConnections.delete(ws);
|
||||
export function handleWebSocketClose(
|
||||
ws: WSContext,
|
||||
sessionId: string,
|
||||
code?: number,
|
||||
reason?: string,
|
||||
) {
|
||||
activeConnections.delete(ws)
|
||||
|
||||
const entry = cleanupBySession.get(sessionId);
|
||||
const duration = entry ? Math.round((Date.now() - entry.openTime) / 1000) : -1;
|
||||
const entry = cleanupBySession.get(sessionId)
|
||||
const duration = entry ? Math.round((Date.now() - entry.openTime) / 1000) : -1
|
||||
|
||||
log(`[WS] Close session=${sessionId} code=${code ?? "none"} reason=${reason || "(none)"} duration=${duration}s`);
|
||||
log(
|
||||
`[WS] Close session=${sessionId} code=${code ?? 'none'} reason=${reason || '(none)'} duration=${duration}s`,
|
||||
)
|
||||
|
||||
if (entry) {
|
||||
entry.unsub();
|
||||
clearInterval(entry.keepalive);
|
||||
cleanupBySession.delete(sessionId);
|
||||
entry.unsub()
|
||||
clearInterval(entry.keepalive)
|
||||
cleanupBySession.delete(sessionId)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -150,88 +171,104 @@ export function handleWebSocketClose(ws: WSContext, sessionId: string, code?: nu
|
||||
* {"subtype":"success","uuid":"...","result":"..."} → type "result"
|
||||
*/
|
||||
function deriveEventType(msg: Record<string, unknown>): string {
|
||||
if (msg.type && typeof msg.type === "string") return msg.type;
|
||||
if (msg.type && typeof msg.type === 'string') return msg.type
|
||||
|
||||
// Child process stream-json format: message.role determines type
|
||||
const message = msg.message as Record<string, unknown> | undefined;
|
||||
if (message && typeof message.role === "string") {
|
||||
return message.role; // "user", "assistant", "system"
|
||||
const message = msg.message as Record<string, unknown> | undefined
|
||||
if (message && typeof message.role === 'string') {
|
||||
return message.role // "user", "assistant", "system"
|
||||
}
|
||||
|
||||
// Result message
|
||||
if (msg.subtype || msg.result !== undefined) return "result";
|
||||
if (msg.subtype || msg.result !== undefined) return 'result'
|
||||
|
||||
// System/init message
|
||||
if (msg.session_id) return "system";
|
||||
if (msg.session_id) return 'system'
|
||||
|
||||
return "unknown";
|
||||
return 'unknown'
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a single SDK message from bridge -> publish to EventBus as inbound.
|
||||
*/
|
||||
export function ingestBridgeMessage(sessionId: string, msg: Record<string, unknown>) {
|
||||
if (msg.type === "keep_alive") return;
|
||||
export function ingestBridgeMessage(
|
||||
sessionId: string,
|
||||
msg: Record<string, unknown>,
|
||||
) {
|
||||
if (msg.type === 'keep_alive') return
|
||||
|
||||
const eventType = deriveEventType(msg);
|
||||
const eventType = deriveEventType(msg)
|
||||
|
||||
log(`[RC-DEBUG] [WS] <- bridge (inbound): sessionId=${sessionId} type=${eventType}${msg.uuid ? ` uuid=${msg.uuid}` : ""} msg=${JSON.stringify(msg).slice(0, 300)}`);
|
||||
log(
|
||||
`[RC-DEBUG] [WS] <- bridge (inbound): sessionId=${sessionId} type=${eventType}${msg.uuid ? ` uuid=${msg.uuid}` : ''} msg=${JSON.stringify(msg).slice(0, 300)}`,
|
||||
)
|
||||
|
||||
let payload: unknown;
|
||||
let payload: unknown
|
||||
|
||||
if (eventType === "assistant" || eventType === "partial_assistant") {
|
||||
const message = msg.message as Record<string, unknown> | undefined;
|
||||
const content = message?.content;
|
||||
if (eventType === 'assistant' || eventType === 'partial_assistant') {
|
||||
const message = msg.message as Record<string, unknown> | undefined
|
||||
const content = message?.content
|
||||
// Extract text from content blocks for simple display
|
||||
let text = "";
|
||||
if (typeof content === "string") {
|
||||
text = content;
|
||||
let text = ''
|
||||
if (typeof content === 'string') {
|
||||
text = content
|
||||
} else if (Array.isArray(content)) {
|
||||
text = content
|
||||
.filter((b: unknown) => b && typeof b === "object" && "type" in (b as Record<string, unknown>) && (b as Record<string, unknown>).type === "text")
|
||||
.map((b: Record<string, unknown>) => (b as Record<string, unknown>).text || "")
|
||||
.join("");
|
||||
.filter(
|
||||
(b: unknown) =>
|
||||
b &&
|
||||
typeof b === 'object' &&
|
||||
'type' in (b as Record<string, unknown>) &&
|
||||
(b as Record<string, unknown>).type === 'text',
|
||||
)
|
||||
.map(
|
||||
(b: Record<string, unknown>) =>
|
||||
(b as Record<string, unknown>).text || '',
|
||||
)
|
||||
.join('')
|
||||
}
|
||||
payload = { message: msg.message, uuid: msg.uuid, content: text };
|
||||
} else if (eventType === "user" || eventType === "system") {
|
||||
payload = { message: msg.message, uuid: msg.uuid, content: text }
|
||||
} else if (eventType === 'user' || eventType === 'system') {
|
||||
payload = {
|
||||
message: msg.message,
|
||||
uuid: msg.uuid,
|
||||
...(typeof msg.isSynthetic === "boolean" ? { isSynthetic: msg.isSynthetic } : {}),
|
||||
};
|
||||
} else if (eventType === "control_request") {
|
||||
payload = { request_id: msg.request_id, request: msg.request };
|
||||
} else if (eventType === "control_response") {
|
||||
payload = { response: msg.response };
|
||||
} else if (eventType === "result" || eventType === "result_success") {
|
||||
payload = { subtype: msg.subtype, uuid: msg.uuid, result: msg.result };
|
||||
...(typeof msg.isSynthetic === 'boolean'
|
||||
? { isSynthetic: msg.isSynthetic }
|
||||
: {}),
|
||||
}
|
||||
} else if (eventType === 'control_request') {
|
||||
payload = { request_id: msg.request_id, request: msg.request }
|
||||
} else if (eventType === 'control_response') {
|
||||
payload = { response: msg.response }
|
||||
} else if (eventType === 'result' || eventType === 'result_success') {
|
||||
payload = { subtype: msg.subtype, uuid: msg.uuid, result: msg.result }
|
||||
} else {
|
||||
payload = msg;
|
||||
payload = msg
|
||||
}
|
||||
|
||||
publishSessionEvent(sessionId, eventType, payload, "inbound");
|
||||
publishSessionEvent(sessionId, eventType, payload, 'inbound')
|
||||
}
|
||||
|
||||
/**
|
||||
* Gracefully close all active WebSocket connections.
|
||||
*/
|
||||
export function closeAllConnections(): void {
|
||||
const count = activeConnections.size;
|
||||
if (count === 0) return;
|
||||
const count = activeConnections.size
|
||||
if (count === 0) return
|
||||
|
||||
log(`[WS] Gracefully closing ${count} active connection(s)...`);
|
||||
log(`[WS] Gracefully closing ${count} active connection(s)...`)
|
||||
for (const [sessionId, entry] of cleanupBySession) {
|
||||
try {
|
||||
entry.unsub();
|
||||
clearInterval(entry.keepalive);
|
||||
entry.unsub()
|
||||
clearInterval(entry.keepalive)
|
||||
if (entry.ws.readyState === 1) {
|
||||
entry.ws.close(1001, "server_shutdown");
|
||||
entry.ws.close(1001, 'server_shutdown')
|
||||
}
|
||||
} catch {
|
||||
// ignore errors during shutdown
|
||||
}
|
||||
}
|
||||
cleanupBySession.clear();
|
||||
activeConnections.clear();
|
||||
log("[WS] All connections closed");
|
||||
cleanupBySession.clear()
|
||||
activeConnections.clear()
|
||||
log('[WS] All connections closed')
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user