249 lines
8.3 KiB
TypeScript
249 lines
8.3 KiB
TypeScript
import { loadChatHistory } from "./controllers/chat";
|
|
import { loadDevices } from "./controllers/devices";
|
|
import { loadNodes } from "./controllers/nodes";
|
|
import { loadAgents } from "./controllers/agents";
|
|
import type { GatewayEventFrame, GatewayHelloOk } from "./gateway";
|
|
import { GatewayBrowserClient } from "./gateway";
|
|
import type { EventLogEntry } from "./app-events";
|
|
import type { AgentsListResult, PresenceEntry, HealthSnapshot, StatusSummary } from "./types";
|
|
import type { Tab } from "./navigation";
|
|
import type { UiSettings } from "./storage";
|
|
import { handleAgentEvent, resetToolStream, type AgentEventPayload } from "./app-tool-stream";
|
|
import { flushChatQueueForEvent } from "./app-chat";
|
|
import {
|
|
applySettings,
|
|
loadCron,
|
|
refreshActiveTab,
|
|
setLastActiveSessionKey,
|
|
} from "./app-settings";
|
|
import { handleChatEvent, type ChatEventPayload } from "./controllers/chat";
|
|
import {
|
|
addExecApproval,
|
|
parseExecApprovalRequested,
|
|
parseExecApprovalResolved,
|
|
removeExecApproval,
|
|
} from "./controllers/exec-approval";
|
|
import type { ClawdbotApp } from "./app";
|
|
import type { ExecApprovalRequest } from "./controllers/exec-approval";
|
|
import { loadAssistantIdentity } from "./controllers/assistant-identity";
|
|
|
|
type GatewayHost = {
|
|
settings: UiSettings;
|
|
password: string;
|
|
client: GatewayBrowserClient | null;
|
|
connected: boolean;
|
|
hello: GatewayHelloOk | null;
|
|
lastError: string | null;
|
|
onboarding?: boolean;
|
|
eventLogBuffer: EventLogEntry[];
|
|
eventLog: EventLogEntry[];
|
|
tab: Tab;
|
|
presenceEntries: PresenceEntry[];
|
|
presenceError: string | null;
|
|
presenceStatus: StatusSummary | null;
|
|
agentsLoading: boolean;
|
|
agentsList: AgentsListResult | null;
|
|
agentsError: string | null;
|
|
debugHealth: HealthSnapshot | null;
|
|
assistantName: string;
|
|
assistantAvatar: string | null;
|
|
assistantAgentId: string | null;
|
|
sessionKey: string;
|
|
chatRunId: string | null;
|
|
execApprovalQueue: ExecApprovalRequest[];
|
|
execApprovalError: string | null;
|
|
};
|
|
|
|
type SessionDefaultsSnapshot = {
|
|
defaultAgentId?: string;
|
|
mainKey?: string;
|
|
mainSessionKey?: string;
|
|
scope?: string;
|
|
};
|
|
|
|
function normalizeSessionKeyForDefaults(
|
|
value: string | undefined,
|
|
defaults: SessionDefaultsSnapshot,
|
|
): string {
|
|
const raw = (value ?? "").trim();
|
|
const mainSessionKey = defaults.mainSessionKey?.trim();
|
|
if (!mainSessionKey) return raw;
|
|
if (!raw) return mainSessionKey;
|
|
const mainKey = defaults.mainKey?.trim() || "main";
|
|
const defaultAgentId = defaults.defaultAgentId?.trim();
|
|
const isAlias =
|
|
raw === "main" ||
|
|
raw === mainKey ||
|
|
(defaultAgentId &&
|
|
(raw === `agent:${defaultAgentId}:main` ||
|
|
raw === `agent:${defaultAgentId}:${mainKey}`));
|
|
return isAlias ? mainSessionKey : raw;
|
|
}
|
|
|
|
function applySessionDefaults(host: GatewayHost, defaults?: SessionDefaultsSnapshot) {
|
|
if (!defaults?.mainSessionKey) return;
|
|
const resolvedSessionKey = normalizeSessionKeyForDefaults(host.sessionKey, defaults);
|
|
const resolvedSettingsSessionKey = normalizeSessionKeyForDefaults(
|
|
host.settings.sessionKey,
|
|
defaults,
|
|
);
|
|
const resolvedLastActiveSessionKey = normalizeSessionKeyForDefaults(
|
|
host.settings.lastActiveSessionKey,
|
|
defaults,
|
|
);
|
|
const nextSessionKey = resolvedSessionKey || resolvedSettingsSessionKey || host.sessionKey;
|
|
const nextSettings = {
|
|
...host.settings,
|
|
sessionKey: resolvedSettingsSessionKey || nextSessionKey,
|
|
lastActiveSessionKey: resolvedLastActiveSessionKey || nextSessionKey,
|
|
};
|
|
const shouldUpdateSettings =
|
|
nextSettings.sessionKey !== host.settings.sessionKey ||
|
|
nextSettings.lastActiveSessionKey !== host.settings.lastActiveSessionKey;
|
|
if (nextSessionKey !== host.sessionKey) {
|
|
host.sessionKey = nextSessionKey;
|
|
}
|
|
if (shouldUpdateSettings) {
|
|
applySettings(host as unknown as Parameters<typeof applySettings>[0], nextSettings);
|
|
}
|
|
}
|
|
|
|
export function connectGateway(host: GatewayHost) {
|
|
host.lastError = null;
|
|
host.hello = null;
|
|
host.connected = false;
|
|
host.execApprovalQueue = [];
|
|
host.execApprovalError = null;
|
|
|
|
host.client?.stop();
|
|
host.client = new GatewayBrowserClient({
|
|
url: host.settings.gatewayUrl,
|
|
token: host.settings.token.trim() ? host.settings.token : undefined,
|
|
password: host.password.trim() ? host.password : undefined,
|
|
clientName: "clawdbot-control-ui",
|
|
mode: "webchat",
|
|
onHello: (hello) => {
|
|
host.connected = true;
|
|
host.hello = hello;
|
|
applySnapshot(host, hello);
|
|
void loadAssistantIdentity(host as unknown as ClawdbotApp);
|
|
void loadAgents(host as unknown as ClawdbotApp);
|
|
void loadNodes(host as unknown as ClawdbotApp, { quiet: true });
|
|
void loadDevices(host as unknown as ClawdbotApp, { quiet: true });
|
|
void refreshActiveTab(host as unknown as Parameters<typeof refreshActiveTab>[0]);
|
|
},
|
|
onClose: ({ code, reason }) => {
|
|
host.connected = false;
|
|
host.lastError = `disconnected (${code}): ${reason || "no reason"}`;
|
|
},
|
|
onEvent: (evt) => handleGatewayEvent(host, evt),
|
|
onGap: ({ expected, received }) => {
|
|
host.lastError = `event gap detected (expected seq ${expected}, got ${received}); refresh recommended`;
|
|
},
|
|
});
|
|
host.client.start();
|
|
}
|
|
|
|
export function handleGatewayEvent(host: GatewayHost, evt: GatewayEventFrame) {
|
|
try {
|
|
handleGatewayEventUnsafe(host, evt);
|
|
} catch (err) {
|
|
console.error("[gateway] handleGatewayEvent error:", evt.event, err);
|
|
}
|
|
}
|
|
|
|
function handleGatewayEventUnsafe(host: GatewayHost, evt: GatewayEventFrame) {
|
|
host.eventLogBuffer = [
|
|
{ ts: Date.now(), event: evt.event, payload: evt.payload },
|
|
...host.eventLogBuffer,
|
|
].slice(0, 250);
|
|
if (host.tab === "debug") {
|
|
host.eventLog = host.eventLogBuffer;
|
|
}
|
|
|
|
if (evt.event === "agent") {
|
|
if (host.onboarding) return;
|
|
handleAgentEvent(
|
|
host as unknown as Parameters<typeof handleAgentEvent>[0],
|
|
evt.payload as AgentEventPayload | undefined,
|
|
);
|
|
return;
|
|
}
|
|
|
|
if (evt.event === "chat") {
|
|
const payload = evt.payload as ChatEventPayload | undefined;
|
|
if (payload?.sessionKey) {
|
|
setLastActiveSessionKey(
|
|
host as unknown as Parameters<typeof setLastActiveSessionKey>[0],
|
|
payload.sessionKey,
|
|
);
|
|
}
|
|
const state = handleChatEvent(host as unknown as ClawdbotApp, payload);
|
|
if (state === "final" || state === "error" || state === "aborted") {
|
|
resetToolStream(host as unknown as Parameters<typeof resetToolStream>[0]);
|
|
void flushChatQueueForEvent(
|
|
host as unknown as Parameters<typeof flushChatQueueForEvent>[0],
|
|
);
|
|
}
|
|
if (state === "final") void loadChatHistory(host as unknown as ClawdbotApp);
|
|
return;
|
|
}
|
|
|
|
if (evt.event === "presence") {
|
|
const payload = evt.payload as { presence?: PresenceEntry[] } | undefined;
|
|
if (payload?.presence && Array.isArray(payload.presence)) {
|
|
host.presenceEntries = payload.presence;
|
|
host.presenceError = null;
|
|
host.presenceStatus = null;
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (evt.event === "cron" && host.tab === "cron") {
|
|
void loadCron(host as unknown as Parameters<typeof loadCron>[0]);
|
|
}
|
|
|
|
if (evt.event === "device.pair.requested" || evt.event === "device.pair.resolved") {
|
|
void loadDevices(host as unknown as ClawdbotApp, { quiet: true });
|
|
}
|
|
|
|
if (evt.event === "exec.approval.requested") {
|
|
const entry = parseExecApprovalRequested(evt.payload);
|
|
if (entry) {
|
|
host.execApprovalQueue = addExecApproval(host.execApprovalQueue, entry);
|
|
host.execApprovalError = null;
|
|
const delay = Math.max(0, entry.expiresAtMs - Date.now() + 500);
|
|
window.setTimeout(() => {
|
|
host.execApprovalQueue = removeExecApproval(host.execApprovalQueue, entry.id);
|
|
}, delay);
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (evt.event === "exec.approval.resolved") {
|
|
const resolved = parseExecApprovalResolved(evt.payload);
|
|
if (resolved) {
|
|
host.execApprovalQueue = removeExecApproval(host.execApprovalQueue, resolved.id);
|
|
}
|
|
}
|
|
}
|
|
|
|
export function applySnapshot(host: GatewayHost, hello: GatewayHelloOk) {
|
|
const snapshot = hello.snapshot as
|
|
| {
|
|
presence?: PresenceEntry[];
|
|
health?: HealthSnapshot;
|
|
sessionDefaults?: SessionDefaultsSnapshot;
|
|
}
|
|
| undefined;
|
|
if (snapshot?.presence && Array.isArray(snapshot.presence)) {
|
|
host.presenceEntries = snapshot.presence;
|
|
}
|
|
if (snapshot?.health) {
|
|
host.debugHealth = snapshot.health;
|
|
}
|
|
if (snapshot?.sessionDefaults) {
|
|
applySessionDefaults(host, snapshot.sessionDefaults);
|
|
}
|
|
}
|