fix: 等待 thread 状态可读以避免过早展示chat页面

This commit is contained in:
Titan 2026-03-19 19:15:51 +08:00
parent 0a38e14b3e
commit f67aa27434
3 changed files with 71 additions and 31 deletions

View File

@ -263,11 +263,11 @@ docker-logs-gateway:
# ==========================================
# Docker Publish Command
# ==========================================
# Usage: make docker-publish VER=v220.20251202 SVC=frontend
# Example: make docker-publish VER=v220.20251202 SVC=frontend
# Usage: make docker-publish VER=[version] SVC=[service name] [PUSH=1]
# Example: make docker-publish VER=v2.0.20251202 SVC=frontend PUSH=0
docker-publish:
@if [ -z "$(VER)" ]; then \
echo "✗ VER is required (e.g. v220.20251202)"; \
echo "✗ VER is required (e.g. v2.0.20251202)"; \
exit 1; \
fi
@if [ -z "$(SVC)" ]; then \
@ -293,9 +293,13 @@ docker-publish:
echo "✗ Docker build failed"; \
exit 1; \
fi; \
docker push $$IMAGE; \
if [ $$? -ne 0 ]; then \
echo "✗ Docker push failed"; \
exit 1; \
fi; \
echo "✓ Docker image $$IMAGE built and pushed successfully"
if [ "$(PUSH)" = "0" ]; then \
echo "✓ Docker image $$IMAGE built successfully (not pushed)"; \
else \
docker push $$IMAGE; \
if [ $$? -ne 0 ]; then \
echo "✗ Docker push failed"; \
exit 1; \
fi; \
echo "✓ Docker image $$IMAGE built and pushed successfully"; \
fi

View File

@ -32,6 +32,7 @@ import { DevTodoList } from "@/components/workspace/dev-todo-list";
import { IframeTestPanel } from "@/components/workspace/iframe-test-panel";
import { InputBox } from "@/components/workspace/input-box";
import { MessageList } from "@/components/workspace/messages";
import { MessageListSkeleton } from "@/components/workspace/messages/skeleton";
import { ThreadContext } from "@/components/workspace/messages/context";
import { ThreadTitle } from "@/components/workspace/thread-title";
import { TodoList } from "@/components/workspace/todo-list";
@ -165,6 +166,10 @@ export default function ChatPage() {
const [hasSubmitted, setHasSubmitted] = useState(false);
const suppressExistingThreadPrefetchUi = reuseExistingThread && !hasSubmitted;
const suppressNewThreadSubmitUi =
isNewThread && createNewSession && hasSubmitted;
const suppressConversationUi =
suppressExistingThreadPrefetchUi || suppressNewThreadSubmitUi;
useEffect(() => {
const pageTitle = isNewThread
@ -172,7 +177,7 @@ export default function ChatPage() {
: thread.values?.title && thread.values.title !== "Untitled"
? thread.values.title
: t.pages.untitled;
if (thread.isThreadLoading && !suppressExistingThreadPrefetchUi) {
if (thread.isThreadLoading && !suppressConversationUi) {
document.title = `Loading... - ${t.pages.appName}`;
} else {
document.title = `${pageTitle} - ${t.pages.appName}`;
@ -184,19 +189,21 @@ export default function ChatPage() {
t.pages.appName,
thread.values.title,
thread.isThreadLoading,
suppressExistingThreadPrefetchUi,
suppressConversationUi,
]);
const [autoSelectFirstArtifact, setAutoSelectFirstArtifact] = useState(true);
useEffect(() => {
setArtifacts(thread.values.artifacts);
if (
env.NEXT_PUBLIC_STATIC_WEBSITE_ONLY === "true" &&
autoSelectFirstArtifact
) {
if (thread?.values?.artifacts?.length > 0) {
setAutoSelectFirstArtifact(false);
selectArtifact(thread.values.artifacts[0]!);
if (!suppressConversationUi) {
setArtifacts(thread.values.artifacts);
if (
env.NEXT_PUBLIC_STATIC_WEBSITE_ONLY === "true" &&
autoSelectFirstArtifact
) {
if (thread?.values?.artifacts?.length > 0) {
setAutoSelectFirstArtifact(false);
selectArtifact(thread.values.artifacts[0]!);
}
}
}
}, [
@ -337,20 +344,21 @@ export default function ChatPage() {
)}
>
<div className="flex size-full justify-center">
<MessageList
className={cn("size-full", !isNewThread && "pt-10")}
threadId={threadId}
thread={thread}
suppressThreadLoading={suppressExistingThreadPrefetchUi}
messagesOverride={
suppressExistingThreadPrefetchUi
? []
: !thread.isLoading && finalState?.messages
{suppressConversationUi ? (
<MessageListSkeleton />
) : (
<MessageList
className={cn("size-full", !isNewThread && "pt-10")}
threadId={threadId}
thread={thread}
messagesOverride={
!thread.isLoading && finalState?.messages
? (finalState.messages as Message[])
: undefined
}
paddingBottom={todoListCollapsed ? 160 : 280}
/>
}
paddingBottom={todoListCollapsed ? 160 : 280}
/>
)}
</div>
</main>
</div>

View File

@ -18,6 +18,29 @@ import type {
AgentThreadState,
} from "./types";
const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
async function waitForThreadStateToBeReadable(
apiClient: ReturnType<typeof getAPIClient>,
threadId: string,
timeoutMs = 3000,
) {
const deadline = Date.now() + timeoutMs;
while (Date.now() < deadline) {
try {
const state = await apiClient.threads.getState<AgentThreadState>(threadId);
if ((state.values.messages?.length ?? 0) > 0) {
return;
}
} catch {
// Ignore transient 404 / not-ready errors while the new thread is being persisted.
}
await sleep(100);
}
}
export function useThreadStream({
threadId,
isNewThread,
@ -188,6 +211,11 @@ export function useSubmitThread({
},
},
);
if (createNewSession && isNewThread && threadId) {
await waitForThreadStateToBeReadable(apiClient, threadId);
}
void queryClient.invalidateQueries({ queryKey: ["threads", "search"] });
afterSubmit?.();
},