mirror of
https://github.com/HKUDS/nanobot.git
synced 2026-08-19 02:26:12 +03:00
fix(webui): move mutations to authenticated websocket requests
This commit is contained in:
@@ -108,6 +108,16 @@ interface PendingRequest<T> {
|
||||
timer: ReturnType<typeof setTimeout>;
|
||||
}
|
||||
|
||||
export class WebUIMutationError extends Error {
|
||||
status: number;
|
||||
|
||||
constructor(status: number, message: string) {
|
||||
super(message);
|
||||
this.status = status;
|
||||
this.name = "WebUIMutationError";
|
||||
}
|
||||
}
|
||||
|
||||
interface PendingChatRequest extends PendingRequest<string> {
|
||||
temporary: boolean;
|
||||
}
|
||||
@@ -203,6 +213,7 @@ export class NanobotClient {
|
||||
private pendingNewChat: PendingChatRequest | null = null;
|
||||
private pendingTranscriptions = new Map<string, PendingRequest<string>>();
|
||||
private pendingSystemCommands = new Map<string, PendingRequest<void>>();
|
||||
private pendingWebUIRequests = new Map<string, PendingRequest<unknown>>();
|
||||
// Frames queued while the socket is not yet OPEN
|
||||
private sendQueue: Outbound[] = [];
|
||||
private reconnectAttempts = 0;
|
||||
@@ -807,6 +818,60 @@ export class NanobotClient {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Send one non-replayable WebUI mutation over the authenticated socket.
|
||||
* A client-side timeout only abandons the reply; the server may finish work
|
||||
* that already started, so timed-out requests are never retried automatically.
|
||||
*/
|
||||
requestMutation<T>(
|
||||
action: string,
|
||||
payload: Record<string, unknown> = {},
|
||||
timeoutMs: number = 20_000,
|
||||
): Promise<T> {
|
||||
const socket = this.socket;
|
||||
if (!socket || socket.readyState !== WS_OPEN) {
|
||||
return Promise.reject(
|
||||
new WebUIMutationError(503, "WebUI connection is not open"),
|
||||
);
|
||||
}
|
||||
const requestId = crypto.randomUUID();
|
||||
const frame: Outbound = {
|
||||
type: "webui_request",
|
||||
request_id: requestId,
|
||||
action,
|
||||
payload,
|
||||
};
|
||||
if (!this.frameFitsTransport(frame)) {
|
||||
return Promise.reject(
|
||||
new WebUIMutationError(413, "WebUI mutation payload is too large"),
|
||||
);
|
||||
}
|
||||
|
||||
return new Promise<T>((resolve, reject) => {
|
||||
const timer = setTimeout(() => {
|
||||
this.pendingWebUIRequests.delete(requestId);
|
||||
reject(
|
||||
new WebUIMutationError(
|
||||
504,
|
||||
`WebUI request timed out after ${timeoutMs}ms`,
|
||||
),
|
||||
);
|
||||
}, timeoutMs);
|
||||
this.pendingWebUIRequests.set(requestId, {
|
||||
resolve: (value) => resolve(value as T),
|
||||
reject,
|
||||
timer,
|
||||
});
|
||||
try {
|
||||
socket.send(JSON.stringify(frame));
|
||||
} catch {
|
||||
clearTimeout(timer);
|
||||
this.pendingWebUIRequests.delete(requestId);
|
||||
reject(new WebUIMutationError(503, "Could not send WebUI request"));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/** Ask the server to create a non-destructive fork before a user-message index. */
|
||||
forkChat(
|
||||
sourceChatId: string,
|
||||
@@ -914,8 +979,8 @@ export class NanobotClient {
|
||||
});
|
||||
}
|
||||
|
||||
setSidebarState(state: SidebarStatePayload): void {
|
||||
this.queueSend({ type: "set_sidebar_state", state });
|
||||
setSidebarState(state: SidebarStatePayload): Promise<SidebarStatePayload> {
|
||||
return this.requestMutation<SidebarStatePayload>("sidebar.update", { state });
|
||||
}
|
||||
|
||||
// -- internals ---------------------------------------------------------
|
||||
@@ -965,6 +1030,23 @@ export class NanobotClient {
|
||||
console.log("[nanobot ws inbound]", summarizeInboundWsPayload(parsed));
|
||||
}
|
||||
|
||||
if (parsed.event === "webui_response") {
|
||||
const pending = this.pendingWebUIRequests.get(parsed.request_id);
|
||||
if (!pending) return;
|
||||
clearTimeout(pending.timer);
|
||||
this.pendingWebUIRequests.delete(parsed.request_id);
|
||||
if (parsed.ok) {
|
||||
pending.resolve(parsed.result);
|
||||
} else {
|
||||
const status = Number.isFinite(parsed.error?.status)
|
||||
? parsed.error.status
|
||||
: 500;
|
||||
const message = parsed.error?.message || "WebUI mutation failed";
|
||||
pending.reject(new WebUIMutationError(status, message));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (parsed.event === "error" && !parsed.turn_id) {
|
||||
const fallback = this.legacyRejectionTarget(parsed);
|
||||
if (fallback) {
|
||||
@@ -1151,6 +1233,13 @@ export class NanobotClient {
|
||||
this.pendingNewChat = null;
|
||||
}
|
||||
this.rejectAllTranscriptions("socket closed");
|
||||
for (const pending of this.pendingWebUIRequests.values()) {
|
||||
clearTimeout(pending.timer);
|
||||
pending.reject(
|
||||
new WebUIMutationError(503, "Socket closed before WebUI response"),
|
||||
);
|
||||
}
|
||||
this.pendingWebUIRequests.clear();
|
||||
for (const pending of this.pendingSystemCommands.values()) {
|
||||
clearTimeout(pending.timer);
|
||||
pending.reject(new Error("socket closed"));
|
||||
|
||||
Reference in New Issue
Block a user