refactor: use backend chat sessions
This commit is contained in:
@@ -165,9 +165,6 @@ export const AgentHistoryPanel = ({
|
|||||||
<Typography variant="subtitle2" fontWeight={800} color="text.primary">
|
<Typography variant="subtitle2" fontWeight={800} color="text.primary">
|
||||||
历史会话
|
历史会话
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography variant="caption" color="text.secondary">
|
|
||||||
本地保存于浏览器
|
|
||||||
</Typography>
|
|
||||||
</Box>
|
</Box>
|
||||||
<Tooltip title="新建对话">
|
<Tooltip title="新建对话">
|
||||||
<motion.div whileHover={{ scale: 1.08 }} whileTap={{ scale: 0.92 }} style={{ display: "flex" }}>
|
<motion.div whileHover={{ scale: 1.08 }} whileTap={{ scale: 0.92 }} style={{ display: "flex" }}>
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ export const GlobalChatbox: React.FC<Props> = ({ open, onClose }) => {
|
|||||||
const {
|
const {
|
||||||
messages,
|
messages,
|
||||||
chatSessions,
|
chatSessions,
|
||||||
activeStorageSessionId,
|
activeSessionId,
|
||||||
branchGroups,
|
branchGroups,
|
||||||
branchTransition,
|
branchTransition,
|
||||||
isHydrating,
|
isHydrating,
|
||||||
@@ -129,33 +129,33 @@ export const GlobalChatbox: React.FC<Props> = ({ open, onClose }) => {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleSelectSession = useCallback(
|
const handleSelectSession = useCallback(
|
||||||
(storageSessionId: string) => {
|
(sessionId: string) => {
|
||||||
composerRef.current?.clear();
|
composerRef.current?.clear();
|
||||||
void switchSession(storageSessionId);
|
void switchSession(sessionId);
|
||||||
},
|
},
|
||||||
[switchSession],
|
[switchSession],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleDeleteSession = useCallback(
|
const handleDeleteSession = useCallback(
|
||||||
(storageSessionId: string) => {
|
(sessionId: string) => {
|
||||||
void removeSession(storageSessionId);
|
void removeSession(sessionId);
|
||||||
},
|
},
|
||||||
[removeSession],
|
[removeSession],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleRenameSession = useCallback(
|
const handleRenameSession = useCallback(
|
||||||
(storageSessionId: string, title: string) => {
|
(sessionId: string, title: string) => {
|
||||||
void renameSession(storageSessionId, title);
|
void renameSession(sessionId, title);
|
||||||
},
|
},
|
||||||
[renameSession],
|
[renameSession],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleRenameActiveSession = useCallback(
|
const handleRenameActiveSession = useCallback(
|
||||||
(title: string) => {
|
(title: string) => {
|
||||||
if (!activeStorageSessionId) return;
|
if (!activeSessionId) return;
|
||||||
void renameSession(activeStorageSessionId, title);
|
void renameSession(activeSessionId, title);
|
||||||
},
|
},
|
||||||
[activeStorageSessionId, renameSession],
|
[activeSessionId, renameSession],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleMouseDown = useCallback((event: React.MouseEvent) => {
|
const handleMouseDown = useCallback((event: React.MouseEvent) => {
|
||||||
@@ -255,7 +255,7 @@ export const GlobalChatbox: React.FC<Props> = ({ open, onClose }) => {
|
|||||||
|
|
||||||
<AgentHeader
|
<AgentHeader
|
||||||
sessionTitle={sessionTitle}
|
sessionTitle={sessionTitle}
|
||||||
canRenameSessionTitle={Boolean(activeStorageSessionId)}
|
canRenameSessionTitle={Boolean(activeSessionId)}
|
||||||
isHydrating={isHydrating}
|
isHydrating={isHydrating}
|
||||||
isStreaming={isStreaming}
|
isStreaming={isStreaming}
|
||||||
isHistoryOpen={isHistoryOpen}
|
isHistoryOpen={isHistoryOpen}
|
||||||
@@ -294,7 +294,7 @@ export const GlobalChatbox: React.FC<Props> = ({ open, onClose }) => {
|
|||||||
>
|
>
|
||||||
<AgentHistoryPanel
|
<AgentHistoryPanel
|
||||||
sessions={chatSessions}
|
sessions={chatSessions}
|
||||||
activeSessionId={activeStorageSessionId}
|
activeSessionId={activeSessionId}
|
||||||
isHydrating={isHydrating}
|
isHydrating={isHydrating}
|
||||||
onNewSession={() => {
|
onNewSession={() => {
|
||||||
handleNewConversation();
|
handleNewConversation();
|
||||||
|
|||||||
@@ -66,23 +66,6 @@ export type Props = {
|
|||||||
|
|
||||||
export type SpeechState = "idle" | "playing" | "paused";
|
export type SpeechState = "idle" | "playing" | "paused";
|
||||||
|
|
||||||
export type LegacyPersistedChatState = {
|
|
||||||
messages: Message[];
|
|
||||||
sessionId?: string;
|
|
||||||
branchGroups?: BranchGroup[];
|
|
||||||
};
|
|
||||||
|
|
||||||
export type ChatSessionRecord = {
|
|
||||||
id: string;
|
|
||||||
title: string;
|
|
||||||
isTitleManuallyEdited?: boolean;
|
|
||||||
createdAt: number;
|
|
||||||
updatedAt: number;
|
|
||||||
sessionId?: string;
|
|
||||||
messages: Message[];
|
|
||||||
branchGroups: BranchGroup[];
|
|
||||||
};
|
|
||||||
|
|
||||||
export type ChatSessionSummary = {
|
export type ChatSessionSummary = {
|
||||||
id: string;
|
id: string;
|
||||||
title: string;
|
title: string;
|
||||||
@@ -90,17 +73,10 @@ export type ChatSessionSummary = {
|
|||||||
updatedAt: number;
|
updatedAt: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ChatStorageMeta = {
|
|
||||||
key: "chat-meta";
|
|
||||||
activeSessionId?: string;
|
|
||||||
migratedFromLocalStorage?: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type LoadedChatState = {
|
export type LoadedChatState = {
|
||||||
storageSessionId?: string;
|
sessionId?: string;
|
||||||
title?: string;
|
title?: string;
|
||||||
isTitleManuallyEdited?: boolean;
|
isTitleManuallyEdited?: boolean;
|
||||||
messages: Message[];
|
messages: Message[];
|
||||||
sessionId?: string;
|
|
||||||
branchGroups: BranchGroup[];
|
branchGroups: BranchGroup[];
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import {
|
import {
|
||||||
loadActiveChatState,
|
createEmptyChatState,
|
||||||
saveActiveChatState,
|
saveActiveChatState,
|
||||||
} from "./chatStorage";
|
} from "./chatStorage";
|
||||||
|
|
||||||
@@ -11,17 +11,13 @@ jest.mock("@/lib/apiFetch", () => ({
|
|||||||
|
|
||||||
describe("chatStorage backend-only persistence", () => {
|
describe("chatStorage backend-only persistence", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
window.localStorage.clear();
|
|
||||||
apiFetch.mockReset();
|
apiFetch.mockReset();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("starts from an empty conversation instead of restoring a stored active id", async () => {
|
it("creates an empty initial conversation state without backend calls", () => {
|
||||||
window.localStorage.setItem("tjwater_agent_active_session_id_v2", "chat-active-1");
|
const loaded = createEmptyChatState();
|
||||||
|
|
||||||
const loaded = await loadActiveChatState();
|
|
||||||
|
|
||||||
expect(loaded).toMatchObject({
|
expect(loaded).toMatchObject({
|
||||||
storageSessionId: undefined,
|
|
||||||
title: undefined,
|
title: undefined,
|
||||||
messages: [],
|
messages: [],
|
||||||
sessionId: undefined,
|
sessionId: undefined,
|
||||||
@@ -30,24 +26,6 @@ describe("chatStorage backend-only persistence", () => {
|
|||||||
expect(apiFetch).not.toHaveBeenCalled();
|
expect(apiFetch).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("starts from an empty conversation when a project has a stored active id", async () => {
|
|
||||||
window.localStorage.setItem(
|
|
||||||
"tjwater_agent_active_session_id_v2:project-a",
|
|
||||||
"chat-project-a",
|
|
||||||
);
|
|
||||||
window.localStorage.setItem(
|
|
||||||
"tjwater_agent_active_session_id_v2:project-b",
|
|
||||||
"chat-project-b",
|
|
||||||
);
|
|
||||||
|
|
||||||
const loaded = await loadActiveChatState("project-b");
|
|
||||||
|
|
||||||
expect(loaded.storageSessionId).toBeUndefined();
|
|
||||||
expect(loaded.title).toBeUndefined();
|
|
||||||
expect(loaded.messages).toEqual([]);
|
|
||||||
expect(apiFetch).not.toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("creates a backend conversation when saving the first non-empty state", async () => {
|
it("creates a backend conversation when saving the first non-empty state", async () => {
|
||||||
apiFetch.mockImplementation(async (url: string, init?: RequestInit) => {
|
apiFetch.mockImplementation(async (url: string, init?: RequestInit) => {
|
||||||
if (url.endsWith("/api/v1/agent/chat/session")) {
|
if (url.endsWith("/api/v1/agent/chat/session")) {
|
||||||
@@ -75,7 +53,6 @@ describe("chatStorage backend-only persistence", () => {
|
|||||||
|
|
||||||
const savedSessionId = await saveActiveChatState(
|
const savedSessionId = await saveActiveChatState(
|
||||||
{
|
{
|
||||||
storageSessionId: undefined,
|
|
||||||
title: "新对话",
|
title: "新对话",
|
||||||
isTitleManuallyEdited: false,
|
isTitleManuallyEdited: false,
|
||||||
messages: [
|
messages: [
|
||||||
@@ -89,13 +66,8 @@ describe("chatStorage backend-only persistence", () => {
|
|||||||
sessionId: undefined,
|
sessionId: undefined,
|
||||||
branchGroups: [],
|
branchGroups: [],
|
||||||
},
|
},
|
||||||
"project-a",
|
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(savedSessionId).toBe("chat-new-1");
|
expect(savedSessionId).toBe("chat-new-1");
|
||||||
expect(
|
|
||||||
window.localStorage.getItem("tjwater_agent_active_session_id_v2:project-a"),
|
|
||||||
).toBeNull();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -9,15 +9,14 @@ import type {
|
|||||||
} from "./GlobalChatbox.types";
|
} from "./GlobalChatbox.types";
|
||||||
import { cloneBranchGroups, cloneMessages } from "./GlobalChatbox.utils";
|
import { cloneBranchGroups, cloneMessages } from "./GlobalChatbox.utils";
|
||||||
|
|
||||||
type RemoteSessionPayload = {
|
type BackendSessionPayload = {
|
||||||
id?: string;
|
id?: string;
|
||||||
title?: string;
|
title?: string;
|
||||||
created_at?: string | number;
|
created_at?: string | number;
|
||||||
updated_at?: string | number;
|
updated_at?: string | number;
|
||||||
};
|
};
|
||||||
|
|
||||||
const emptyLoadedChatState = (): LoadedChatState => ({
|
export const createEmptyChatState = (): LoadedChatState => ({
|
||||||
storageSessionId: undefined,
|
|
||||||
title: undefined,
|
title: undefined,
|
||||||
isTitleManuallyEdited: false,
|
isTitleManuallyEdited: false,
|
||||||
messages: [],
|
messages: [],
|
||||||
@@ -58,7 +57,7 @@ const toMillis = (value: string | number | undefined) =>
|
|||||||
|
|
||||||
const normalizeTitle = (value?: string) => value?.trim() || "新对话";
|
const normalizeTitle = (value?: string) => value?.trim() || "新对话";
|
||||||
|
|
||||||
const fetchRemoteChatSessions = async (): Promise<ChatSessionSummary[]> => {
|
const fetchBackendChatSessions = async (): Promise<ChatSessionSummary[]> => {
|
||||||
const response = await apiFetch(`${config.AGENT_URL}/api/v1/agent/chat/sessions`, {
|
const response = await apiFetch(`${config.AGENT_URL}/api/v1/agent/chat/sessions`, {
|
||||||
method: "GET",
|
method: "GET",
|
||||||
projectHeaderMode: "include",
|
projectHeaderMode: "include",
|
||||||
@@ -69,7 +68,7 @@ const fetchRemoteChatSessions = async (): Promise<ChatSessionSummary[]> => {
|
|||||||
throw new Error(await response.text());
|
throw new Error(await response.text());
|
||||||
}
|
}
|
||||||
const payload = (await response.json()) as {
|
const payload = (await response.json()) as {
|
||||||
sessions?: RemoteSessionPayload[];
|
sessions?: BackendSessionPayload[];
|
||||||
};
|
};
|
||||||
return (payload.sessions ?? [])
|
return (payload.sessions ?? [])
|
||||||
.map((session) => ({
|
.map((session) => ({
|
||||||
@@ -82,7 +81,7 @@ const fetchRemoteChatSessions = async (): Promise<ChatSessionSummary[]> => {
|
|||||||
.sort(compareSessionsByAnchorTime);
|
.sort(compareSessionsByAnchorTime);
|
||||||
};
|
};
|
||||||
|
|
||||||
const fetchRemoteChatSession = async (sessionId: string): Promise<LoadedChatState> => {
|
const fetchBackendChatSession = async (sessionId: string): Promise<LoadedChatState> => {
|
||||||
const response = await apiFetch(
|
const response = await apiFetch(
|
||||||
`${config.AGENT_URL}/api/v1/agent/chat/session/${encodeURIComponent(sessionId)}`,
|
`${config.AGENT_URL}/api/v1/agent/chat/session/${encodeURIComponent(sessionId)}`,
|
||||||
{
|
{
|
||||||
@@ -94,7 +93,7 @@ const fetchRemoteChatSession = async (sessionId: string): Promise<LoadedChatStat
|
|||||||
);
|
);
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
if (response.status === 404) {
|
if (response.status === 404) {
|
||||||
return emptyLoadedChatState();
|
return createEmptyChatState();
|
||||||
}
|
}
|
||||||
throw new Error(await response.text());
|
throw new Error(await response.text());
|
||||||
}
|
}
|
||||||
@@ -107,16 +106,15 @@ const fetchRemoteChatSession = async (sessionId: string): Promise<LoadedChatStat
|
|||||||
branch_groups?: BranchGroup[];
|
branch_groups?: BranchGroup[];
|
||||||
};
|
};
|
||||||
return {
|
return {
|
||||||
storageSessionId: payload.id,
|
|
||||||
title: normalizeTitle(payload.title),
|
title: normalizeTitle(payload.title),
|
||||||
isTitleManuallyEdited: payload.is_title_manually_edited ?? false,
|
isTitleManuallyEdited: payload.is_title_manually_edited ?? false,
|
||||||
messages: sanitizeMessages(payload.messages),
|
messages: sanitizeMessages(payload.messages),
|
||||||
sessionId: payload.session_id,
|
sessionId: payload.session_id ?? payload.id,
|
||||||
branchGroups: sanitizeBranchGroups(payload.branch_groups),
|
branchGroups: sanitizeBranchGroups(payload.branch_groups),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const createRemoteChatSession = async (payload?: {
|
const createBackendChatSession = async (payload?: {
|
||||||
sessionId?: string;
|
sessionId?: string;
|
||||||
parentSessionId?: string;
|
parentSessionId?: string;
|
||||||
}) => {
|
}) => {
|
||||||
@@ -146,7 +144,7 @@ const createRemoteChatSession = async (payload?: {
|
|||||||
return sessionId;
|
return sessionId;
|
||||||
};
|
};
|
||||||
|
|
||||||
const saveRemoteChatState = async (
|
const saveBackendChatState = async (
|
||||||
sessionId: string,
|
sessionId: string,
|
||||||
state: LoadedChatState,
|
state: LoadedChatState,
|
||||||
): Promise<string> => {
|
): Promise<string> => {
|
||||||
@@ -175,7 +173,7 @@ const saveRemoteChatState = async (
|
|||||||
return payload.id ?? payload.session_id ?? sessionId;
|
return payload.id ?? payload.session_id ?? sessionId;
|
||||||
};
|
};
|
||||||
|
|
||||||
const updateRemoteChatSessionTitle = async (
|
const updateBackendChatSessionTitle = async (
|
||||||
sessionId: string,
|
sessionId: string,
|
||||||
title: string,
|
title: string,
|
||||||
isTitleManuallyEdited?: boolean,
|
isTitleManuallyEdited?: boolean,
|
||||||
@@ -201,7 +199,7 @@ const updateRemoteChatSessionTitle = async (
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const deleteRemoteChatSession = async (sessionId: string) => {
|
const deleteBackendChatSession = async (sessionId: string) => {
|
||||||
const response = await apiFetch(
|
const response = await apiFetch(
|
||||||
`${config.AGENT_URL}/api/v1/agent/chat/session/${encodeURIComponent(sessionId)}`,
|
`${config.AGENT_URL}/api/v1/agent/chat/session/${encodeURIComponent(sessionId)}`,
|
||||||
{
|
{
|
||||||
@@ -216,42 +214,34 @@ const deleteRemoteChatSession = async (sessionId: string) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const loadActiveChatState = async (
|
|
||||||
_projectId?: string | null,
|
|
||||||
): Promise<LoadedChatState> => {
|
|
||||||
return emptyLoadedChatState();
|
|
||||||
};
|
|
||||||
|
|
||||||
export const saveActiveChatState = async (
|
export const saveActiveChatState = async (
|
||||||
state: LoadedChatState,
|
state: LoadedChatState,
|
||||||
_projectId?: string | null,
|
|
||||||
): Promise<string | undefined> => {
|
): Promise<string | undefined> => {
|
||||||
if (typeof window === "undefined") return state.storageSessionId;
|
if (typeof window === "undefined") return state.sessionId;
|
||||||
|
|
||||||
if (!hasChatContent(state)) {
|
if (!hasChatContent(state)) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
let remoteSessionId = state.sessionId ?? state.storageSessionId;
|
let backendSessionId = state.sessionId;
|
||||||
if (!remoteSessionId) {
|
if (!backendSessionId) {
|
||||||
remoteSessionId = await createRemoteChatSession();
|
backendSessionId = await createBackendChatSession();
|
||||||
}
|
}
|
||||||
|
|
||||||
const savedSessionId = await saveRemoteChatState(remoteSessionId, {
|
const savedSessionId = await saveBackendChatState(backendSessionId, {
|
||||||
...state,
|
...state,
|
||||||
storageSessionId: remoteSessionId,
|
sessionId: backendSessionId,
|
||||||
sessionId: remoteSessionId,
|
|
||||||
});
|
});
|
||||||
return savedSessionId;
|
return savedSessionId;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const listChatSessions = async (): Promise<ChatSessionSummary[]> => {
|
export const listChatSessions = async (): Promise<ChatSessionSummary[]> => {
|
||||||
if (typeof window === "undefined") return [];
|
if (typeof window === "undefined") return [];
|
||||||
return await fetchRemoteChatSessions();
|
return await fetchBackendChatSessions();
|
||||||
};
|
};
|
||||||
|
|
||||||
export const updateChatSessionTitle = async (
|
export const updateChatSessionTitle = async (
|
||||||
storageSessionId: string,
|
sessionId: string,
|
||||||
title: string,
|
title: string,
|
||||||
options?: {
|
options?: {
|
||||||
isTitleManuallyEdited?: boolean;
|
isTitleManuallyEdited?: boolean;
|
||||||
@@ -261,8 +251,8 @@ export const updateChatSessionTitle = async (
|
|||||||
|
|
||||||
const normalizedTitle = title.trim();
|
const normalizedTitle = title.trim();
|
||||||
if (!normalizedTitle) return;
|
if (!normalizedTitle) return;
|
||||||
await updateRemoteChatSessionTitle(
|
await updateBackendChatSessionTitle(
|
||||||
storageSessionId,
|
sessionId,
|
||||||
normalizedTitle,
|
normalizedTitle,
|
||||||
options?.isTitleManuallyEdited,
|
options?.isTitleManuallyEdited,
|
||||||
);
|
);
|
||||||
@@ -270,20 +260,18 @@ export const updateChatSessionTitle = async (
|
|||||||
|
|
||||||
export const loadChatSessionById = async (
|
export const loadChatSessionById = async (
|
||||||
sessionId: string,
|
sessionId: string,
|
||||||
_projectId?: string | null,
|
|
||||||
): Promise<LoadedChatState> => {
|
): Promise<LoadedChatState> => {
|
||||||
if (typeof window === "undefined") return emptyLoadedChatState();
|
if (typeof window === "undefined") return createEmptyChatState();
|
||||||
|
|
||||||
return await fetchRemoteChatSession(sessionId);
|
return await fetchBackendChatSession(sessionId);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const deleteChatSession = async (
|
export const deleteChatSession = async (
|
||||||
sessionId: string,
|
sessionId: string,
|
||||||
_projectId?: string | null,
|
|
||||||
): Promise<string | undefined> => {
|
): Promise<string | undefined> => {
|
||||||
if (typeof window === "undefined") return undefined;
|
if (typeof window === "undefined") return undefined;
|
||||||
|
|
||||||
await deleteRemoteChatSession(sessionId);
|
await deleteBackendChatSession(sessionId);
|
||||||
const nextActiveSession = (await listChatSessions())[0];
|
const nextActiveSession = (await listChatSessions())[0];
|
||||||
return nextActiveSession?.id;
|
return nextActiveSession?.id;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -12,44 +12,38 @@ jest.mock("@/lib/chatStream", () => ({
|
|||||||
streamAgentChat: jest.fn(async () => undefined),
|
streamAgentChat: jest.fn(async () => undefined),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const loadActiveChatState = jest.fn();
|
|
||||||
const listChatSessions = jest.fn();
|
const listChatSessions = jest.fn();
|
||||||
const saveActiveChatState = jest.fn();
|
const saveActiveChatState = jest.fn();
|
||||||
const updateChatSessionTitle = jest.fn();
|
const updateChatSessionTitle = jest.fn();
|
||||||
|
|
||||||
jest.mock("../chatStorage", () => ({
|
jest.mock("../chatStorage", () => ({
|
||||||
deleteChatSession: jest.fn(async () => undefined),
|
createEmptyChatState: jest.fn(() => ({
|
||||||
listChatSessions: (...args: unknown[]) => listChatSessions(...args),
|
title: undefined,
|
||||||
loadActiveChatState: (...args: unknown[]) => loadActiveChatState(...args),
|
|
||||||
loadChatSessionById: jest.fn(async () => ({
|
|
||||||
storageSessionId: "session-loaded",
|
|
||||||
title: "已存在会话",
|
|
||||||
isTitleManuallyEdited: false,
|
isTitleManuallyEdited: false,
|
||||||
messages: [],
|
messages: [],
|
||||||
sessionId: undefined,
|
sessionId: undefined,
|
||||||
branchGroups: [],
|
branchGroups: [],
|
||||||
})),
|
})),
|
||||||
|
deleteChatSession: jest.fn(async () => undefined),
|
||||||
|
listChatSessions: (...args: unknown[]) => listChatSessions(...args),
|
||||||
|
loadChatSessionById: jest.fn(async () => ({
|
||||||
|
title: "已存在会话",
|
||||||
|
isTitleManuallyEdited: false,
|
||||||
|
messages: [],
|
||||||
|
sessionId: "session-loaded",
|
||||||
|
branchGroups: [],
|
||||||
|
})),
|
||||||
saveActiveChatState: (...args: unknown[]) => saveActiveChatState(...args),
|
saveActiveChatState: (...args: unknown[]) => saveActiveChatState(...args),
|
||||||
updateChatSessionTitle: (...args: unknown[]) => updateChatSessionTitle(...args),
|
updateChatSessionTitle: (...args: unknown[]) => updateChatSessionTitle(...args),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
describe("useAgentChatSession", () => {
|
describe("useAgentChatSession", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
loadActiveChatState.mockReset();
|
|
||||||
listChatSessions.mockReset();
|
listChatSessions.mockReset();
|
||||||
saveActiveChatState.mockReset();
|
saveActiveChatState.mockReset();
|
||||||
updateChatSessionTitle.mockReset();
|
updateChatSessionTitle.mockReset();
|
||||||
jest.mocked(streamAgentChat).mockReset();
|
jest.mocked(streamAgentChat).mockReset();
|
||||||
saveActiveChatState.mockImplementation(async (state) => state.storageSessionId);
|
saveActiveChatState.mockImplementation(async (state) => state.sessionId);
|
||||||
|
|
||||||
loadActiveChatState.mockResolvedValue({
|
|
||||||
storageSessionId: undefined,
|
|
||||||
title: undefined,
|
|
||||||
isTitleManuallyEdited: false,
|
|
||||||
messages: [],
|
|
||||||
sessionId: undefined,
|
|
||||||
branchGroups: [],
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("does not add a new empty session to history until there is actual chat content", async () => {
|
it("does not add a new empty session to history until there is actual chat content", async () => {
|
||||||
@@ -70,7 +64,7 @@ describe("useAgentChatSession", () => {
|
|||||||
|
|
||||||
await waitFor(() => expect(result.current.sessionTitle).toBe("新对话"));
|
await waitFor(() => expect(result.current.sessionTitle).toBe("新对话"));
|
||||||
expect(result.current.chatSessions).toEqual([]);
|
expect(result.current.chatSessions).toEqual([]);
|
||||||
expect(result.current.activeStorageSessionId).toBeUndefined();
|
expect(result.current.activeSessionId).toBeUndefined();
|
||||||
expect(result.current.messages).toEqual([]);
|
expect(result.current.messages).toEqual([]);
|
||||||
expect(result.current.isStreaming).toBe(false);
|
expect(result.current.isStreaming).toBe(false);
|
||||||
expect(listChatSessions).toHaveBeenCalledTimes(1);
|
expect(listChatSessions).toHaveBeenCalledTimes(1);
|
||||||
@@ -164,14 +158,6 @@ describe("useAgentChatSession", () => {
|
|||||||
|
|
||||||
it("ignores generated session titles after the title was edited manually", async () => {
|
it("ignores generated session titles after the title was edited manually", async () => {
|
||||||
listChatSessions.mockResolvedValue([]);
|
listChatSessions.mockResolvedValue([]);
|
||||||
loadActiveChatState.mockResolvedValue({
|
|
||||||
storageSessionId: "session-1",
|
|
||||||
title: "手动标题",
|
|
||||||
isTitleManuallyEdited: true,
|
|
||||||
messages: [],
|
|
||||||
sessionId: "session-1",
|
|
||||||
branchGroups: [],
|
|
||||||
});
|
|
||||||
jest.mocked(streamAgentChat).mockImplementationOnce(async ({ onEvent }) => {
|
jest.mocked(streamAgentChat).mockImplementationOnce(async ({ onEvent }) => {
|
||||||
onEvent({
|
onEvent({
|
||||||
type: "session_title",
|
type: "session_title",
|
||||||
@@ -193,13 +179,23 @@ describe("useAgentChatSession", () => {
|
|||||||
|
|
||||||
await waitFor(() => expect(result.current.isHydrating).toBe(false));
|
await waitFor(() => expect(result.current.isHydrating).toBe(false));
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
await result.current.switchSession("session-loaded");
|
||||||
|
});
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
await result.current.renameSession("session-loaded", "手动标题");
|
||||||
|
});
|
||||||
|
|
||||||
|
await waitFor(() => expect(updateChatSessionTitle).toHaveBeenCalled());
|
||||||
|
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
await result.current.sendPrompt("帮我分析一下");
|
await result.current.sendPrompt("帮我分析一下");
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(result.current.sessionTitle).toBe("手动标题");
|
expect(result.current.sessionTitle).toBe("手动标题");
|
||||||
expect(updateChatSessionTitle).not.toHaveBeenCalledWith(
|
expect(updateChatSessionTitle).not.toHaveBeenCalledWith(
|
||||||
"session-1",
|
"session-loaded",
|
||||||
"自动标题",
|
"自动标题",
|
||||||
expect.anything(),
|
expect.anything(),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -19,9 +19,9 @@ import {
|
|||||||
createId,
|
createId,
|
||||||
} from "../GlobalChatbox.utils";
|
} from "../GlobalChatbox.utils";
|
||||||
import {
|
import {
|
||||||
|
createEmptyChatState,
|
||||||
deleteChatSession,
|
deleteChatSession,
|
||||||
listChatSessions,
|
listChatSessions,
|
||||||
loadActiveChatState,
|
|
||||||
loadChatSessionById,
|
loadChatSessionById,
|
||||||
saveActiveChatState,
|
saveActiveChatState,
|
||||||
updateChatSessionTitle,
|
updateChatSessionTitle,
|
||||||
@@ -50,7 +50,6 @@ type PromptRunOptions = {
|
|||||||
|
|
||||||
const createPersistedStateKey = (state: LoadedChatState) =>
|
const createPersistedStateKey = (state: LoadedChatState) =>
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
storageSessionId: state.storageSessionId ?? null,
|
|
||||||
title: state.title ?? null,
|
title: state.title ?? null,
|
||||||
isTitleManuallyEdited: state.isTitleManuallyEdited ?? false,
|
isTitleManuallyEdited: state.isTitleManuallyEdited ?? false,
|
||||||
sessionId: state.sessionId ?? null,
|
sessionId: state.sessionId ?? null,
|
||||||
@@ -151,7 +150,6 @@ export const useAgentChatSession = ({
|
|||||||
onBeforeSend,
|
onBeforeSend,
|
||||||
getModel,
|
getModel,
|
||||||
}: UseAgentChatSessionOptions) => {
|
}: UseAgentChatSessionOptions) => {
|
||||||
const storageSessionIdRef = useRef<string | undefined>(undefined);
|
|
||||||
const hydrationCompletedRef = useRef(false);
|
const hydrationCompletedRef = useRef(false);
|
||||||
const hydrationNonceRef = useRef(0);
|
const hydrationNonceRef = useRef(0);
|
||||||
|
|
||||||
@@ -171,11 +169,10 @@ export const useAgentChatSession = ({
|
|||||||
const titleUpdateNonceRef = useRef(0);
|
const titleUpdateNonceRef = useRef(0);
|
||||||
const lastPersistedStateKeyRef = useRef(
|
const lastPersistedStateKeyRef = useRef(
|
||||||
createPersistedStateKey({
|
createPersistedStateKey({
|
||||||
storageSessionId: undefined,
|
sessionId: undefined,
|
||||||
title: undefined,
|
title: undefined,
|
||||||
isTitleManuallyEdited: false,
|
isTitleManuallyEdited: false,
|
||||||
messages: [],
|
messages: [],
|
||||||
sessionId: undefined,
|
|
||||||
branchGroups: [],
|
branchGroups: [],
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
@@ -196,10 +193,8 @@ export const useAgentChatSession = ({
|
|||||||
hydrationCompletedRef.current = false;
|
hydrationCompletedRef.current = false;
|
||||||
|
|
||||||
if (!projectId) {
|
if (!projectId) {
|
||||||
storageSessionIdRef.current = undefined;
|
|
||||||
sessionIdRef.current = undefined;
|
sessionIdRef.current = undefined;
|
||||||
lastPersistedStateKeyRef.current = createPersistedStateKey({
|
lastPersistedStateKeyRef.current = createPersistedStateKey({
|
||||||
storageSessionId: undefined,
|
|
||||||
title: undefined,
|
title: undefined,
|
||||||
isTitleManuallyEdited: false,
|
isTitleManuallyEdited: false,
|
||||||
messages: [],
|
messages: [],
|
||||||
@@ -222,12 +217,11 @@ export const useAgentChatSession = ({
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const [loadedState, sessions] = await Promise.all([
|
const [loadedState, sessions] = await Promise.all([
|
||||||
loadActiveChatState(projectId),
|
Promise.resolve(createEmptyChatState()),
|
||||||
listChatSessions(),
|
listChatSessions(),
|
||||||
]);
|
]);
|
||||||
if (cancelled) return;
|
if (cancelled) return;
|
||||||
|
|
||||||
storageSessionIdRef.current = loadedState.storageSessionId;
|
|
||||||
sessionIdRef.current = loadedState.sessionId;
|
sessionIdRef.current = loadedState.sessionId;
|
||||||
lastPersistedStateKeyRef.current = createPersistedStateKey(loadedState);
|
lastPersistedStateKeyRef.current = createPersistedStateKey(loadedState);
|
||||||
hydrationCompletedRef.current = true;
|
hydrationCompletedRef.current = true;
|
||||||
@@ -262,7 +256,6 @@ export const useAgentChatSession = ({
|
|||||||
const currentHydrationNonce = hydrationNonceRef.current;
|
const currentHydrationNonce = hydrationNonceRef.current;
|
||||||
const persistTimer = window.setTimeout(() => {
|
const persistTimer = window.setTimeout(() => {
|
||||||
const state: LoadedChatState = {
|
const state: LoadedChatState = {
|
||||||
storageSessionId: storageSessionIdRef.current,
|
|
||||||
title: sessionTitle,
|
title: sessionTitle,
|
||||||
isTitleManuallyEdited: isSessionTitleManuallyEdited,
|
isTitleManuallyEdited: isSessionTitleManuallyEdited,
|
||||||
messages,
|
messages,
|
||||||
@@ -271,7 +264,6 @@ export const useAgentChatSession = ({
|
|||||||
};
|
};
|
||||||
if (
|
if (
|
||||||
isStreaming &&
|
isStreaming &&
|
||||||
!state.storageSessionId &&
|
|
||||||
!state.sessionId &&
|
!state.sessionId &&
|
||||||
state.messages.length > 0
|
state.messages.length > 0
|
||||||
) {
|
) {
|
||||||
@@ -283,13 +275,13 @@ export const useAgentChatSession = ({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
void saveActiveChatState(state, projectId)
|
void saveActiveChatState(state)
|
||||||
.then((storageSessionId) => {
|
.then((sessionId) => {
|
||||||
if (hydrationNonceRef.current !== currentHydrationNonce) return;
|
if (hydrationNonceRef.current !== currentHydrationNonce) return;
|
||||||
storageSessionIdRef.current = storageSessionId;
|
sessionIdRef.current = sessionId;
|
||||||
lastPersistedStateKeyRef.current = createPersistedStateKey({
|
lastPersistedStateKeyRef.current = createPersistedStateKey({
|
||||||
...state,
|
...state,
|
||||||
storageSessionId,
|
sessionId,
|
||||||
});
|
});
|
||||||
return listChatSessions();
|
return listChatSessions();
|
||||||
})
|
})
|
||||||
@@ -431,10 +423,10 @@ export const useAgentChatSession = ({
|
|||||||
const nextTitle = event.title.trim();
|
const nextTitle = event.title.trim();
|
||||||
if (nextTitle && !isSessionTitleManuallyEditedRef.current) {
|
if (nextTitle && !isSessionTitleManuallyEditedRef.current) {
|
||||||
setSessionTitle(nextTitle);
|
setSessionTitle(nextTitle);
|
||||||
const currentStorageSessionId = storageSessionIdRef.current;
|
const currentSessionId = sessionIdRef.current;
|
||||||
if (currentStorageSessionId) {
|
if (currentSessionId) {
|
||||||
const currentNonce = ++titleUpdateNonceRef.current;
|
const currentNonce = ++titleUpdateNonceRef.current;
|
||||||
void updateChatSessionTitle(currentStorageSessionId, nextTitle, {
|
void updateChatSessionTitle(currentSessionId, nextTitle, {
|
||||||
isTitleManuallyEdited: false,
|
isTitleManuallyEdited: false,
|
||||||
})
|
})
|
||||||
.then(() => listChatSessions())
|
.then(() => listChatSessions())
|
||||||
@@ -555,10 +547,8 @@ export const useAgentChatSession = ({
|
|||||||
setBranchTransition(null);
|
setBranchTransition(null);
|
||||||
hydrationNonceRef.current += 1;
|
hydrationNonceRef.current += 1;
|
||||||
titleUpdateNonceRef.current += 1;
|
titleUpdateNonceRef.current += 1;
|
||||||
storageSessionIdRef.current = undefined;
|
|
||||||
sessionIdRef.current = undefined;
|
sessionIdRef.current = undefined;
|
||||||
lastPersistedStateKeyRef.current = createPersistedStateKey({
|
lastPersistedStateKeyRef.current = createPersistedStateKey({
|
||||||
storageSessionId: undefined,
|
|
||||||
title: "新对话",
|
title: "新对话",
|
||||||
isTitleManuallyEdited: false,
|
isTitleManuallyEdited: false,
|
||||||
messages: [],
|
messages: [],
|
||||||
@@ -574,21 +564,20 @@ export const useAgentChatSession = ({
|
|||||||
}, [isHydrating, isStreaming]);
|
}, [isHydrating, isStreaming]);
|
||||||
|
|
||||||
const switchSession = useCallback(
|
const switchSession = useCallback(
|
||||||
async (nextStorageSessionId: string) => {
|
async (nextSessionId: string) => {
|
||||||
if (isHydrating || isStreaming || storageSessionIdRef.current === nextStorageSessionId) {
|
if (isHydrating || isStreaming || sessionIdRef.current === nextSessionId) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setIsHydrating(true);
|
setIsHydrating(true);
|
||||||
try {
|
try {
|
||||||
const [nextState, sessions] = await Promise.all([
|
const [nextState, sessions] = await Promise.all([
|
||||||
loadChatSessionById(nextStorageSessionId, projectId),
|
loadChatSessionById(nextSessionId),
|
||||||
listChatSessions(),
|
listChatSessions(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
hydrationNonceRef.current += 1;
|
hydrationNonceRef.current += 1;
|
||||||
titleUpdateNonceRef.current += 1;
|
titleUpdateNonceRef.current += 1;
|
||||||
storageSessionIdRef.current = nextState.storageSessionId;
|
|
||||||
sessionIdRef.current = nextState.sessionId;
|
sessionIdRef.current = nextState.sessionId;
|
||||||
lastPersistedStateKeyRef.current = createPersistedStateKey(nextState);
|
lastPersistedStateKeyRef.current = createPersistedStateKey(nextState);
|
||||||
setBranchTransition(null);
|
setBranchTransition(null);
|
||||||
@@ -604,32 +593,29 @@ export const useAgentChatSession = ({
|
|||||||
setIsHydrating(false);
|
setIsHydrating(false);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[isHydrating, isStreaming, projectId],
|
[isHydrating, isStreaming],
|
||||||
);
|
);
|
||||||
|
|
||||||
const removeSession = useCallback(
|
const removeSession = useCallback(
|
||||||
async (targetStorageSessionId: string) => {
|
async (targetSessionId: string) => {
|
||||||
if (isHydrating || isStreaming) return;
|
if (isHydrating || isStreaming) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const nextActiveSessionId = await deleteChatSession(
|
const nextActiveSessionId = await deleteChatSession(
|
||||||
targetStorageSessionId,
|
targetSessionId,
|
||||||
projectId,
|
|
||||||
);
|
);
|
||||||
const sessions = await listChatSessions();
|
const sessions = await listChatSessions();
|
||||||
setChatSessions(sessions);
|
setChatSessions(sessions);
|
||||||
|
|
||||||
if (storageSessionIdRef.current !== targetStorageSessionId) {
|
if (sessionIdRef.current !== targetSessionId) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!nextActiveSessionId) {
|
if (!nextActiveSessionId) {
|
||||||
hydrationNonceRef.current += 1;
|
hydrationNonceRef.current += 1;
|
||||||
titleUpdateNonceRef.current += 1;
|
titleUpdateNonceRef.current += 1;
|
||||||
storageSessionIdRef.current = undefined;
|
|
||||||
sessionIdRef.current = undefined;
|
sessionIdRef.current = undefined;
|
||||||
lastPersistedStateKeyRef.current = createPersistedStateKey({
|
lastPersistedStateKeyRef.current = createPersistedStateKey({
|
||||||
storageSessionId: undefined,
|
|
||||||
title: undefined,
|
title: undefined,
|
||||||
isTitleManuallyEdited: false,
|
isTitleManuallyEdited: false,
|
||||||
messages: [],
|
messages: [],
|
||||||
@@ -647,12 +633,11 @@ export const useAgentChatSession = ({
|
|||||||
|
|
||||||
setIsHydrating(true);
|
setIsHydrating(true);
|
||||||
const [nextState, sessionsAfterDelete] = await Promise.all([
|
const [nextState, sessionsAfterDelete] = await Promise.all([
|
||||||
loadChatSessionById(nextActiveSessionId, projectId),
|
loadChatSessionById(nextActiveSessionId),
|
||||||
listChatSessions(),
|
listChatSessions(),
|
||||||
]);
|
]);
|
||||||
hydrationNonceRef.current += 1;
|
hydrationNonceRef.current += 1;
|
||||||
titleUpdateNonceRef.current += 1;
|
titleUpdateNonceRef.current += 1;
|
||||||
storageSessionIdRef.current = nextState.storageSessionId;
|
|
||||||
sessionIdRef.current = nextState.sessionId;
|
sessionIdRef.current = nextState.sessionId;
|
||||||
lastPersistedStateKeyRef.current = createPersistedStateKey(nextState);
|
lastPersistedStateKeyRef.current = createPersistedStateKey(nextState);
|
||||||
setBranchTransition(null);
|
setBranchTransition(null);
|
||||||
@@ -668,7 +653,7 @@ export const useAgentChatSession = ({
|
|||||||
setIsHydrating(false);
|
setIsHydrating(false);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[isHydrating, isStreaming, projectId],
|
[isHydrating, isStreaming],
|
||||||
);
|
);
|
||||||
|
|
||||||
const sendPrompt = useCallback(
|
const sendPrompt = useCallback(
|
||||||
@@ -679,22 +664,22 @@ export const useAgentChatSession = ({
|
|||||||
);
|
);
|
||||||
|
|
||||||
const renameSession = useCallback(
|
const renameSession = useCallback(
|
||||||
async (targetStorageSessionId: string, nextTitle: string) => {
|
async (targetSessionId: string, nextTitle: string) => {
|
||||||
const normalizedTitle = nextTitle.trim();
|
const normalizedTitle = nextTitle.trim();
|
||||||
if (!normalizedTitle || isHydrating) return;
|
if (!normalizedTitle || isHydrating) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await updateChatSessionTitle(targetStorageSessionId, normalizedTitle, {
|
await updateChatSessionTitle(targetSessionId, normalizedTitle, {
|
||||||
isTitleManuallyEdited: true,
|
isTitleManuallyEdited: true,
|
||||||
});
|
});
|
||||||
const sessions = await listChatSessions();
|
const sessions = await listChatSessions();
|
||||||
setChatSessions(sessions);
|
setChatSessions(sessions);
|
||||||
|
|
||||||
if (storageSessionIdRef.current === targetStorageSessionId) {
|
if (sessionIdRef.current === targetSessionId) {
|
||||||
setSessionTitle(normalizedTitle);
|
setSessionTitle(normalizedTitle);
|
||||||
setIsSessionTitleManuallyEdited(true);
|
setIsSessionTitleManuallyEdited(true);
|
||||||
lastPersistedStateKeyRef.current = createPersistedStateKey({
|
lastPersistedStateKeyRef.current = createPersistedStateKey({
|
||||||
storageSessionId: targetStorageSessionId,
|
sessionId: targetSessionId,
|
||||||
title: normalizedTitle,
|
title: normalizedTitle,
|
||||||
isTitleManuallyEdited: true,
|
isTitleManuallyEdited: true,
|
||||||
messages,
|
messages,
|
||||||
@@ -864,7 +849,7 @@ export const useAgentChatSession = ({
|
|||||||
return {
|
return {
|
||||||
messages,
|
messages,
|
||||||
chatSessions,
|
chatSessions,
|
||||||
activeStorageSessionId: storageSessionIdRef.current,
|
activeSessionId: sessionIdRef.current,
|
||||||
branchGroups,
|
branchGroups,
|
||||||
branchTransition,
|
branchTransition,
|
||||||
isHydrating,
|
isHydrating,
|
||||||
|
|||||||
@@ -103,11 +103,11 @@ describe("streamAgentChat", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("parses legacy tool_call arguments when params is empty", async () => {
|
it("parses tool_call arguments when params is empty", async () => {
|
||||||
apiFetch.mockResolvedValue({
|
apiFetch.mockResolvedValue({
|
||||||
ok: true,
|
ok: true,
|
||||||
body: makeStream([
|
body: makeStream([
|
||||||
'event: tool_call\ndata: {"conversationId":"agent-1e75dd01-29e","tool":"locate_features","params":{},"arguments":"{\\"ids\\":[\\"142902\\"],\\"feature_type\\":\\"junction\\"}"}\n\n',
|
'event: tool_call\ndata: {"session_id":"agent-1e75dd01-29e","tool":"locate_features","params":{},"arguments":"{\\"ids\\":[\\"142902\\"],\\"feature_type\\":\\"junction\\"}"}\n\n',
|
||||||
'event: done\ndata: {"session_id":"agent-1e75dd01-29e"}\n\n',
|
'event: done\ndata: {"session_id":"agent-1e75dd01-29e"}\n\n',
|
||||||
]),
|
]),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -163,7 +163,6 @@ export const streamAgentChat = async ({
|
|||||||
try {
|
try {
|
||||||
const parsed = JSON.parse(data) as {
|
const parsed = JSON.parse(data) as {
|
||||||
session_id?: string;
|
session_id?: string;
|
||||||
conversationId?: string;
|
|
||||||
content?: string;
|
content?: string;
|
||||||
message?: string;
|
message?: string;
|
||||||
detail?: string;
|
detail?: string;
|
||||||
@@ -223,7 +222,7 @@ export const streamAgentChat = async ({
|
|||||||
} else if (event === "tool_call") {
|
} else if (event === "tool_call") {
|
||||||
onEvent({
|
onEvent({
|
||||||
type: "tool_call",
|
type: "tool_call",
|
||||||
sessionId: parsed.session_id ?? parsed.conversationId ?? "",
|
sessionId: parsed.session_id ?? "",
|
||||||
tool: parsed.tool ?? "",
|
tool: parsed.tool ?? "",
|
||||||
params: resolveToolParams(parsed.params, parsed.arguments),
|
params: resolveToolParams(parsed.params, parsed.arguments),
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user