debug: 修复新会话是query参数的问题
This commit is contained in:
parent
917f0ef591
commit
3d4521f37f
|
|
@ -47,7 +47,7 @@ export default function AgentChatPage() {
|
|||
history.replaceState(
|
||||
null,
|
||||
"",
|
||||
`/workspace/agents/${agent_name}/chats/${threadId}`,
|
||||
`/workspace/agents/${agent_name}/chats/new?isnew=false&thread_id=${threadId}`,
|
||||
);
|
||||
},
|
||||
onFinish: (state) => {
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ import { useNotification } from "@/core/notification/hooks";
|
|||
import { useLocalSettings } from "@/core/settings";
|
||||
// [移植自 main 分支 ef9a071] 导入 skill 初始化 API
|
||||
import { bootstrapRemoteSkill } from "@/core/skills";
|
||||
import { useThreadStream } from "@/core/threads/hooks";
|
||||
import { useThreadStream, useTruncateThread } from "@/core/threads/hooks";
|
||||
import { textOfMessage } from "@/core/threads/utils";
|
||||
import { env } from "@/env";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
|
@ -71,6 +71,21 @@ export default function ChatPage() {
|
|||
|
||||
const { showNotification } = useNotification();
|
||||
|
||||
// 截断会话 hook
|
||||
const truncateThread = useTruncateThread();
|
||||
// 跟踪已截断的 threadId,避免重复截断
|
||||
const truncatedThreadIdRef = useRef<string | null>(null);
|
||||
|
||||
// 当 isnew=false 且有 thread_id 时,截断会话历史
|
||||
useEffect(() => {
|
||||
// 只在非新会话且需要复用 thread_id 时截断
|
||||
if (!createNewSession && threadId && truncatedThreadIdRef.current !== threadId) {
|
||||
console.log("[ChatPage] Truncating thread:", threadId);
|
||||
truncatedThreadIdRef.current = threadId;
|
||||
truncateThread.mutate({ threadId });
|
||||
}
|
||||
}, [createNewSession, threadId, truncateThread]);
|
||||
|
||||
// [移植自 main 分支 ef9a071] skill 初始化状态
|
||||
const [isSkillBootstrapping, setIsSkillBootstrapping] = useState(false);
|
||||
const skillBootstrappedKeyRef = useRef<string | null>(null);
|
||||
|
|
@ -180,7 +195,9 @@ export default function ChatPage() {
|
|||
}, [threadId, skillBootstrap, showNotification]);
|
||||
|
||||
const [thread, sendMessage] = useThreadStream({
|
||||
threadId: isNewThread ? undefined : threadId,
|
||||
// [修复] 使用 createNewSession 而不是 isNewThread 来决定是否创建新会话
|
||||
// isnew=false 时应该复用现有 threadId,不应该是 undefined
|
||||
threadId: createNewSession ? undefined : threadId,
|
||||
context: settings.context,
|
||||
isMock,
|
||||
// [移植自 main 分支 4119fdc] 传递 uploadTarget
|
||||
|
|
@ -188,7 +205,11 @@ export default function ChatPage() {
|
|||
onStart: () => {
|
||||
setIsNewThread(false);
|
||||
// ! Important: Never use next.js router for navigation in this case, otherwise it will cause the thread to re-mount and lose all states. Use native history API instead.
|
||||
history.replaceState(null, "", `/workspace/chats/${threadId}`);
|
||||
history.replaceState(
|
||||
null,
|
||||
"",
|
||||
`/workspace/chats/new?isnew=false&thread_id=${threadId}`,
|
||||
);
|
||||
},
|
||||
onFinish: (state) => {
|
||||
if (document.hidden || !document.hasFocus()) {
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ export default function WorkspaceLayout({
|
|||
onOpenChange={handleOpenChange}
|
||||
>
|
||||
{/* TODO: !!!!必须注释!!!!! */}
|
||||
{/* <WorkspaceSidebar className="" /> */}
|
||||
<WorkspaceSidebar className="" />
|
||||
<SidebarInset className="min-w-0">{children}</SidebarInset>
|
||||
</SidebarProvider>
|
||||
<Toaster />
|
||||
|
|
|
|||
|
|
@ -13,7 +13,9 @@ export default function WorkspacePage() {
|
|||
})
|
||||
.find((thread) => thread.isDirectory() && !thread.name.startsWith("."));
|
||||
if (firstThread) {
|
||||
return redirect(`/workspace/chats/${firstThread.name}`);
|
||||
return redirect(
|
||||
`/workspace/chats/new?isnew=false&thread_id=${firstThread.name}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
return redirect("/workspace/chats/new");
|
||||
|
|
|
|||
|
|
@ -77,12 +77,17 @@ export function useThreadChat(): ThreadChatResult {
|
|||
return result;
|
||||
}, [threadIdFromPath, searchParams]);
|
||||
|
||||
// [移植自 main 分支 e2fdfa7] UI模式仅依赖路由:/workspace/chats/new 总是"新页面"模式
|
||||
// [移植自 main 分支 e2fdfa7] UI模式:如果有 thread_id 参数则认为是已有会话
|
||||
const isNewThread = useMemo(() => {
|
||||
const result = threadIdFromPath === "new";
|
||||
console.log("[useThreadChat] isNewThread:", result);
|
||||
return result;
|
||||
}, [threadIdFromPath]);
|
||||
if (threadIdFromPath !== "new") {
|
||||
return false;
|
||||
}
|
||||
// 有 thread_id 参数说明是复用现有会话
|
||||
if (queryThreadId) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}, [threadIdFromPath, queryThreadId]);
|
||||
|
||||
// [移植自 main 分支 4119fdc] 获取上传目标
|
||||
const uploadTarget = useMemo(() => {
|
||||
|
|
@ -212,12 +217,15 @@ export function useThreadChat(): ThreadChatResult {
|
|||
if (threadIdFromPath === "new") {
|
||||
// [移植自 main 分支 4119fdc] 优先使用 URL 中的 thread_id
|
||||
threadIdRef.current = queryThreadId || uuid();
|
||||
isNewThreadRef.current = true;
|
||||
// 如果有 queryThreadId,说明是复用现有会话,不是新会话
|
||||
isNewThreadRef.current = !queryThreadId;
|
||||
console.log(
|
||||
"[useThreadChat] initial threadId (new route):",
|
||||
threadIdRef.current,
|
||||
"queryThreadId:",
|
||||
queryThreadId,
|
||||
"isNewThread:",
|
||||
isNewThreadRef.current,
|
||||
);
|
||||
} else {
|
||||
threadIdRef.current = threadIdFromPath;
|
||||
|
|
@ -232,6 +240,17 @@ export function useThreadChat(): ThreadChatResult {
|
|||
const [threadId, setThreadId] = useState(threadIdRef.current);
|
||||
const [isNewThreadState, setIsNewThread] = useState(isNewThreadRef.current);
|
||||
|
||||
// 监听 queryThreadId 变化,更新 threadId
|
||||
useEffect(() => {
|
||||
// 当 URL 中的 thread_id 参数变化时,更新 threadId
|
||||
if (queryThreadId && queryThreadId !== threadId) {
|
||||
console.log("[useThreadChat] queryThreadId changed, updating threadId:", queryThreadId);
|
||||
threadIdRef.current = queryThreadId;
|
||||
setThreadId(queryThreadId);
|
||||
setIsNewThread(false);
|
||||
}
|
||||
}, [queryThreadId, threadId]);
|
||||
|
||||
useEffect(() => {
|
||||
console.log("[useThreadChat] useEffect: pathname changed to:", pathname);
|
||||
if (pathname.endsWith("/new")) {
|
||||
|
|
|
|||
|
|
@ -168,6 +168,12 @@ export function InputBox({
|
|||
const [isFocused, setIsFocused] = useState(false);
|
||||
const containerRef = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
// isNewThread 变化时重置 isFocused 状态
|
||||
useEffect(() => {
|
||||
setIsFocused(false);
|
||||
onFocusChange?.(false);
|
||||
}, [isNewThread, onFocusChange]);
|
||||
|
||||
// isNewThread 时禁用收缩,始终保持展开
|
||||
const effectiveIsFocused = isNewThread || isFocused;
|
||||
|
||||
|
|
@ -1035,6 +1041,19 @@ function IframeSkillDialogButton({
|
|||
}) {
|
||||
const { t } = useI18n();
|
||||
|
||||
// TODO: 测试按钮,模拟宿主页发送 postMessage,测试完成后删除
|
||||
const handleTestPostMessage = () => {
|
||||
const testMessage = {
|
||||
type: "selectedSkill",
|
||||
id: 5,
|
||||
title: "文档处理",
|
||||
};
|
||||
console.log("[Test] Simulating postMessage from parent:", testMessage);
|
||||
window.dispatchEvent(
|
||||
new MessageEvent("message", { data: testMessage })
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
<Tooltip content={t.inputBox.selectSkill}>
|
||||
|
|
@ -1055,6 +1074,14 @@ function IframeSkillDialogButton({
|
|||
</svg>
|
||||
</PromptInputButton>
|
||||
</Tooltip>
|
||||
{/* TODO: 测试按钮,测试完成后删除
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleTestPostMessage}
|
||||
className="rounded bg-blue-500 px-2 py-1 text-xs text-white hover:bg-blue-600"
|
||||
>
|
||||
测试postMessage
|
||||
</button> */}
|
||||
{selectedSkill && (
|
||||
<Badge
|
||||
variant="secondary"
|
||||
|
|
|
|||
|
|
@ -59,15 +59,15 @@ export function RecentChatList() {
|
|||
deleteThread({ threadId });
|
||||
if (threadId === threadIdFromPath) {
|
||||
const threadIndex = threads.findIndex((t) => t.thread_id === threadId);
|
||||
let nextThreadId = "new";
|
||||
let nextPath = "/workspace/chats/new";
|
||||
if (threadIndex > -1) {
|
||||
if (threads[threadIndex + 1]) {
|
||||
nextThreadId = threads[threadIndex + 1]!.thread_id;
|
||||
nextPath = `/workspace/chats/new?isnew=false&thread_id=${threads[threadIndex + 1]!.thread_id}`;
|
||||
} else if (threads[threadIndex - 1]) {
|
||||
nextThreadId = threads[threadIndex - 1]!.thread_id;
|
||||
nextPath = `/workspace/chats/new?isnew=false&thread_id=${threads[threadIndex - 1]!.thread_id}`;
|
||||
}
|
||||
}
|
||||
void router.push(`/workspace/chats/${nextThreadId}`);
|
||||
void router.push(nextPath);
|
||||
}
|
||||
},
|
||||
[deleteThread, router, threadIdFromPath, threads],
|
||||
|
|
@ -100,7 +100,7 @@ export function RecentChatList() {
|
|||
window.location.hostname === "127.0.0.1";
|
||||
// On localhost: use Vercel URL; On production: use current origin
|
||||
const baseUrl = isLocalhost ? VERCEL_URL : window.location.origin;
|
||||
const shareUrl = `${baseUrl}/workspace/chats/${threadId}`;
|
||||
const shareUrl = `${baseUrl}/workspace/chats/new?isnew=false&thread_id=${threadId}`;
|
||||
try {
|
||||
await navigator.clipboard.writeText(shareUrl);
|
||||
toast.success(t.clipboard.linkCopied);
|
||||
|
|
|
|||
|
|
@ -71,8 +71,8 @@ export function useThreadStream({
|
|||
|
||||
useEffect(() => {
|
||||
const normalizedThreadId = threadId ?? null;
|
||||
if (!normalizedThreadId) {
|
||||
// Just reset for new thread creation when threadId becomes null/undefined
|
||||
// 当 threadId 变化时,更新 onStreamThreadId 以重新获取数据
|
||||
if (normalizedThreadId !== threadIdRef.current) {
|
||||
startedRef.current = false;
|
||||
setOnStreamThreadId(normalizedThreadId);
|
||||
}
|
||||
|
|
@ -533,3 +533,16 @@ export function useRenameThread() {
|
|||
},
|
||||
});
|
||||
}
|
||||
|
||||
// 截断会话历史(清空消息)
|
||||
export function useTruncateThread() {
|
||||
const apiClient = getAPIClient();
|
||||
return useMutation({
|
||||
mutationFn: async ({ threadId }: { threadId: string }) => {
|
||||
// 通过更新 state,设置 messages 为空数组来截断会话
|
||||
await apiClient.threads.updateState(threadId, {
|
||||
values: { messages: [] },
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import type { Message } from "@langchain/langgraph-sdk";
|
|||
import type { AgentThread } from "./types";
|
||||
|
||||
export function pathOfThread(threadId: string) {
|
||||
return `/workspace/chats/${threadId}`;
|
||||
return `/workspace/chats/new?isnew=false&thread_id=${threadId}`;
|
||||
}
|
||||
|
||||
export function textOfMessage(message: Message) {
|
||||
|
|
|
|||
|
|
@ -21,6 +21,22 @@ interface UseIframeSkillReturn {
|
|||
clearSkill: () => void;
|
||||
}
|
||||
|
||||
// 来自宿主页的 postMessage 类型
|
||||
interface SelectedSkillMessage {
|
||||
type: "selectedSkill";
|
||||
id: number;
|
||||
title: string;
|
||||
}
|
||||
|
||||
function isSelectedSkillMessage(data: unknown): data is SelectedSkillMessage {
|
||||
return (
|
||||
typeof data === "object" &&
|
||||
data !== null &&
|
||||
(data as SelectedSkillMessage).type === "selectedSkill" &&
|
||||
typeof (data as SelectedSkillMessage).id === "number"
|
||||
);
|
||||
}
|
||||
|
||||
export function useIframeSkill(): UseIframeSkillReturn {
|
||||
const searchParams = useSearchParams();
|
||||
const skillIdFromQuery = searchParams.get("skill_id");
|
||||
|
|
@ -43,6 +59,24 @@ export function useIframeSkill(): UseIframeSkillReturn {
|
|||
}
|
||||
}, [skillIdFromQuery, titleFromQuery]);
|
||||
|
||||
// 监听来自宿主页的 postMessage
|
||||
useEffect(() => {
|
||||
const handleMessage = (event: MessageEvent) => {
|
||||
if (!isSelectedSkillMessage(event.data)) {
|
||||
return;
|
||||
}
|
||||
const { id, title } = event.data;
|
||||
console.log("[useIframeSkill] Received selectedSkill from parent:", {
|
||||
id,
|
||||
title,
|
||||
});
|
||||
setSelectedSkill({ skill_id: String(id), title });
|
||||
};
|
||||
|
||||
window.addEventListener("message", handleMessage);
|
||||
return () => window.removeEventListener("message", handleMessage);
|
||||
}, []);
|
||||
|
||||
// 发送选择预定义 skill
|
||||
const sendSelectSkill = useCallback((skill_id: string) => {
|
||||
const message = { type: MESSAGE_TYPES.SELECT_SKILL, skill_id };
|
||||
|
|
|
|||
Loading…
Reference in New Issue