Files
deer-flow/frontend/src/core/api/api-client.ts
T
zgenu 737abc0e45 fix: ignore stale run reconnect conflicts (#3284)
* fix: ignore stale run reconnect conflicts

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* fix: ignore stale run reconnect conflicts

---------

Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-05-28 17:29:30 +08:00

172 lines
5.2 KiB
TypeScript

"use client";
import { Client as LangGraphClient } from "@langchain/langgraph-sdk/client";
import { getLangGraphBaseURL } from "../config";
import { isStaticWebsiteOnly } from "../static-mode";
import {
loadStaticDemoThread,
loadStaticDemoThreads,
staticDemoThreadState,
} from "../threads/static-demo";
import type { AgentThreadState } from "../threads/types";
import { isStateChangingMethod, readCsrfCookie } from "./fetcher";
import { sanitizeRunStreamOptions } from "./stream-mode";
/**
* SDK ``onRequest`` hook that mints the ``X-CSRF-Token`` header from the
* live ``csrf_token`` cookie just before each outbound fetch.
*
* Reading the cookie per-request (rather than baking it into the SDK's
* ``defaultHeaders`` at construction) handles login / logout / password
* change cookie rotation transparently. Both the ``/api/langgraph/*`` SDK
* path and the direct REST endpoints in ``fetcher.ts:fetchWithAuth``
* share :func:`readCsrfCookie` and :const:`STATE_CHANGING_METHODS` so
* the contract stays in lockstep.
*/
function injectCsrfHeader(_url: URL, init: RequestInit): RequestInit {
if (!isStateChangingMethod(init.method ?? "GET")) {
return init;
}
const token = readCsrfCookie();
if (!token) return init;
const headers = new Headers(init.headers);
if (!headers.has("X-CSRF-Token")) {
headers.set("X-CSRF-Token", token);
}
return { ...init, headers };
}
export function isInactiveRunStreamError(error: unknown): boolean {
const status =
typeof error === "object" && error !== null
? Reflect.get(error, "status")
: undefined;
const message =
typeof error === "string"
? error
: error instanceof Error
? error.message
: typeof error === "object" && error !== null
? String(Reflect.get(error, "message") ?? "")
: "";
// Match the gateway's store-only run response in
// backend/app/gateway/routers/thread_runs.py until the API exposes a
// structured error code for inactive run streams.
return (
(status === 409 || message.includes("HTTP 409")) &&
message.includes("not active on this worker") &&
message.includes("cannot be streamed")
);
}
export function clearReconnectRun(
threadId: string | null | undefined,
runId: string,
): void {
if (typeof window === "undefined" || !threadId) return;
const key = `lg:stream:${threadId}`;
try {
const storage = window.sessionStorage;
if (storage.getItem(key) === runId) {
storage.removeItem(key);
}
} catch {
// Ignore storage access failures so reconnect cleanup never throws.
}
}
function createCompatibleClient(isMock?: boolean): LangGraphClient {
if (isStaticWebsiteOnly() && !isMock) {
return createStaticClient();
}
const apiUrl = getLangGraphBaseURL(isMock);
console.log(`Creating API client with base URL: ${apiUrl}`);
const client = new LangGraphClient({
apiUrl,
onRequest: injectCsrfHeader,
});
const originalRunStream = client.runs.stream.bind(client.runs);
client.runs.stream = ((threadId, assistantId, payload) =>
originalRunStream(
threadId,
assistantId,
sanitizeRunStreamOptions(payload),
)) as typeof client.runs.stream;
const originalJoinStream = client.runs.joinStream.bind(client.runs);
client.runs.joinStream = async function* (threadId, runId, options) {
try {
yield* originalJoinStream(
threadId,
runId,
sanitizeRunStreamOptions(options),
);
} catch (error) {
if (isInactiveRunStreamError(error)) {
clearReconnectRun(threadId, runId);
return;
}
throw error;
}
} as typeof client.runs.joinStream;
return client;
}
function createStaticClient(): LangGraphClient {
const apiUrl =
typeof window === "undefined"
? "http://localhost:3000"
: window.location.origin;
const client = new LangGraphClient({ apiUrl });
client.threads.search = (async (query) => {
return loadStaticDemoThreads(query);
}) as typeof client.threads.search;
client.threads.get = (async (threadId) => {
return loadStaticDemoThread(threadId);
}) as typeof client.threads.get;
client.threads.getState = (async (threadId) => {
return staticDemoThreadState(await loadStaticDemoThread(threadId));
}) as typeof client.threads.getState;
client.threads.getHistory = (async (threadId) => {
return [staticDemoThreadState(await loadStaticDemoThread(threadId))];
}) as typeof client.threads.getHistory;
client.threads.update = (async (threadId) => {
return loadStaticDemoThread(threadId);
}) as typeof client.threads.update;
client.runs.list = (async () => []) as typeof client.runs.list;
client.runs.stream = async function* () {
/* empty */
} as typeof client.runs.stream;
client.runs.joinStream = async function* () {
/* empty */
} as typeof client.runs.joinStream;
return client as LangGraphClient<AgentThreadState>;
}
const _clients = new Map<string, LangGraphClient>();
export function getAPIClient(isMock?: boolean): LangGraphClient {
const cacheKey = isMock ? "mock" : "default";
let client = _clients.get(cacheKey);
if (!client) {
client = createCompatibleClient(isMock);
_clients.set(cacheKey, client);
}
return client;
}