fix(remote-control): harden self-hosted session flows (#278)

Co-authored-by: chengzifeng <chengzifeng@meituan.com>
This commit is contained in:
Cheng Zi Feng
2026-04-16 10:46:31 +08:00
committed by GitHub
parent 5a4c820e1d
commit fe08cacf8d
24 changed files with 1252 additions and 162 deletions

View File

@@ -1,32 +1,35 @@
import { storeListActiveEnvironments, storeUpdateEnvironment } from "../store";
import { storeListSessions, storeUpdateSession } from "../store";
import { storeListSessions } from "../store";
import { config } from "../config";
import { updateSessionStatus } from "./session";
export function startDisconnectMonitor() {
export function runDisconnectMonitorSweep(now = Date.now()) {
const timeoutMs = config.disconnectTimeout * 1000;
// Check environment heartbeat timeout
const envs = storeListActiveEnvironments();
for (const env of envs) {
if (env.lastPollAt && now - env.lastPollAt.getTime() > timeoutMs) {
console.log(`[RCS] Environment ${env.id} timed out (no poll for ${Math.round((now - env.lastPollAt.getTime()) / 1000)}s)`);
storeUpdateEnvironment(env.id, { status: "disconnected" });
}
}
// Check session timeout (2x disconnect timeout with no update)
const sessions = storeListSessions();
for (const session of sessions) {
if (session.status === "running" || session.status === "idle") {
const elapsed = now - session.updatedAt.getTime();
if (elapsed > timeoutMs * 2) {
console.log(`[RCS] Session ${session.id} marked inactive (no update for ${Math.round(elapsed / 1000)}s)`);
updateSessionStatus(session.id, "inactive");
}
}
}
}
export function startDisconnectMonitor() {
setInterval(() => {
const now = Date.now();
// Check environment heartbeat timeout
const envs = storeListActiveEnvironments();
for (const env of envs) {
if (env.lastPollAt && now - env.lastPollAt.getTime() > timeoutMs) {
console.log(`[RCS] Environment ${env.id} timed out (no poll for ${Math.round((now - env.lastPollAt.getTime()) / 1000)}s)`);
storeUpdateEnvironment(env.id, { status: "disconnected" });
}
}
// Check session timeout (2x disconnect timeout with no update)
const sessions = storeListSessions();
for (const session of sessions) {
if (session.status === "running" || session.status === "idle") {
const elapsed = now - session.updatedAt.getTime();
if (elapsed > timeoutMs * 2) {
console.log(`[RCS] Session ${session.id} marked inactive (no update for ${Math.round(elapsed / 1000)}s)`);
storeUpdateSession(session.id, { status: "inactive" });
}
}
}
runDisconnectMonitorSweep();
}, 60_000); // Check every minute
}

View File

@@ -1,14 +1,20 @@
import {
storeCreateSession,
storeGetSession,
storeIsSessionOwner,
storeUpdateSession,
storeListSessions,
storeListSessionsByUsername,
storeListSessionsByEnvironment,
storeListSessionsByOwnerUuid,
} from "../store";
import { removeEventBus } from "../transport/event-bus";
import { getAllEventBuses, removeEventBus } from "../transport/event-bus";
import type { CreateSessionRequest, CreateCodeSessionRequest, SessionResponse, SessionSummaryResponse } from "../types/api";
import { v4 as uuid } from "uuid";
const CODE_SESSION_PREFIX = "cse_";
const WEB_SESSION_PREFIX = "session_";
const CLOSED_SESSION_STATUSES = new Set(["archived", "inactive"]);
function toResponse(row: { id: string; environmentId: string | null; title: string | null; status: string; source: string; permissionMode: string | null; workerEpoch: number; username: string | null; createdAt: Date; updatedAt: Date }): SessionResponse {
return {
@@ -25,6 +31,24 @@ function toResponse(row: { id: string; environmentId: string | null; title: stri
};
}
export function toWebSessionId(sessionId: string): string {
if (!sessionId.startsWith(CODE_SESSION_PREFIX)) return sessionId;
return `${WEB_SESSION_PREFIX}${sessionId.slice(CODE_SESSION_PREFIX.length)}`;
}
function toCompatibleCodeSessionId(sessionId: string): string | null {
if (!sessionId.startsWith(WEB_SESSION_PREFIX)) return null;
return `${CODE_SESSION_PREFIX}${sessionId.slice(WEB_SESSION_PREFIX.length)}`;
}
export function toWebSessionResponse(session: SessionResponse): SessionResponse {
return { ...session, id: toWebSessionId(session.id) };
}
function toWebSessionSummaryResponse(session: SessionSummaryResponse): SessionSummaryResponse {
return { ...session, id: toWebSessionId(session.id) };
}
export function createSession(req: CreateSessionRequest & { username?: string }): SessionResponse {
const record = storeCreateSession({
environmentId: req.environment_id,
@@ -51,16 +75,78 @@ export function getSession(sessionId: string): SessionResponse | null {
return record ? toResponse(record) : null;
}
export function isSessionClosedStatus(status: string | null | undefined): boolean {
return !!status && CLOSED_SESSION_STATUSES.has(status);
}
export function resolveExistingSessionId(sessionId: string): string | null {
if (storeGetSession(sessionId)) {
return sessionId;
}
const compatibleCodeSessionId = toCompatibleCodeSessionId(sessionId);
if (compatibleCodeSessionId && storeGetSession(compatibleCodeSessionId)) {
return compatibleCodeSessionId;
}
return null;
}
export function resolveExistingWebSessionId(sessionId: string): string | null {
return resolveExistingSessionId(sessionId);
}
export function resolveOwnedWebSessionId(sessionId: string, uuid: string): string | null {
if (storeIsSessionOwner(sessionId, uuid)) {
return sessionId;
}
const compatibleCodeSessionId = toCompatibleCodeSessionId(sessionId);
if (compatibleCodeSessionId && storeIsSessionOwner(compatibleCodeSessionId, uuid)) {
return compatibleCodeSessionId;
}
return null;
}
export function listWebSessionsByOwnerUuid(uuid: string): SessionResponse[] {
return storeListSessionsByOwnerUuid(uuid)
.filter((session) => !isSessionClosedStatus(session.status))
.map(toResponse)
.map(toWebSessionResponse);
}
export function listWebSessionSummariesByOwnerUuid(uuid: string): SessionSummaryResponse[] {
return storeListSessionsByOwnerUuid(uuid)
.filter((session) => !isSessionClosedStatus(session.status))
.map(toSummaryResponse)
.map(toWebSessionSummaryResponse);
}
export function updateSessionTitle(sessionId: string, title: string) {
storeUpdateSession(sessionId, { title });
}
export function updateSessionStatus(sessionId: string, status: string) {
storeUpdateSession(sessionId, { status });
const bus = getAllEventBuses().get(sessionId);
if (!bus) return;
bus.publish({
id: uuid(),
sessionId,
type: "session_status",
payload: { status },
direction: "inbound",
});
}
export function touchSession(sessionId: string) {
storeUpdateSession(sessionId, {});
}
export function archiveSession(sessionId: string) {
storeUpdateSession(sessionId, { status: "archived" });
updateSessionStatus(sessionId, "archived");
removeEventBus(sessionId);
}

View File

@@ -51,6 +51,8 @@ export function normalizePayload(type: string, payload: unknown): Record<string,
raw: payload,
};
if (typeof p.uuid === "string" && p.uuid) normalized.uuid = p.uuid;
// Preserve tool fields
if (p.tool_name) normalized.tool_name = p.tool_name;
if (p.name) normalized.tool_name = p.name;