fix: 修复新会话重复请求并更新部署脚本默认目标

This commit is contained in:
肖应宇 2026-04-08 16:02:14 +08:00
parent fd884bd676
commit 1243bd0aac
5 changed files with 52 additions and 65 deletions

View File

@ -67,10 +67,19 @@ export default function ChatPage() {
// 新逻辑:历史渲染和新会话仅由路由 /chats/new 控制,不再读取 isnew/is_chatting 参数。 // 新逻辑:历史渲染和新会话仅由路由 /chats/new 控制,不再读取 isnew/is_chatting 参数。
const shouldRenderHistory = !showWelcomeStyle; const shouldRenderHistory = !showWelcomeStyle;
const createNewSession = useMemo(() => isNewThread, [isNewThread]); const createNewSession = useMemo(() => isNewThread, [isNewThread]);
const safeThreadId = useMemo(() => {
if (!threadId || threadId === "new") {
return undefined;
}
return threadId;
}, [threadId]);
const streamThreadId = useMemo(() => { const streamThreadId = useMemo(() => {
return isNewThread && createNewSession ? undefined : threadId; if (isNewThread && createNewSession) {
}, [createNewSession, isNewThread, threadId]); return undefined;
}
return safeThreadId;
}, [createNewSession, isNewThread, safeThreadId]);
const { showNotification } = useNotification(); const { showNotification } = useNotification();
@ -79,7 +88,7 @@ export default function ChatPage() {
skillError: selectedSkillError, skillError: selectedSkillError,
clearSkillError: clearSelectedSkillError, clearSkillError: clearSelectedSkillError,
isBootstrapping: isSelectedSkillBootstrapping, isBootstrapping: isSelectedSkillBootstrapping,
} = useSelectedSkillListener({ threadId }); } = useSelectedSkillListener({ threadId: safeThreadId ?? null });
// 对话行为控制器 // 对话行为控制器
const [thread, sendMessage, isUploading] = useThreadStream({ const [thread, sendMessage, isUploading] = useThreadStream({
threadId: streamThreadId, threadId: streamThreadId,
@ -495,7 +504,7 @@ export default function ChatPage() {
if (threadId && threadId !== "new") { if (threadId && threadId !== "new") {
nextQuery.set("thread_id", threadId); nextQuery.set("thread_id", threadId);
} }
router.replace(`/workspace/chats/new?${nextQuery.toString()}`); router.replace(`/workspace/chats/${threadId}?is_chatting=false`);
}} }}
> >

View File

@ -1,7 +1,7 @@
"use client"; "use client";
import { useParams, usePathname, useSearchParams } from "next/navigation"; import { useParams, usePathname, useSearchParams } from "next/navigation";
import { useCallback, useEffect, useState } from "react"; import { useEffect, useState } from "react";
export function useThreadChat() { export function useThreadChat() {
@ -26,53 +26,13 @@ export function useThreadChat() {
const rawPathThreadId = params?.thread_id ?? threadIdFromPathname; const rawPathThreadId = params?.thread_id ?? threadIdFromPathname;
const isNewRoute = rawPathThreadId === "new"; const isNewRoute = rawPathThreadId === "new";
const threadIdFromPathOrParams:string = isNewRoute const threadIdFromPathOrParams = isNewRoute
? threadIdFromSearchParams?? params.thread_id ? normalizeThreadId(threadIdFromSearchParams)
: params.thread_id; : normalizeThreadId(rawPathThreadId);
// console.log("[useThreadChat] pathname", pathname); // console.log("[useThreadChat] pathname", pathname);
// console.log("[useThreadChat] params.thread_id", params?.thread_id); // console.log("[useThreadChat] params.thread_id", params?.thread_id);
// console.log("[useThreadChat] threadIdFromPathname", threadIdFromPathname); // console.log("[useThreadChat] threadIdFromPathname", threadIdFromPathname);
// console.log("[useThreadChat] threadIdFromPath", threadIdFromPath); // console.log("[useThreadChat] threadIdFromPath", threadIdFromPath);
// 持久化兜底:用于处理首屏水合或 params 时序问题。
const readStoredThreadId = () => {
if (typeof window === "undefined") {
return undefined;
}
const stored = window.sessionStorage.getItem("workspace.thread_id");
return isValidThreadId(stored) ? stored : undefined;
};
// 读取 query 的 thread_id先用 hook必要时用 window 兜底)。
const readQueryThreadId = () => {
const fromHook = threadIdFromSearchParams;
if (isValidThreadId(fromHook)) {
return fromHook;
}
if (typeof window === "undefined") {
return undefined;
}
const fromLocation = new URLSearchParams(window.location.search).get(
"thread_id",
);
if (isValidThreadId(fromLocation)) {
return fromLocation.trim();
}
return undefined;
};
const queryThreadIdFromParams = readQueryThreadId();
// console.log("[useThreadChat] query.thread_id", queryThreadIdFromParams);
// 归一化:当值为 "new" 时,替换为 query 中的 thread_id如果存在
const normalizeThreadId = useCallback(
(value?: string | null) => {
if (!value) {
return undefined;
}
return value === "new" ? queryThreadIdFromParams : value;
},
[queryThreadIdFromParams],
);
// New session is only controlled by `/workspace/chats/new`. // New session is only controlled by `/workspace/chats/new`.
const [isNewThread, setIsNewThread] = useState(() => isNewRoute); const [isNewThread, setIsNewThread] = useState(() => isNewRoute);
@ -82,7 +42,7 @@ export function useThreadChat() {
// console.log("[useThreadChat] effectiveThreadIdFromPath", effectiveThreadIdFromPath); // console.log("[useThreadChat] effectiveThreadIdFromPath", effectiveThreadIdFromPath);
const [threadId, setThreadId] = useState<string>(() => { const [threadId, setThreadId] = useState<string>(() => {
return threadIdFromPathOrParams; return threadIdFromPathOrParams ?? "";
}); });
@ -93,11 +53,10 @@ export function useThreadChat() {
} }
setIsNewThread(isNewRoute); setIsNewThread(isNewRoute);
// Prefer path thread id, fall back to query thread_id when path is /new. // Prefer path thread id, fall back to query thread_id when path is /new.
setThreadId(threadIdFromPathOrParams); setThreadId(threadIdFromPathOrParams ?? "");
setShowWelcomeStyle(isNewRoute || !isChattingFromQuery); setShowWelcomeStyle(isNewRoute || !isChattingFromQuery);
}, [ }, [
isNewRoute, isNewRoute,
normalizeThreadId,
pathname, pathname,
searchParams, searchParams,
isChattingFromQuery, isChattingFromQuery,
@ -114,6 +73,11 @@ export function useThreadChat() {
}; };
} }
function normalizeThreadId(value?: string | null): string | undefined {
if (!value) return undefined;
return isValidThreadId(value) ? value.trim() : undefined;
}
function isValidThreadId(value?: string | null): value is string { function isValidThreadId(value?: string | null): value is string {
if (!value) return false; if (!value) return false;
const normalized = value.trim().toLowerCase(); const normalized = value.trim().toLowerCase();

View File

@ -117,31 +117,31 @@ export const zhCN: Translations = {
prompt: prompt:
"为[主题/产品]撰写吸引人的自媒体文案,包括标题、正文和话题标签。", "为[主题/产品]撰写吸引人的自媒体文案,包括标题、正文和话题标签。",
icon: PenLineIcon, icon: PenLineIcon,
children: [{ id: "1245", name: "自媒体文案" }], children: [{ id: "1245", name: "微信文章撰写" }],
}, },
{ {
suggestion: "需求文档", suggestion: "需求文档",
prompt: "编写[项目/功能]的需求文档,包含功能描述、用户故事和验收标准。", prompt: "编写[项目/功能]的需求文档,包含功能描述、用户故事和验收标准。",
icon: CompassIcon, icon: CompassIcon,
children: [{ id: "520", name: "需求文档" }], children: [{ id: "520", name: "分解功能产品需求文档" }],
}, },
{ {
suggestion: "使用指南", suggestion: "使用指南",
prompt: "编写[产品/功能]的使用指南,包含操作步骤、注意事项和常见问题。", prompt: "编写[产品/功能]的使用指南,包含操作步骤、注意事项和常见问题。",
icon: GraduationCapIcon, icon: GraduationCapIcon,
children: [{ id: "409", name: "使用指南" }], children: [{ id: "409", name: "指南编写" }],
}, },
{ {
suggestion: "Excel数据分析", suggestion: "Excel数据分析",
prompt: "对[Excel文件/数据]进行分析,生成数据洞察和可视化建议。", prompt: "对[Excel文件/数据]进行分析,生成数据洞察和可视化建议。",
icon: MicroscopeIcon, icon: MicroscopeIcon,
children: [{ id: "5", name: "Excel数据分析" }], children: [{ id: "5", name: "数据分析" }],
}, },
{ {
suggestion: "市场调研", suggestion: "市场调研",
prompt: "针对[行业/产品]进行市场调研,分析市场规模、竞品和趋势。", prompt: "针对[行业/产品]进行市场调研,分析市场规模、竞品和趋势。",
icon: ShapesIcon, icon: ShapesIcon,
children: [{ id: "1216", name: "市场研" }], children: [{ id: "1216", name: "市场究报告" }],
}, },
], ],
suggestionsCreate: [ suggestionsCreate: [

View File

@ -328,15 +328,20 @@ export function useThreadStream({
} }
setOptimisticMessages(newOptimistic); setOptimisticMessages(newOptimistic);
if (resolvedThreadId) { // For "new chat with prefilled thread_id" flows, calling onStart before
// submit can trigger route switch too early, which causes the new page to
// fetch history before the thread/run is actually created.
// Let useStream.onCreated -> handleStreamStart drive onStart instead.
if (resolvedThreadId && !createNewSession) {
_handleOnStart(resolvedThreadId); _handleOnStart(resolvedThreadId);
} }
let uploadedFileInfo: UploadedFileInfo[] = []; let uploadedFileInfo: UploadedFileInfo[] = [];
try { try {
// 新会话模式下,删除旧线程并创建同名新线程 // 新会话模式下,仅在本地已有历史消息时才重置旧线程。
if (createNewSession && resolvedThreadId) { // 对于全新 thread_id避免多发一次 DELETE /threads/{id}(通常会 404
if (createNewSession && resolvedThreadId && thread.messages.length > 0) {
await apiClient.threads.delete(resolvedThreadId).catch(() => undefined); await apiClient.threads.delete(resolvedThreadId).catch(() => undefined);
} }

View File

@ -5,16 +5,21 @@ set -euo pipefail
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
cd "$REPO_ROOT" cd "$REPO_ROOT"
# Default deploy target (can be overridden by positional args)
DEFAULT_REMOTE="root@111.228.39.147"
DEFAULT_REMOTE_APP_DIR="/root/deerflow2"
usage() { usage() {
cat <<'EOF' cat <<'EOF'
Usage: Usage:
./scripts/deploy-frontend-standalone.sh <remote> <remote_app_dir> ./scripts/deploy-frontend-standalone.sh [remote] [remote_app_dir]
Arguments: Arguments:
remote SSH target, e.g. user@example.com remote SSH target, optional. Default: root@111.228.39.147
remote_app_dir Remote deerflow2 root dir, e.g. /home/user/deerflow2 remote_app_dir Remote deerflow2 root dir, optional. Default: /root/deerflow2
Example: Example:
./scripts/deploy-frontend-standalone.sh
./scripts/deploy-frontend-standalone.sh ubuntu@1.2.3.4 /opt/deerflow2 ./scripts/deploy-frontend-standalone.sh ubuntu@1.2.3.4 /opt/deerflow2
Notes: Notes:
@ -31,13 +36,17 @@ if [ "${1:-}" = "-h" ] || [ "${1:-}" = "--help" ]; then
exit 0 exit 0
fi fi
if [ "$#" -ne 2 ]; then if [ "$#" -gt 2 ]; then
usage usage
exit 1 exit 1
fi fi
REMOTE="$1" REMOTE="${1:-$DEFAULT_REMOTE}"
REMOTE_APP_DIR="$2" REMOTE_APP_DIR="${2:-$DEFAULT_REMOTE_APP_DIR}"
echo "==> Deploy target:"
echo " remote: $REMOTE"
echo " app dir: $REMOTE_APP_DIR"
echo "==> Building frontend (standalone)..." echo "==> Building frontend (standalone)..."
pnpm -C frontend build pnpm -C frontend build