mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-12 07:20:45 +00:00
refactor(webchat): extract shared chat state helpers
This commit is contained in:
@@ -17,6 +17,17 @@ type GatewayClientMock = {
|
||||
const gatewayClientInstances: GatewayClientMock[] = [];
|
||||
|
||||
vi.mock("./gateway.ts", () => {
|
||||
function resolveGatewayErrorDetailCode(
|
||||
error: { details?: unknown } | null | undefined,
|
||||
): string | null {
|
||||
const details = error?.details;
|
||||
if (!details || typeof details !== "object") {
|
||||
return null;
|
||||
}
|
||||
const code = (details as { code?: unknown }).code;
|
||||
return typeof code === "string" ? code : null;
|
||||
}
|
||||
|
||||
class GatewayBrowserClient {
|
||||
readonly start = vi.fn();
|
||||
readonly stop = vi.fn();
|
||||
@@ -52,7 +63,7 @@ vi.mock("./gateway.ts", () => {
|
||||
}
|
||||
}
|
||||
|
||||
return { GatewayBrowserClient };
|
||||
return { GatewayBrowserClient, resolveGatewayErrorDetailCode };
|
||||
});
|
||||
|
||||
function createHost() {
|
||||
|
||||
@@ -217,6 +217,42 @@ export function handleGatewayEvent(host: GatewayHost, evt: GatewayEventFrame) {
|
||||
}
|
||||
}
|
||||
|
||||
function handleTerminalChatEvent(
|
||||
host: GatewayHost,
|
||||
payload: ChatEventPayload | undefined,
|
||||
state: ReturnType<typeof handleChatEvent>,
|
||||
) {
|
||||
if (state !== "final" && state !== "error" && state !== "aborted") {
|
||||
return;
|
||||
}
|
||||
resetToolStream(host as unknown as Parameters<typeof resetToolStream>[0]);
|
||||
void flushChatQueueForEvent(host as unknown as Parameters<typeof flushChatQueueForEvent>[0]);
|
||||
const runId = payload?.runId;
|
||||
if (!runId || !host.refreshSessionsAfterChat.has(runId)) {
|
||||
return;
|
||||
}
|
||||
host.refreshSessionsAfterChat.delete(runId);
|
||||
if (state === "final") {
|
||||
void loadSessions(host as unknown as OpenClawApp, {
|
||||
activeMinutes: CHAT_SESSIONS_ACTIVE_MINUTES,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function handleChatGatewayEvent(host: GatewayHost, payload: ChatEventPayload | undefined) {
|
||||
if (payload?.sessionKey) {
|
||||
setLastActiveSessionKey(
|
||||
host as unknown as Parameters<typeof setLastActiveSessionKey>[0],
|
||||
payload.sessionKey,
|
||||
);
|
||||
}
|
||||
const state = handleChatEvent(host as unknown as OpenClawApp, payload);
|
||||
handleTerminalChatEvent(host, payload, state);
|
||||
if (state === "final" && shouldReloadHistoryForFinalEvent(payload)) {
|
||||
void loadChatHistory(host as unknown as OpenClawApp);
|
||||
}
|
||||
}
|
||||
|
||||
function handleGatewayEventUnsafe(host: GatewayHost, evt: GatewayEventFrame) {
|
||||
host.eventLogBuffer = [
|
||||
{ ts: Date.now(), event: evt.event, payload: evt.payload },
|
||||
@@ -238,30 +274,7 @@ function handleGatewayEventUnsafe(host: GatewayHost, evt: GatewayEventFrame) {
|
||||
}
|
||||
|
||||
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 OpenClawApp, 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]);
|
||||
const runId = payload?.runId;
|
||||
if (runId && host.refreshSessionsAfterChat.has(runId)) {
|
||||
host.refreshSessionsAfterChat.delete(runId);
|
||||
if (state === "final") {
|
||||
void loadSessions(host as unknown as OpenClawApp, {
|
||||
activeMinutes: CHAT_SESSIONS_ACTIVE_MINUTES,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
if (state === "final" && shouldReloadHistoryForFinalEvent(payload)) {
|
||||
void loadChatHistory(host as unknown as OpenClawApp);
|
||||
}
|
||||
handleChatGatewayEvent(host, evt.payload as ChatEventPayload | undefined);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -58,33 +58,53 @@ function dataUrlToBase64(dataUrl: string): { content: string; mimeType: string }
|
||||
return { mimeType: match[1], content: match[2] };
|
||||
}
|
||||
|
||||
function normalizeAbortedAssistantMessage(message: unknown): Record<string, unknown> | null {
|
||||
type AssistantMessageNormalizationOptions = {
|
||||
roleRequirement: "required" | "optional";
|
||||
roleCaseSensitive?: boolean;
|
||||
requireContentArray?: boolean;
|
||||
allowTextField?: boolean;
|
||||
};
|
||||
|
||||
function normalizeAssistantMessage(
|
||||
message: unknown,
|
||||
options: AssistantMessageNormalizationOptions,
|
||||
): Record<string, unknown> | null {
|
||||
if (!message || typeof message !== "object") {
|
||||
return null;
|
||||
}
|
||||
const candidate = message as Record<string, unknown>;
|
||||
if (candidate.role !== "assistant") {
|
||||
const roleValue = candidate.role;
|
||||
if (typeof roleValue === "string") {
|
||||
const role = options.roleCaseSensitive ? roleValue : roleValue.toLowerCase();
|
||||
if (role !== "assistant") {
|
||||
return null;
|
||||
}
|
||||
} else if (options.roleRequirement === "required") {
|
||||
return null;
|
||||
}
|
||||
if (!("content" in candidate) || !Array.isArray(candidate.content)) {
|
||||
|
||||
if (options.requireContentArray) {
|
||||
return Array.isArray(candidate.content) ? candidate : null;
|
||||
}
|
||||
if (!("content" in candidate) && !(options.allowTextField && "text" in candidate)) {
|
||||
return null;
|
||||
}
|
||||
return candidate;
|
||||
}
|
||||
|
||||
function normalizeAbortedAssistantMessage(message: unknown): Record<string, unknown> | null {
|
||||
return normalizeAssistantMessage(message, {
|
||||
roleRequirement: "required",
|
||||
roleCaseSensitive: true,
|
||||
requireContentArray: true,
|
||||
});
|
||||
}
|
||||
|
||||
function normalizeFinalAssistantMessage(message: unknown): Record<string, unknown> | null {
|
||||
if (!message || typeof message !== "object") {
|
||||
return null;
|
||||
}
|
||||
const candidate = message as Record<string, unknown>;
|
||||
const role = typeof candidate.role === "string" ? candidate.role.toLowerCase() : "";
|
||||
if (role && role !== "assistant") {
|
||||
return null;
|
||||
}
|
||||
if (!("content" in candidate) && !("text" in candidate)) {
|
||||
return null;
|
||||
}
|
||||
return candidate;
|
||||
return normalizeAssistantMessage(message, {
|
||||
roleRequirement: "optional",
|
||||
allowTextField: true,
|
||||
});
|
||||
}
|
||||
|
||||
export async function sendChatMessage(
|
||||
|
||||
Reference in New Issue
Block a user