feat(turbo): add direct LLM chat mode with runtime toggle

Add 极速 mode that streams via /api/direct-chat without the lead agent stack,
plus VITE_TURBO_MODE_VISIBLE in runtime-config.js (default off). Also disable
global memory, tighten Qwen/vLLM thinking control for flash mode, and force
thinking off on the direct-chat endpoint.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
董宏凯andCursor committed 2026-09-09 21:58:36 +08:00
1 parent 4f7f07504a
commit d765afe2ac
23 files changed
+1295 -394

No files matched your search

+87 -8
View File
@@ -13,6 +13,7 @@ import { useLocation } from "react-router-dom";
import { useRouteBase } from "@/core/route-base";
import { useCallback, useEffect, useMemo, useRef, useState, type ReactNode } from "react";
import { useQuery } from "@tanstack/react-query";
import { toast } from "sonner";
import type { PromptInputMessage } from "@/components/ai-elements/prompt-input";
import { Button } from "@/components/ui/button";
@@ -48,6 +49,7 @@ import { useNotification } from "@/core/notification/hooks";
import { saveLastAgentId, useThreadSettings } from "@/core/settings";
import { stopActiveThreadBeforeClarificationReply, stopThreadRun, useThreadStream } from "@/core/threads/hooks";
import { useResearchWritingFeed } from "@/core/threads/use-research-writing-feed";
import { useDirectChatMessages, isDirectChatProfile, isTurboModeVisible } from "@/core/threads/use-direct-chat";
import { clearResearchReportStream } from "@/core/threads/use-research-report-stream";
import { textOfMessage } from "@/core/threads/utils";
import { resolveWritingModeReportPath } from "@/core/threads/writing-mode";
@@ -560,9 +562,26 @@ export default function AgentChatPage({
},
});
const directChat = useDirectChatMessages(threadId);
const directChatSelected =
isDirectChatProfile(settings.context) && isTurboModeVisible();
// 写作模式深度研究进度:注入 displayThread 尾部(进度步骤条 + report.md
// 流式 stub),成功且真实产物落地后自动撤出。
const { displayThread } = useResearchWritingFeed(threadId, thread);
const { displayThread: researchDisplayThread } = useResearchWritingFeed(threadId, thread);
const displayThread = useMemo(() => {
const base = researchDisplayThread;
if (directChat.messages.length === 0 && !directChat.isLoading) {
return base;
}
const messages = [...base.messages, ...directChat.messages];
return {
...base,
values: { ...base.values, messages },
messages,
isLoading: base.isLoading || directChat.isLoading,
} as typeof base;
}, [directChat.isLoading, directChat.messages, researchDisplayThread]);
// ── agentfx「入库」协助卡 ──────────────────────────────────────────────────
// 任务工作区(taskBind)里,内置研判智能体写出的 task-reports.json 产物落地且
@@ -743,6 +762,62 @@ export default function AgentChatPage({
const handleSubmit = useCallback(
(message: PromptInputMessage, extraContext?: Record<string, unknown>) => {
if (directChatSelected) {
if (message.files && message.files.length > 0) {
toast.error("极速问答暂不支持附件,请移除附件后重试。");
return;
}
setHasStartedConversation(true);
saveLastAgentId(agent_id);
window.dispatchEvent(
new CustomEvent("workspace:sandbox-resume-autoscroll"),
);
void (async () => {
try {
let targetThreadId = threadId;
if (isNewThread) {
const created = await getAPIClient().threads.create({
metadata: {
agent_id,
...(Object.keys(conversationMetadata).length > 0
? conversationMetadata
: {}),
},
});
targetThreadId = created.thread_id;
knowledgeSelectionThread.current = targetThreadId;
activeThreadIdRef.current = targetThreadId;
setThreadId(targetThreadId);
setIsNewThread(false);
history.replaceState(
null,
"",
`${window.location.pathname}${window.location.search}#${agentChatsBase}/${targetThreadId}`,
);
const title = message.text.trim().replace(/\s+/g, " ").slice(0, 60);
if (title) {
void getAPIClient().threads
.updateState(targetThreadId, { values: { title } })
.catch(() => undefined);
}
}
if (!targetThreadId) {
toast.error("无法创建会话。");
return;
}
await directChat.submit({
targetThreadId,
query: message.text,
modelName: settings.context.model_name,
});
} catch (error) {
toast.error(
error instanceof Error ? error.message : "极速问答失败。",
);
}
})();
return;
}
setHasStartedConversation(true);
// Remember this agent so the new-chat landing screen pre-selects it next
// time (covers both direct agent chats and the ChatPage handoff replay).
@@ -767,7 +842,7 @@ export default function AgentChatPage({
...pageReportThemeContext,
});
},
[sendMessage, threadId, agent_id, settings.context.writing_mode, writingArtifactPath, baseMode],
[sendMessage, threadId, agent_id, settings.context.writing_mode, settings.context.model_name, writingArtifactPath, baseMode, directChat, directChatSelected, isNewThread, conversationMetadata, agentChatsBase, setThreadId, setIsNewThread],
);
// 任务流程:切换 / 新建会话时,停掉「上一会话服务端在跑」的 run(老对话要停止)。
@@ -852,8 +927,12 @@ export default function AgentChatPage({
);
const handleStop = useCallback(async () => {
if (directChat.isLoading) {
directChat.stop();
return;
}
await thread.stop();
}, [thread]);
}, [directChat, thread]);
const isLandingState = isNewThread && !hasStartedConversation;
// 任务闸门面板:仅 rwfx(taskGate)在新建对话且尚未开始时,于问答页顶部展示目的树 + 覆盖确认。
@@ -867,7 +946,7 @@ export default function AgentChatPage({
const status = thread.error
? "error"
: thread.isLoading
: thread.isLoading || directChat.isLoading
? "streaming"
: "ready";
@@ -1084,7 +1163,7 @@ export default function AgentChatPage({
autoFocus
status={status}
context={settings.context}
disabled={isDemoMode || knowledgeSelectionLoading}
disabled={isDemoMode || knowledgeSelectionLoading || directChat.isLoading}
onContextChange={(context) =>
setSettings("context", context)
}
@@ -1104,7 +1183,7 @@ export default function AgentChatPage({
pendingKnowledgeSelection.current === null &&
pendingAssistantKnowledgeSelection.current === null
}
disabled={isDemoMode || knowledgeSelectionLoading}
disabled={isDemoMode || knowledgeSelectionLoading || directChat.isLoading}
label="知识空间"
/>
)
@@ -1175,7 +1254,7 @@ export default function AgentChatPage({
assistantId={agent_id}
status={status}
context={settings.context}
disabled={isDemoMode || knowledgeSelectionLoading}
disabled={isDemoMode || knowledgeSelectionLoading || directChat.isLoading}
onContextChange={(context) =>
setSettings("context", context)
}
@@ -1190,7 +1269,7 @@ export default function AgentChatPage({
assistantValue={selectedAssistantKnowledgeBaseIds}
onChange={changeKnowledgeSelection}
onAssistantChange={changeAssistantKnowledgeSelection}
disabled={isDemoMode || knowledgeSelectionLoading}
disabled={isDemoMode || knowledgeSelectionLoading || directChat.isLoading}
readOnly
label="知识空间"
/>
+69 -5
View File
@@ -69,6 +69,7 @@ import { clearResearchReportStream } from "@/core/threads/use-research-report-st
import { resolveWritingModeReportPath } from "@/core/threads/writing-mode";
import { queueLlmWikiConversationDeposit } from "@/core/llmwiki/conversation-deposit";
import { useSingleRoundRagMessages } from "@/core/threads/use-single-round-rag";
import { useDirectChatMessages, isDirectChatProfile, isTurboModeVisible } from "@/core/threads/use-direct-chat";
import { useMultiRoundRagMessages, mergeMessagesByTime, toLangGraphMessages } from "@/core/threads/use-multi-round-rag";
import { useLayoutMode } from "@/core/system-settings/hooks";
import { useRouteBase } from "@/core/route-base";
@@ -348,9 +349,12 @@ export default function ChatPage() {
});
const singleRoundRag = useSingleRoundRagMessages(threadId);
const multiRoundRag = useMultiRoundRagMessages(threadId);
const directChat = useDirectChatMessages(threadId);
const singleRoundRagSelected = isSingleRoundRagProfile(settings.context);
const multiRoundRagSelected = isMultiRoundRagProfile(settings.context);
const frontendRagSelected = isFrontendRagProfile(settings.context);
const directChatSelected =
isDirectChatProfile(settings.context) && isTurboModeVisible();
// 写作模式深度研究进度:注入 displayThread 尾部(进度步骤条 + report.md
// 流式 stub),成功且真实产物落地后自动撤出。
const { displayThread: researchDisplayThread } = useResearchWritingFeed(threadId, thread);
@@ -360,13 +364,15 @@ export default function ChatPage() {
if (
singleRoundRag.messages.length === 0 &&
ragMessages.length === 0 &&
directChat.messages.length === 0 &&
!singleRoundRag.isLoading &&
!multiRoundRag.isLoading
!multiRoundRag.isLoading &&
!directChat.isLoading
) {
return base;
}
const messages = mergeMessagesByTime(
[...base.messages, ...singleRoundRag.messages],
[...base.messages, ...singleRoundRag.messages, ...directChat.messages],
ragMessages,
);
return {
@@ -374,9 +380,14 @@ export default function ChatPage() {
values: { ...base.values, messages },
messages,
isLoading:
base.isLoading || singleRoundRag.isLoading || multiRoundRag.isLoading,
base.isLoading ||
singleRoundRag.isLoading ||
multiRoundRag.isLoading ||
directChat.isLoading,
} as typeof base;
}, [
directChat.isLoading,
directChat.messages,
multiRoundRag.isLoading,
multiRoundRag.messages,
researchDisplayThread,
@@ -392,6 +403,52 @@ export default function ChatPage() {
const handleSubmit = useCallback(
(message: PromptInputMessage) => {
if (directChatSelected) {
if (message.files && message.files.length > 0) {
toast.error("极速问答暂不支持附件,请移除附件后重试。");
return;
}
setHasStartedConversation(true);
window.dispatchEvent(
new CustomEvent("workspace:sandbox-resume-autoscroll"),
);
void (async () => {
try {
let targetThreadId = threadId;
if (isNewThread) {
const created = await getAPIClient(isMock).threads.create({});
targetThreadId = created.thread_id;
setThreadId(targetThreadId);
setIsNewThread(false);
history.replaceState(
null,
"",
`${window.location.pathname}${window.location.search}#${routeBase}/chats/${targetThreadId}`,
);
const title = message.text.trim().replace(/\s+/g, " ").slice(0, 60);
if (title) {
void getAPIClient(isMock).threads
.updateState(targetThreadId, { values: { title } })
.catch(() => undefined);
}
}
if (!targetThreadId) {
toast.error("无法创建会话。");
return;
}
await directChat.submit({
targetThreadId,
query: message.text,
modelName: settings.context.model_name,
});
} catch (error) {
toast.error(
error instanceof Error ? error.message : "极速问答失败。",
);
}
})();
return;
}
if (frontendRagSelected) {
if (message.files && message.files.length > 0) {
toast.error("RAG 问答暂不支持附件,请移除附件后重试。");
@@ -560,6 +617,8 @@ export default function ChatPage() {
multiRoundRag,
multiRoundRagSelected,
frontendRagSelected,
directChat,
directChatSelected,
routeBase,
setIsNewThread,
setThreadId,
@@ -655,6 +714,10 @@ export default function ChatPage() {
saveLastAgentId(agentId);
}, []);
const handleStop = useCallback(async () => {
if (directChat.isLoading) {
directChat.stop();
return;
}
if (singleRoundRag.isLoading) {
singleRoundRag.stop();
return;
@@ -664,7 +727,7 @@ export default function ChatPage() {
return;
}
await thread.stop();
}, [multiRoundRag, singleRoundRag, thread]);
}, [directChat, multiRoundRag, singleRoundRag, thread]);
const promptPrefixProp = useMemo(() => {
if (!promptPrefixSettings?.enabled) return undefined;
@@ -914,7 +977,8 @@ export default function ChatPage() {
agentHandoffPending ||
knowledgeSelectionLoading ||
singleRoundRag.isLoading ||
multiRoundRag.isLoading
multiRoundRag.isLoading ||
directChat.isLoading
}
onContextChange={(context) =>
setSettings("context", context)