debug: 修复新会话是query参数的问题
This commit is contained in:
parent
917f0ef591
commit
3d4521f37f
|
|
@ -47,7 +47,7 @@ export default function AgentChatPage() {
|
||||||
history.replaceState(
|
history.replaceState(
|
||||||
null,
|
null,
|
||||||
"",
|
"",
|
||||||
`/workspace/agents/${agent_name}/chats/${threadId}`,
|
`/workspace/agents/${agent_name}/chats/new?isnew=false&thread_id=${threadId}`,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
onFinish: (state) => {
|
onFinish: (state) => {
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,7 @@ import { useNotification } from "@/core/notification/hooks";
|
||||||
import { useLocalSettings } from "@/core/settings";
|
import { useLocalSettings } from "@/core/settings";
|
||||||
// [移植自 main 分支 ef9a071] 导入 skill 初始化 API
|
// [移植自 main 分支 ef9a071] 导入 skill 初始化 API
|
||||||
import { bootstrapRemoteSkill } from "@/core/skills";
|
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 { textOfMessage } from "@/core/threads/utils";
|
||||||
import { env } from "@/env";
|
import { env } from "@/env";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
@ -71,6 +71,21 @@ export default function ChatPage() {
|
||||||
|
|
||||||
const { showNotification } = useNotification();
|
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 初始化状态
|
// [移植自 main 分支 ef9a071] skill 初始化状态
|
||||||
const [isSkillBootstrapping, setIsSkillBootstrapping] = useState(false);
|
const [isSkillBootstrapping, setIsSkillBootstrapping] = useState(false);
|
||||||
const skillBootstrappedKeyRef = useRef<string | null>(null);
|
const skillBootstrappedKeyRef = useRef<string | null>(null);
|
||||||
|
|
@ -180,7 +195,9 @@ export default function ChatPage() {
|
||||||
}, [threadId, skillBootstrap, showNotification]);
|
}, [threadId, skillBootstrap, showNotification]);
|
||||||
|
|
||||||
const [thread, sendMessage] = useThreadStream({
|
const [thread, sendMessage] = useThreadStream({
|
||||||
threadId: isNewThread ? undefined : threadId,
|
// [修复] 使用 createNewSession 而不是 isNewThread 来决定是否创建新会话
|
||||||
|
// isnew=false 时应该复用现有 threadId,不应该是 undefined
|
||||||
|
threadId: createNewSession ? undefined : threadId,
|
||||||
context: settings.context,
|
context: settings.context,
|
||||||
isMock,
|
isMock,
|
||||||
// [移植自 main 分支 4119fdc] 传递 uploadTarget
|
// [移植自 main 分支 4119fdc] 传递 uploadTarget
|
||||||
|
|
@ -188,7 +205,11 @@ export default function ChatPage() {
|
||||||
onStart: () => {
|
onStart: () => {
|
||||||
setIsNewThread(false);
|
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.
|
// ! 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) => {
|
onFinish: (state) => {
|
||||||
if (document.hidden || !document.hasFocus()) {
|
if (document.hidden || !document.hasFocus()) {
|
||||||
|
|
|
||||||
|
|
@ -37,7 +37,7 @@ export default function WorkspaceLayout({
|
||||||
onOpenChange={handleOpenChange}
|
onOpenChange={handleOpenChange}
|
||||||
>
|
>
|
||||||
{/* TODO: !!!!必须注释!!!!! */}
|
{/* TODO: !!!!必须注释!!!!! */}
|
||||||
{/* <WorkspaceSidebar className="" /> */}
|
<WorkspaceSidebar className="" />
|
||||||
<SidebarInset className="min-w-0">{children}</SidebarInset>
|
<SidebarInset className="min-w-0">{children}</SidebarInset>
|
||||||
</SidebarProvider>
|
</SidebarProvider>
|
||||||
<Toaster />
|
<Toaster />
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,9 @@ export default function WorkspacePage() {
|
||||||
})
|
})
|
||||||
.find((thread) => thread.isDirectory() && !thread.name.startsWith("."));
|
.find((thread) => thread.isDirectory() && !thread.name.startsWith("."));
|
||||||
if (firstThread) {
|
if (firstThread) {
|
||||||
return redirect(`/workspace/chats/${firstThread.name}`);
|
return redirect(
|
||||||
|
`/workspace/chats/new?isnew=false&thread_id=${firstThread.name}`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return redirect("/workspace/chats/new");
|
return redirect("/workspace/chats/new");
|
||||||
|
|
|
||||||
|
|
@ -77,12 +77,17 @@ export function useThreadChat(): ThreadChatResult {
|
||||||
return result;
|
return result;
|
||||||
}, [threadIdFromPath, searchParams]);
|
}, [threadIdFromPath, searchParams]);
|
||||||
|
|
||||||
// [移植自 main 分支 e2fdfa7] UI模式仅依赖路由:/workspace/chats/new 总是"新页面"模式
|
// [移植自 main 分支 e2fdfa7] UI模式:如果有 thread_id 参数则认为是已有会话
|
||||||
const isNewThread = useMemo(() => {
|
const isNewThread = useMemo(() => {
|
||||||
const result = threadIdFromPath === "new";
|
if (threadIdFromPath !== "new") {
|
||||||
console.log("[useThreadChat] isNewThread:", result);
|
return false;
|
||||||
return result;
|
}
|
||||||
}, [threadIdFromPath]);
|
// 有 thread_id 参数说明是复用现有会话
|
||||||
|
if (queryThreadId) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}, [threadIdFromPath, queryThreadId]);
|
||||||
|
|
||||||
// [移植自 main 分支 4119fdc] 获取上传目标
|
// [移植自 main 分支 4119fdc] 获取上传目标
|
||||||
const uploadTarget = useMemo(() => {
|
const uploadTarget = useMemo(() => {
|
||||||
|
|
@ -212,12 +217,15 @@ export function useThreadChat(): ThreadChatResult {
|
||||||
if (threadIdFromPath === "new") {
|
if (threadIdFromPath === "new") {
|
||||||
// [移植自 main 分支 4119fdc] 优先使用 URL 中的 thread_id
|
// [移植自 main 分支 4119fdc] 优先使用 URL 中的 thread_id
|
||||||
threadIdRef.current = queryThreadId || uuid();
|
threadIdRef.current = queryThreadId || uuid();
|
||||||
isNewThreadRef.current = true;
|
// 如果有 queryThreadId,说明是复用现有会话,不是新会话
|
||||||
|
isNewThreadRef.current = !queryThreadId;
|
||||||
console.log(
|
console.log(
|
||||||
"[useThreadChat] initial threadId (new route):",
|
"[useThreadChat] initial threadId (new route):",
|
||||||
threadIdRef.current,
|
threadIdRef.current,
|
||||||
"queryThreadId:",
|
"queryThreadId:",
|
||||||
queryThreadId,
|
queryThreadId,
|
||||||
|
"isNewThread:",
|
||||||
|
isNewThreadRef.current,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
threadIdRef.current = threadIdFromPath;
|
threadIdRef.current = threadIdFromPath;
|
||||||
|
|
@ -232,6 +240,17 @@ export function useThreadChat(): ThreadChatResult {
|
||||||
const [threadId, setThreadId] = useState(threadIdRef.current);
|
const [threadId, setThreadId] = useState(threadIdRef.current);
|
||||||
const [isNewThreadState, setIsNewThread] = useState(isNewThreadRef.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(() => {
|
useEffect(() => {
|
||||||
console.log("[useThreadChat] useEffect: pathname changed to:", pathname);
|
console.log("[useThreadChat] useEffect: pathname changed to:", pathname);
|
||||||
if (pathname.endsWith("/new")) {
|
if (pathname.endsWith("/new")) {
|
||||||
|
|
|
||||||
|
|
@ -168,6 +168,12 @@ export function InputBox({
|
||||||
const [isFocused, setIsFocused] = useState(false);
|
const [isFocused, setIsFocused] = useState(false);
|
||||||
const containerRef = useRef<HTMLDivElement | null>(null);
|
const containerRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
|
||||||
|
// isNewThread 变化时重置 isFocused 状态
|
||||||
|
useEffect(() => {
|
||||||
|
setIsFocused(false);
|
||||||
|
onFocusChange?.(false);
|
||||||
|
}, [isNewThread, onFocusChange]);
|
||||||
|
|
||||||
// isNewThread 时禁用收缩,始终保持展开
|
// isNewThread 时禁用收缩,始终保持展开
|
||||||
const effectiveIsFocused = isNewThread || isFocused;
|
const effectiveIsFocused = isNewThread || isFocused;
|
||||||
|
|
||||||
|
|
@ -1035,6 +1041,19 @@ function IframeSkillDialogButton({
|
||||||
}) {
|
}) {
|
||||||
const { t } = useI18n();
|
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 (
|
return (
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Tooltip content={t.inputBox.selectSkill}>
|
<Tooltip content={t.inputBox.selectSkill}>
|
||||||
|
|
@ -1055,6 +1074,14 @@ function IframeSkillDialogButton({
|
||||||
</svg>
|
</svg>
|
||||||
</PromptInputButton>
|
</PromptInputButton>
|
||||||
</Tooltip>
|
</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 && (
|
{selectedSkill && (
|
||||||
<Badge
|
<Badge
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
|
|
|
||||||
|
|
@ -59,15 +59,15 @@ export function RecentChatList() {
|
||||||
deleteThread({ threadId });
|
deleteThread({ threadId });
|
||||||
if (threadId === threadIdFromPath) {
|
if (threadId === threadIdFromPath) {
|
||||||
const threadIndex = threads.findIndex((t) => t.thread_id === threadId);
|
const threadIndex = threads.findIndex((t) => t.thread_id === threadId);
|
||||||
let nextThreadId = "new";
|
let nextPath = "/workspace/chats/new";
|
||||||
if (threadIndex > -1) {
|
if (threadIndex > -1) {
|
||||||
if (threads[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]) {
|
} 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],
|
[deleteThread, router, threadIdFromPath, threads],
|
||||||
|
|
@ -100,7 +100,7 @@ export function RecentChatList() {
|
||||||
window.location.hostname === "127.0.0.1";
|
window.location.hostname === "127.0.0.1";
|
||||||
// On localhost: use Vercel URL; On production: use current origin
|
// On localhost: use Vercel URL; On production: use current origin
|
||||||
const baseUrl = isLocalhost ? VERCEL_URL : window.location.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 {
|
try {
|
||||||
await navigator.clipboard.writeText(shareUrl);
|
await navigator.clipboard.writeText(shareUrl);
|
||||||
toast.success(t.clipboard.linkCopied);
|
toast.success(t.clipboard.linkCopied);
|
||||||
|
|
|
||||||
|
|
@ -71,8 +71,8 @@ export function useThreadStream({
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const normalizedThreadId = threadId ?? null;
|
const normalizedThreadId = threadId ?? null;
|
||||||
if (!normalizedThreadId) {
|
// 当 threadId 变化时,更新 onStreamThreadId 以重新获取数据
|
||||||
// Just reset for new thread creation when threadId becomes null/undefined
|
if (normalizedThreadId !== threadIdRef.current) {
|
||||||
startedRef.current = false;
|
startedRef.current = false;
|
||||||
setOnStreamThreadId(normalizedThreadId);
|
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";
|
import type { AgentThread } from "./types";
|
||||||
|
|
||||||
export function pathOfThread(threadId: string) {
|
export function pathOfThread(threadId: string) {
|
||||||
return `/workspace/chats/${threadId}`;
|
return `/workspace/chats/new?isnew=false&thread_id=${threadId}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function textOfMessage(message: Message) {
|
export function textOfMessage(message: Message) {
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,22 @@ interface UseIframeSkillReturn {
|
||||||
clearSkill: () => void;
|
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 {
|
export function useIframeSkill(): UseIframeSkillReturn {
|
||||||
const searchParams = useSearchParams();
|
const searchParams = useSearchParams();
|
||||||
const skillIdFromQuery = searchParams.get("skill_id");
|
const skillIdFromQuery = searchParams.get("skill_id");
|
||||||
|
|
@ -43,6 +59,24 @@ export function useIframeSkill(): UseIframeSkillReturn {
|
||||||
}
|
}
|
||||||
}, [skillIdFromQuery, titleFromQuery]);
|
}, [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
|
// 发送选择预定义 skill
|
||||||
const sendSelectSkill = useCallback((skill_id: string) => {
|
const sendSelectSkill = useCallback((skill_id: string) => {
|
||||||
const message = { type: MESSAGE_TYPES.SELECT_SKILL, skill_id };
|
const message = { type: MESSAGE_TYPES.SELECT_SKILL, skill_id };
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue