sync update from LAN repo (source master-yx @ cf505220)

This commit is contained in:
gozeros committed 2026-10-21 15:11:11 +08:00
1 parent e243ec3a46
commit 19d3cb96dc
114 files changed
+14787 -451

No files matched your search

+28 -14
View File
@@ -64,6 +64,10 @@ import {
useThreadSettings,
} from "@/core/settings";
import { useThreadStream, stopActiveThreadBeforeClarificationReply } from "@/core/threads/hooks";
import {
myProjectConversationTypeFromSearchParams,
myProjectThreadMetadata,
} from "@/core/threads/project-metadata";
import { useResearchWritingFeed } from "@/core/threads/use-research-writing-feed";
import { clearResearchReportStream } from "@/core/threads/use-research-report-stream";
import { resolveWritingModeReportPath } from "@/core/threads/writing-mode";
@@ -286,16 +290,28 @@ export default function ChatPage() {
}, [selectedAgentId, selectorAgents, mineAgents, squareAgents, selectorLoading]);
const { showNotification } = useNotification();
const projectConversationType =
myProjectConversationTypeFromSearchParams(searchParams);
const newThreadMetadata = useMemo<Record<string, unknown> | undefined>(() => {
const metadata: Record<string, unknown> = projectConversationType
? myProjectThreadMetadata(projectConversationType)
: {};
if (knowledgeSelectionReady) {
metadata.llmwiki_knowledge_base_ids = selectedKnowledgeBaseIds;
metadata.assistant_knowledge_base_ids = selectedAssistantKnowledgeBaseIds;
}
return Object.keys(metadata).length > 0 ? metadata : undefined;
}, [
knowledgeSelectionReady,
projectConversationType,
selectedAssistantKnowledgeBaseIds,
selectedKnowledgeBaseIds,
]);
const [thread, sendMessage, isUploading] = useThreadStream({
threadId: isNewThread ? undefined : threadId,
context: streamContext,
threadMetadata: knowledgeSelectionReady
? {
llmwiki_knowledge_base_ids: selectedKnowledgeBaseIds,
assistant_knowledge_base_ids: selectedAssistantKnowledgeBaseIds,
}
: undefined,
threadMetadata: newThreadMetadata,
isMock,
onStart: (createdThreadId) => {
knowledgeSelectionThread.current = createdThreadId;
@@ -462,7 +478,9 @@ export default function ChatPage() {
try {
let targetThreadId = threadId;
if (isNewThread) {
const created = await getAPIClient(isMock).threads.create({});
const created = await getAPIClient(isMock).threads.create({
metadata: newThreadMetadata ?? {},
});
targetThreadId = created.thread_id;
setThreadId(targetThreadId);
setIsNewThread(false);
@@ -522,13 +540,8 @@ export default function ChatPage() {
try {
const created = await getAPIClient(isMock).threads.create({
metadata: {
...newThreadMetadata,
agent_id: selectedAgentId,
...(knowledgeSelectionReady
? {
llmwiki_knowledge_base_ids: selectedKnowledgeBaseIds,
assistant_knowledge_base_ids: selectedAssistantKnowledgeBaseIds,
}
: {}),
},
});
sessionStorage.setItem(
@@ -547,7 +560,7 @@ export default function ChatPage() {
);
saveLastAgentId(selectedAgentId);
router.push(
`/page/workspace/agents/${selectedAgentId}/chats/${created.thread_id}`,
`${routeBase}/agents/${encodeURIComponent(selectedAgentId)}/chats/${encodeURIComponent(created.thread_id)}`,
);
} catch (err) {
agentHandoffPendingRef.current = false;
@@ -606,6 +619,7 @@ export default function ChatPage() {
selectedAssistantKnowledgeBaseIds,
selectedKnowledgeBaseIds,
knowledgeSelectionReady,
newThreadMetadata,
agentHandoffPending,
isMock,
router,
@@ -5,6 +5,7 @@ import {
ChevronDown,
CircleHelp,
Clock3,
FolderKanban,
LoaderCircle,
MessageSquareText,
PanelLeftClose,
@@ -20,7 +21,12 @@ import {
} from "lucide-react";
import { type FormEvent, useCallback, useMemo, useState } from "react";
import { useLocation, useNavigate } from "react-router-dom";
import { Toaster } from "sonner";
import {
MyProjectThreadBadge,
MyProjectThreadIcon,
} from "@/components/workspace/my-project-thread-indicator";
import { useAgent, useAgents } from "@/core/agents";
import { getAccountDisplayName } from "@/core/auth";
import {
@@ -29,28 +35,41 @@ import {
useScheduledTaskRuns,
useScheduledTasks,
} from "@/core/scheduled-tasks";
import { useLocalSettings } from "@/core/settings";
import {
useAppearanceSettings,
useCitationDisplaySettings,
} from "@/core/system-settings/hooks";
import { useThreadStream, useThreads } from "@/core/threads/hooks";
import {
myProjectConversationTypeOfThread,
myProjectConversationTypeFromSearchParams,
myProjectThreadMetadata,
} from "@/core/threads/project-metadata";
import { textOfMessage, titleOfThread } from "@/core/threads/utils";
import { uuid } from "@/core/utils/uuid";
import {
OFFICIAL_DEERFLOW_HOME_ROUTE,
OFFICIAL_DEERFLOW_ROUTE_PREFIX,
} from "@/core/official-deerflow/routes";
import StrategyProjectsPage from "@/strategy-selection/StrategyProjectsPage";
import StrategySelectionPage from "@/strategy-selection/StrategySelectionPage";
import "./official-deerflow-entry.css";
type OfficialPage = "chat" | "agents" | "agent-chat" | "scheduled" | "settings";
type OfficialPage =
| "chat"
| "projects"
| "agents"
| "agent-chat"
| "scheduled"
| "settings";
function pageFromPath(pathname: string): OfficialPage {
const path = pathname.slice(OFFICIAL_DEERFLOW_ROUTE_PREFIX.length);
if (path.startsWith("/workspace/agents/") && path.includes("/chats")) {
return "agent-chat";
}
if (path.startsWith("/workspace/projects")) return "projects";
if (path.startsWith("/workspace/agents")) return "agents";
if (path.startsWith("/workspace/scheduled-tasks")) return "scheduled";
if (path.startsWith("/workspace/settings")) return "settings";
@@ -78,6 +97,11 @@ function parseScheduledTaskId(pathname: string) {
return match ? decodeURIComponent(match[1]!) : null;
}
function parseProjectId(pathname: string) {
const match = pathname.match(/\/workspace\/projects\/([^/]+)/);
return match ? decodeURIComponent(match[1]!) : null;
}
function readableError(error: unknown, fallback: string) {
return error instanceof Error && error.message ? error.message : fallback;
}
@@ -159,20 +183,36 @@ function ChatWorkspace({
routeThreadId: string;
}) {
const navigate = useNavigate();
const location = useLocation();
const [prompt, setPrompt] = useState("");
const [sendError, setSendError] = useState<string | null>(null);
const isNewThread = routeThreadId === "new";
const projectConversationType = isNewThread
? myProjectConversationTypeFromSearchParams(
new URLSearchParams(location.search),
)
: null;
// A new conversation uses the same client-generated id approach as the
// existing ChatPage. The backend owns the persisted id once streaming starts.
const [draftThreadId] = useState<string>(() => uuid() as string);
const activeThreadId = isNewThread ? draftThreadId : routeThreadId;
const { agent } = useAgent(agentId, { enabled: !!agentId });
const [localSettings] = useLocalSettings();
const [thread, sendMessage, isUploading] = useThreadStream({
threadId: isNewThread ? undefined : routeThreadId,
assistantId: agentId ?? undefined,
threadMetadata: projectConversationType
? myProjectThreadMetadata(projectConversationType)
: undefined,
context: {
...localSettings.context,
// This compact official shell has no model/mode controls of its own.
// Do not inherit a hidden selection saved by the customized workspace
// (for example an obsolete or quota-exhausted model); an omitted model
// lets the backend resolve the current system default or the agent's
// globally configured model.
model_name: undefined,
mode: undefined,
reasoning_effort: undefined,
writing_mode: false,
...(agentId ? { agent_id: agentId } : {}),
},
onStart: (createdThreadId) => {
@@ -658,6 +698,10 @@ export default function OfficialDeerFlowEntry() {
() => parseScheduledTaskId(location.pathname),
[location.pathname],
);
const projectId = useMemo(
() => parseProjectId(location.pathname),
[location.pathname],
);
const { data: threads = [], isLoading: threadsLoading, error: threadsError } =
useThreads({
limit: 20,
@@ -666,10 +710,12 @@ export default function OfficialDeerFlowEntry() {
select: ["thread_id", "updated_at", "values", "metadata"],
});
const go = (path: string) => navigate(path);
const projectBasePath = `${OFFICIAL_DEERFLOW_ROUTE_PREFIX}/workspace/projects`;
const nav = [
{ icon: PlusSquare, label: "新对话", path: OFFICIAL_DEERFLOW_HOME_ROUTE, pages: ["chat"] },
{ icon: MessageSquareText, label: "对话", path: `${OFFICIAL_DEERFLOW_ROUTE_PREFIX}/workspace/chats/new`, pages: ["chat"] },
{ icon: FolderKanban, label: "我的项目", path: projectBasePath, pages: ["projects"] },
{ icon: Bot, label: "智能体", path: `${OFFICIAL_DEERFLOW_ROUTE_PREFIX}/workspace/agents`, pages: ["agents", "agent-chat"] },
{ icon: CalendarClock, label: "定时任务", path: `${OFFICIAL_DEERFLOW_ROUTE_PREFIX}/workspace/scheduled-tasks`, pages: ["scheduled"] },
] as const;
@@ -706,29 +752,43 @@ export default function OfficialDeerFlowEntry() {
) : threadsError ? (
<p className="official-sidebar-error">对话列表加载失败</p>
) : threads.length ? (
threads.map((chat) => (
<button
className={`official-recent-chat${chatRoute.threadId === chat.thread_id ? " is-active" : ""}`}
key={chat.thread_id}
onClick={() => {
const agentId = typeof chat.context?.agent_id === "string"
? chat.context.agent_id
: typeof chat.metadata?.agent_id === "string"
? chat.metadata.agent_id
: null;
go(
agentId
? `${OFFICIAL_DEERFLOW_ROUTE_PREFIX}/workspace/agents/${encodeURIComponent(agentId)}/chats/${encodeURIComponent(chat.thread_id)}`
: `${OFFICIAL_DEERFLOW_ROUTE_PREFIX}/workspace/chats/${encodeURIComponent(chat.thread_id)}`,
);
}}
type="button"
>
<MessageSquareText aria-hidden="true" size={15} />
<span>{titleOfThread(chat, "未命名对话")}</span>
<small>{formatTimestamp(chat.updated_at)}</small>
</button>
))
threads.map((chat) => {
const projectType = myProjectConversationTypeOfThread(chat);
return (
<button
className={`official-recent-chat${chatRoute.threadId === chat.thread_id ? " is-active" : ""}`}
key={chat.thread_id}
onClick={() => {
const agentId = typeof chat.context?.agent_id === "string"
? chat.context.agent_id
: typeof chat.metadata?.agent_id === "string"
? chat.metadata.agent_id
: null;
go(
agentId
? `${OFFICIAL_DEERFLOW_ROUTE_PREFIX}/workspace/agents/${encodeURIComponent(agentId)}/chats/${encodeURIComponent(chat.thread_id)}`
: `${OFFICIAL_DEERFLOW_ROUTE_PREFIX}/workspace/chats/${encodeURIComponent(chat.thread_id)}`,
);
}}
type="button"
>
{projectType ? (
<MyProjectThreadIcon className="size-[15px]" type={projectType} />
) : (
<MessageSquareText aria-hidden="true" size={15} />
)}
<span className="official-recent-chat-copy">
<span className="official-recent-chat-title">
{titleOfThread(chat, "未命名对话")}
</span>
{projectType && (
<MyProjectThreadBadge type={projectType} />
)}
</span>
<small>{formatTimestamp(chat.updated_at)}</small>
</button>
);
})
) : (
<p className="official-sidebar-empty">暂无历史对话</p>
)}
@@ -754,6 +814,17 @@ export default function OfficialDeerFlowEntry() {
routeThreadId={chatRoute.threadId}
/>
)}
{page === "projects" && (
projectId ? (
<StrategySelectionPage
key={projectId}
projectBasePath={projectBasePath}
projectId={projectId}
/>
) : (
<StrategyProjectsPage basePath={projectBasePath} />
)
)}
{page === "agents" && (
<AgentsPage
openAgent={(agentId) =>
@@ -771,6 +842,7 @@ export default function OfficialDeerFlowEntry() {
{page === "scheduled" && <ScheduledTasksPage selectedTaskId={scheduledTaskId} />}
{page === "settings" && <SettingsPage />}
</div>
<Toaster position="top-center" />
</main>
);
}
+22 -1
View File
@@ -1,4 +1,4 @@
import { Navigate, Route, Routes } from "react-router-dom";
import { Navigate, Route, Routes, useParams } from "react-router-dom";
import { Toaster } from "sonner";
import { SidebarInset, SidebarProvider } from "@/components/ui/sidebar";
@@ -78,6 +78,8 @@ import UsersPage from "./UsersPage";
import LlmMetricsPage from "./LlmMetricsPage";
import AdminScheduledTasksPage from "./AdminScheduledTasksPage";
import AdminUserActivityPage from "./AdminUserActivityPage";
import StrategyProjectsPage from "@/strategy-selection/StrategyProjectsPage";
import StrategySelectionPage from "@/strategy-selection/StrategySelectionPage";
import { ChatRuntime } from "./WorkspaceRoutes";
/**
@@ -98,6 +100,11 @@ function StrategyChatThreadLayout() {
);
}
function StrategyProjectWorkbenchRoute() {
const { project_id: projectId = "" } = useParams<{ project_id: string }>();
return <StrategySelectionPage key={projectId} projectId={projectId} />;
}
export default function StrategyRoutes() {
const storedAuth = getStoredAuth();
const canManageBusinessMappings = !storedAuth || storedAuth.system_role === "admin";
@@ -110,6 +117,20 @@ export default function StrategyRoutes() {
<Route index element={<Navigate to="qa/general" replace />} />
<Route path="qa/general" element={<GeneralQA />} />
<Route
path="qa/projects"
element={
<StrategyProjectsPage basePath="/page/strategy/qa/projects" />
}
/>
<Route
path="qa/projects/:project_id"
element={<StrategyProjectWorkbenchRoute />}
/>
<Route
path="qa/strategy-selection"
element={<Navigate to="/page/strategy/qa/projects" replace />}
/>
<Route
path="qa/expert-qa"
element={
@@ -64,6 +64,8 @@
.official-recent-section > p { color: #90908e; font-size: 12px; margin: 0 8px 9px; }
.official-recent-chat { display: grid; font-size: 13px; gap: 8px; grid-template-columns: 15px minmax(0, 1fr) auto; padding: 8px; }
.official-recent-chat span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.official-recent-chat-copy { align-items: center; display: flex; gap: 6px; min-width: 0; }
.official-recent-chat-title { flex: 1; min-width: 0; }
.official-recent-chat small { color: #acaca9; font-size: 11px; }
.official-recent-chat.is-active { background: #efefed; color: #252525; }
.official-sidebar-loading,