diff --git a/frontend-web/src/App.tsx b/frontend-web/src/App.tsx index 660465b4..0c191a09 100644 --- a/frontend-web/src/App.tsx +++ b/frontend-web/src/App.tsx @@ -99,11 +99,15 @@ export function App() { } /> } + element={} /> } + element={} + /> + } /> void; +}; + +type SettingsSection = "account" | "appearance" | "memory" | "tools" | "about"; + +export function PageSidebar({ + collapsed, + onToggleCollapsed, +}: PageSidebarProps) { + const { theme } = useTheme(); + const isDark = theme === "dark"; + const isAdmin = getStoredAuth()?.system_role === "admin"; + const visibleMenuItems = useSidebarMenuItems(!!isAdmin); + const { t } = useI18n(); + const { data: unreadCount = 0 } = useUnreadCount(); + // defaultExpandedIds 是共享的 ReadonlySet,复制一份给 useState(避免外部突变) + const [expandedIds, setExpandedIds] = useState>(() => new Set(defaultExpandedIds)); + const [settingsOpen, setSettingsOpen] = useState(false); + const [settingsDefaultSection, setSettingsDefaultSection] = + useState("appearance"); + const [notificationOpen, setNotificationOpen] = useState(false); + const [mounted, setMounted] = useState(false); + const navigate = useNavigate(); + const { pathname } = useLocation(); + + useEffect(() => { + setMounted(true); + }, []); + + const openSettings = (section: SettingsSection) => { + setSettingsDefaultSection(section); + setSettingsOpen(true); + }; + + const iconButtonClass = isDark + ? "rounded-lg p-2 text-slate-400 transition-all duration-300 ease-out hover:scale-105 hover:bg-slate-800/50 hover:text-white" + : "rounded-lg p-2 text-gray-500 transition-all duration-300 ease-out hover:scale-105 hover:bg-red-50 hover:text-red-600"; + const panelClass = isDark + ? "ml-4 space-y-1 border-l border-blue-800/30 pl-4" + : "ml-4 space-y-1 border-l border-red-100 pl-4"; + const labelClass = (compact: boolean, small = false) => + `overflow-hidden whitespace-nowrap transition-all duration-300 ease-out ${ + compact ? "max-w-0 opacity-0" : "max-w-[220px] opacity-100" + } ${small ? "text-xs" : "text-sm"} flex-1 font-medium`; + const menuButtonClass = (isActive: boolean, depth = 0) => + `group relative flex w-full items-center rounded-[2px] py-2 text-left transition-all duration-300 ease-out theme-transition ${ + collapsed && depth === 0 ? "justify-center px-2" : "gap-3 px-3" + } ${ + isActive + ? isDark + ? "bg-blue-500/15 text-cyan-300 font-semibold" + : "bg-red-50 text-red-600 font-semibold" + : isDark + ? "text-slate-400 hover:bg-slate-800/50 hover:text-white" + : "text-gray-600 hover:bg-red-50 hover:text-red-600" + }`; + const footerButtonClass = menuButtonClass(false, 0); + + const toggleExpanded = (id: string) => { + setExpandedIds((prev) => { + const next = new Set(prev); + if (next.has(id)) { + next.delete(id); + } else { + next.add(id); + } + return next; + }); + }; + + const renderLeaf = (item: MenuItem, depth = 0) => { + const Icon = item.icon; + const isActive = !!item.path && pathname === item.path; + + return ( + item.path && navigate(item.path)} + className={menuButtonClass(isActive, depth)} + > + {isActive && ( + + )} + + {(!collapsed || depth > 0) && ( + 0)}> + {item.label} + + )} + + ); + }; + + const renderBranch = (item: MenuItem, depth = 0) => { + const expanded = expandedIds.has(item.id); + const isTopLevel = depth === 0; + const Icon = depth > 0 && item.icon === Folder && expanded ? FolderOpen : item.icon; + + return ( + + { + if (collapsed && isTopLevel) { + onToggleCollapsed(); + return; + } + toggleExpanded(item.id); + }} + title={collapsed && isTopLevel ? item.label : undefined} + className={menuButtonClass(false, depth)} + > + 0 ? "scale-105" : "" + }`} + /> + {(!collapsed || !isTopLevel) && ( + 0)}> + {item.label} + + )} + {(!collapsed || !isTopLevel) && ( + + )} + + + {(!collapsed || !isTopLevel) && item.children && ( + + {item.children.map((child) => + child.children ? renderBranch(child, depth + 1) : renderLeaf(child, depth + 1), + )} + + )} + + ); + }; + + return ( + <> + + + + > + ); +} diff --git a/frontend-web/src/components/page-sidebar.tsx b/frontend-web/src/components/page-sidebar.tsx index 58e59a72..42b556fe 100644 --- a/frontend-web/src/components/page-sidebar.tsx +++ b/frontend-web/src/components/page-sidebar.tsx @@ -22,14 +22,18 @@ import { NotificationDialog } from "@/components/workspace/notification-dialog"; import { SettingsDialog } from "@/components/workspace/settings"; import { useTheme } from "@/contexts/ThemeContext"; import { getStoredAuth } from "@/core/auth"; +import { isWujieEnv } from "@/core/embed/wujie"; import { useI18n } from "@/core/i18n/hooks"; import { useUnreadCount } from "@/core/notifications"; import { defaultExpandedIds, + getActiveThirdLevelItems, type MenuItem, } from "@/core/page-layout/sidebar-menu"; import { useSidebarMenuItems } from "@/core/page-layout/use-sidebar-menu"; +import "@/components/page-sdebar.css"; + type PageSidebarProps = { collapsed: boolean; onToggleCollapsed: () => void; @@ -37,7 +41,201 @@ type PageSidebarProps = { type SettingsSection = "account" | "appearance" | "memory" | "tools" | "about"; -export function PageSidebar({ +const hasActiveChild = (item: MenuItem, pathname: string): boolean => { + if (item.path && pathname === item.path) { + return true; + } + return item.children?.some((child) => hasActiveChild(child, pathname)) ?? false; +}; + +const findFirstRoutePath = (items?: MenuItem[]): string | null => { + if (!items?.length) { + return null; + } + for (const child of items) { + if (child.path) { + return child.path; + } + const nested = findFirstRoutePath(child.children); + if (nested) { + return nested; + } + } + return null; +}; + +/** + * 浅色侧边栏入口。 + * + * - 普通桌面环境:渲染「二级导航图标卡片」(一级在顶栏、二级在侧边栏,参考 导航-1.png)。 + * - wujie 嵌入环境:顶栏被宿主隐藏,一级导航无处安放,故回落到原手风琴整树菜单 + * (保证嵌入模式下所有层级仍可达,零回归)。 + */ +export function PageSidebar({ collapsed, onToggleCollapsed }: PageSidebarProps) { + if (isWujieEnv()) { + return ( + + ); + } + return ; +} + +/** + * 浅色二级导航侧边栏(图标卡片,红/蓝主题色经 --brand-* 变量自动切换)。 + * 浅色模式不支持侧边栏收起,故无收起按钮、不接收 collapsed 相关 props。 + */ +function SecondaryNavSidebar() { + const isAdmin = getStoredAuth()?.system_role === "admin"; + const visibleMenuItems = useSidebarMenuItems(!!isAdmin); + const { t } = useI18n(); + const { data: unreadCount = 0 } = useUnreadCount(); + const [settingsOpen, setSettingsOpen] = useState(false); + const [settingsDefaultSection, setSettingsDefaultSection] = + useState("appearance"); + const [notificationOpen, setNotificationOpen] = useState(false); + const [mounted, setMounted] = useState(false); + const navigate = useNavigate(); + const { pathname } = useLocation(); + + useEffect(() => { + setMounted(true); + }, []); + + const openSettings = (section: SettingsSection) => { + setSettingsDefaultSection(section); + setSettingsOpen(true); + }; + + // 当前激活的一级菜单(与顶栏一级导航联动);其 children 即侧边栏要渲染的二级导航。 + const activeTopItem = + visibleMenuItems.find((item) => hasActiveChild(item, pathname)) ?? + visibleMenuItems[0]; + const secondaryItems = activeTopItem?.children ?? []; + + const isItemActive = (item: MenuItem) => + item.path ? pathname === item.path : hasActiveChild(item, pathname); + + const handleItemClick = (item: MenuItem) => { + const path = item.path ?? findFirstRoutePath(item.children); + if (path) { + navigate(path); + } + }; + + // 当前激活二级项的三级子项;非空时侧边栏在右侧展开「三级列」并整体变宽(见 css 过渡)。 + const thirdItems = getActiveThirdLevelItems(visibleMenuItems, pathname); + const expanded = thirdItems.length > 0; + + return ( + <> + + + + > + ); +} + +/** + * 旧版浅色整树手风琴侧边栏。仅用于 wujie 嵌入环境(顶栏被宿主隐藏时保证所有层级可达)。 + */ +function LegacyAccordionSidebar({ collapsed, onToggleCollapsed, }: PageSidebarProps) { @@ -66,9 +264,6 @@ export function PageSidebar({ setSettingsOpen(true); }; - const iconButtonClass = isDark - ? "rounded-lg p-2 text-slate-400 transition-all duration-300 ease-out hover:scale-105 hover:bg-slate-800/50 hover:text-white" - : "rounded-lg p-2 text-gray-500 transition-all duration-300 ease-out hover:scale-105 hover:bg-red-50 hover:text-red-600"; const panelClass = isDark ? "ml-4 space-y-1 border-l border-blue-800/30 pl-4" : "ml-4 space-y-1 border-l border-red-100 pl-4"; diff --git a/frontend-web/src/components/workspace/artifacts/artifact-file-detail.tsx b/frontend-web/src/components/workspace/artifacts/artifact-file-detail.tsx index d8e4cfdd..5188d8d5 100644 --- a/frontend-web/src/components/workspace/artifacts/artifact-file-detail.tsx +++ b/frontend-web/src/components/workspace/artifacts/artifact-file-detail.tsx @@ -121,12 +121,19 @@ export function ArtifactFileDetail({ filepath: filepathFromProps, threadId, onClose, + allowVirtualMarkdownEdit = false, }: { className?: string; filepath: string; threadId: string; /** 覆盖默认关闭行为(仅 setOpen(false));圆桌 Step3 文件预览用于退出沙箱模式。 */ onClose?: () => void; + /** + * 圆桌沙箱(Step2/Step3)用:流式结束后,把 `write-file:` 虚拟 md 预览切回真实 + * artifacts API(可加载 + PATCH 保存),从而支持用户编辑(对齐主聊天页 md 可编辑)。 + * 默认 false —— 主聊天页等其它调用方行为不变。 + */ + allowVirtualMarkdownEdit?: boolean; }) { const { t } = useI18n(); const { artifacts, setOpen, select } = useArtifacts(); @@ -141,10 +148,6 @@ export function ArtifactFileDetail({ } return filepathFromProps; }, [filepathFromProps, isWriteFileVirtual]); - /** write_file 流式预览走 tool args 文本;图片等二进制产物始终走 artifacts API。 */ - const useWriteFileStreaming = useMemo(() => { - return isWriteFileVirtual && !isImageFile(filepath); - }, [filepath, isWriteFileVirtual]); const isSkillFile = useMemo(() => { return filepath.endsWith(".skill"); }, [filepath]); @@ -153,6 +156,24 @@ export function ArtifactFileDetail({ const isSpreadsheetFile = useMemo(() => { return /\.xlsx$/i.test(filepath); }, [filepath]); + // 圆桌沙箱:write_file 虚拟预览的 md 在流式结束(thread 非 loading)后,切回真实 + // artifacts API 加载并允许编辑/保存。流式进行中仍走内存 stub 只读预览;仅 + // allowVirtualMarkdownEdit 开启的调用方(Step2/Step3 沙箱)生效,主聊天页不受影响。 + const editVirtualMarkdown = useMemo(() => { + return ( + allowVirtualMarkdownEdit && + isWriteFileVirtual && + !isImageFile(filepath) && + !isMock && + !thread.isLoading && + checkCodeFile(filepath).language === "markdown" + ); + }, [allowVirtualMarkdownEdit, isWriteFileVirtual, filepath, isMock, thread.isLoading]); + /** write_file 流式预览走 tool args 文本;图片等二进制产物始终走 artifacts API; + * 圆桌结束后可编辑的 md(editVirtualMarkdown)亦回到真实 API(可加载 + PATCH 保存)。 */ + const useWriteFileStreaming = useMemo(() => { + return isWriteFileVirtual && !isImageFile(filepath) && !editVirtualMarkdown; + }, [filepath, isWriteFileVirtual, editVirtualMarkdown]); const { isCodeFile, language } = useMemo(() => { if (useWriteFileStreaming) { let nextLanguage = checkCodeFile(filepath).language; @@ -173,7 +194,10 @@ export function ArtifactFileDetail({ }, [language]); const { content, isApplyingEdit } = useArtifactContent({ threadId, - filepath: filepathFromProps, + // editVirtualMarkdown:用解码后的真实路径走 artifacts API(而非 write-file 虚拟 + // URL),这样保存后 useUpdateArtifactContent 写入的查询缓存才能命中,编辑内容 + // 不会被旧的内存 stub 值覆盖回退。 + filepath: editVirtualMarkdown ? filepath : filepathFromProps, enabled: isCodeFile && !useWriteFileStreaming, }); diff --git a/frontend-web/src/components/workspace/settings/appearance-settings-page.tsx b/frontend-web/src/components/workspace/settings/appearance-settings-page.tsx index 83c984c5..ae309232 100644 --- a/frontend-web/src/components/workspace/settings/appearance-settings-page.tsx +++ b/frontend-web/src/components/workspace/settings/appearance-settings-page.tsx @@ -26,6 +26,7 @@ import { useLocalSettings } from "@/core/settings"; import { useAppearanceSettings, useCitationDisplaySettings, + useLayoutMode, useUpdateAppearanceSettings, useUpdateCitationDisplaySettings, } from "@/core/system-settings/hooks"; @@ -57,6 +58,9 @@ export function AppearanceSettingsPage({ onClose }: { onClose?: () => void }) { const globalDefault = appearance?.default_theme ?? "light"; const globalSkillView = appearance?.default_skill_view ?? "list"; const globalAgentView = appearance?.default_agent_view ?? "card"; + const globalLayoutMode = appearance?.default_layout_mode ?? "full"; + // 有效界面模式:用户自己选过则以其为准,否则跟随管理员系统默认(再否则会话级回退)。 + const [effectiveLayoutMode, setLayoutMode] = useLayoutMode(); const cleanupWords = citationDisplay?.cleanup_words ?? settings.display.citationCleanupWords ?? []; @@ -74,6 +78,18 @@ export function AppearanceSettingsPage({ onClose }: { onClose?: () => void }) { } }; + const handleSetDefaultLayoutMode = async (value: "full" | "compact") => { + if (value === globalLayoutMode) return; + try { + await updateAppearance.mutateAsync({ default_layout_mode: value }); + toast.success( + `已将系统默认界面模式设置为${value === "full" ? "全量模式" : "简洁模式"}`, + ); + } catch (e) { + toast.error(e instanceof Error ? e.message : "保存失败"); + } + }; + const handleSetDefaultView = async ( kind: "skill" | "agent", value: "card" | "list", @@ -156,10 +172,10 @@ export function AppearanceSettingsPage({ onClose }: { onClose?: () => void }) { { - setSettings("display", { layoutMode: "full" }); + setLayoutMode("full"); onClose?.(); navigate("/page/workspace/chats/new"); }} @@ -169,10 +185,10 @@ export function AppearanceSettingsPage({ onClose }: { onClose?: () => void }) { { - setSettings("display", { layoutMode: "compact" }); + setLayoutMode("compact"); onClose?.(); navigate("/page/workspace/chats/new"); }} @@ -183,6 +199,43 @@ export function AppearanceSettingsPage({ onClose }: { onClose?: () => void }) { + {isAdmin && ( + <> + + + + void handleSetDefaultLayoutMode("full")} + disabled={updateAppearance.isPending} + > + + 全量模式 + + void handleSetDefaultLayoutMode("compact")} + disabled={updateAppearance.isPending} + > + + 简洁模式 + + + 当前系统默认: + {globalLayoutMode === "full" ? "全量模式" : "简洁模式"} + + + + > + )} + {isAdmin && ( <> diff --git a/frontend-web/src/core/artifacts/hooks.ts b/frontend-web/src/core/artifacts/hooks.ts index 45419ff3..0e9818ac 100644 --- a/frontend-web/src/core/artifacts/hooks.ts +++ b/frontend-web/src/core/artifacts/hooks.ts @@ -133,8 +133,18 @@ export function useUpdateArtifactContent() { return useMutation({ mutationFn: updateArtifactContent, onSuccess: (content, variables) => { - queryClient.setQueryData( - ["artifact", variables.filepath, variables.threadId, variables.isMock], + // useArtifactContent 的查询键带第 5 个元素 publicShareCode,这里用前缀 + // 过滤(setQueriesData)匹配,避免精确键缺少 publicShareCode 写到幽灵缓存、 + // 导致保存后内容被旧缓存重置回原值。 + queryClient.setQueriesData( + { + queryKey: [ + "artifact", + variables.filepath, + variables.threadId, + variables.isMock, + ], + }, { content, url: undefined, diff --git a/frontend-web/src/core/auth/yuser-login.ts b/frontend-web/src/core/auth/yuser-login.ts index 27e2419c..f118d245 100644 --- a/frontend-web/src/core/auth/yuser-login.ts +++ b/frontend-web/src/core/auth/yuser-login.ts @@ -31,15 +31,44 @@ export async function loginByYUserId( { yuserId: yUserId }, )) as UserDetailResponse; - const result = res.data; - if (!result?.id) { - throw new Error("无法根据 yUserId 获取用户信息。"); - } - - localStorage.setItem(EXTERNAL_USER_ID_STORAGE_KEY, result.id); - localStorage.setItem(EXTERNAL_USER_INFO_STORAGE_KEY, JSON.stringify(result)); + // consumer 返回 { accessToken, gateDepartName, typeName, userName }。 + // 一并存入路由参数 yUserId,使「应用管理」里 source=yUserId 的路由参数能取到(与 + // source=userName/gateDepartName/typeName 直接取接口值统一走 userInfo[source])。 + // 注意:接口也会返回 accessToken,但 XK/PG 及 source=accessToken 要用路由透传的 + // access_token,故登录流程随后会用 persistAccessToken 覆盖 userInfo.accessToken。 + const result = res.data ?? {}; + localStorage.setItem( + EXTERNAL_USER_INFO_STORAGE_KEY, + JSON.stringify({ ...result, yUserId }), + ); } export function persistToolsToken(token: string): void { localStorage.setItem(TOOLS_TOKEN_STORAGE_KEY, token); } + +/** + * 把内网登录路由参数 access_token 合并写入 userInfo.accessToken(覆盖接口返回的同名值)。 + * 据此拼参的有:XK/PG 中任务 iframe(XK 用 accessToken、PG 用 access_token,二者均读 + * external-links.getAccessToken() = userInfo.accessToken);以及「应用管理」里 + * source=accessToken 的轻应用路由参数(resolveRouteParamValue 取 userInfo.accessToken)。 + * 用合并而非覆盖整对象,避免抹掉 loginByYUserId 从 consumer 拿到的用户信息。 + */ +export function persistAccessToken(token: string): void { + const trimmed = token?.trim(); + if (!trimmed) return; + let info: Record = {}; + try { + const raw = localStorage.getItem(EXTERNAL_USER_INFO_STORAGE_KEY); + if (raw) { + const parsed = JSON.parse(raw); + if (parsed && typeof parsed === "object") { + info = parsed as Record; + } + } + } catch { + info = {}; + } + info.accessToken = trimmed; + localStorage.setItem(EXTERNAL_USER_INFO_STORAGE_KEY, JSON.stringify(info)); +} diff --git a/frontend-web/src/core/page-layout/sidebar-menu.ts b/frontend-web/src/core/page-layout/sidebar-menu.ts index 908e596a..7e3ccbf5 100644 --- a/frontend-web/src/core/page-layout/sidebar-menu.ts +++ b/frontend-web/src/core/page-layout/sidebar-menu.ts @@ -9,15 +9,16 @@ */ import type { ElementType } from "react"; import { + Activity, BookOpen, Bot, Brain, CalendarClock, CircleHelp, + ClipboardList, Database, FileBox, FileText, - Folder, Gauge, Grid3X3, HelpCircle, @@ -29,6 +30,7 @@ import { ListChecks, ListTodo, Network, + Radar, Settings, Sparkles, Target, @@ -77,7 +79,7 @@ export const sidebarMenuItems: MenuItem[] = [ { id: "xk-center-task", label: "XK中任务", - icon: Folder, + icon: ClipboardList, children: [ { id: "xk-task-jc", @@ -96,7 +98,7 @@ export const sidebarMenuItems: MenuItem[] = [ { id: "coh-center-task", label: "COH中任务", - icon: Folder, + icon: Activity, children: [ { id: "coh-task-3qfx", @@ -121,7 +123,7 @@ export const sidebarMenuItems: MenuItem[] = [ { id: "pg-center-task", label: "PG中任务", - icon: Folder, + icon: Gauge, children: [ { id: "pg-task-pg", @@ -146,7 +148,7 @@ export const sidebarMenuItems: MenuItem[] = [ { id: "ty-center-task", label: "TY中任务", - icon: Folder, + icon: Radar, children: [ { id: "ty-task-xdty", @@ -296,6 +298,31 @@ export const defaultExpandedIds: ReadonlySet = new Set([ "comprehensive-management", ]); +/** 该菜单项自身或任意后代的 path 是否等于当前路由 */ +export function menuItemContainsPath(item: MenuItem, pathname: string): boolean { + if (item.path && item.path === pathname) { + return true; + } + return item.children?.some((child) => menuItemContainsPath(child, pathname)) ?? false; +} + +/** + * 在「一级 → 二级 → 三级」菜单中,取当前路由所在的「激活二级项」的三级子项。 + * 浅色侧边栏据此在二级右侧展开三级列;无三级则返回空数组(侧边栏保持窄栏)。 + */ +export function getActiveThirdLevelItems( + items: MenuItem[], + pathname: string, +): MenuItem[] { + const activeTop = + items.find((item) => menuItemContainsPath(item, pathname)) ?? items[0]; + const secondary = activeTop?.children ?? []; + const activeSecond = secondary.find( + (item) => (item.children?.length ?? 0) > 0 && menuItemContainsPath(item, pathname), + ); + return activeSecond?.children ?? []; +} + /** 按 admin 角色过滤菜单:非 admin 不展示 ``adminOnly: true`` 的项及其子项 */ export function filterMenuItemsByRole(items: MenuItem[], isAdmin: boolean): MenuItem[] { return items diff --git a/frontend-web/src/core/page-layout/use-sidebar-menu.ts b/frontend-web/src/core/page-layout/use-sidebar-menu.ts index 1f2ec6b0..006c767e 100644 --- a/frontend-web/src/core/page-layout/use-sidebar-menu.ts +++ b/frontend-web/src/core/page-layout/use-sidebar-menu.ts @@ -8,12 +8,15 @@ * 浅色 PageSidebar 与深色 PageSidebarDark 共用此 hook,保证两套皮肤同步。 */ import { useMemo } from "react"; +import { useLocation } from "react-router-dom"; import { Layout } from "lucide-react"; +import { getStoredAuth } from "@/core/auth"; import { useLightApps, type LightApp } from "@/strategy-components/api/light-apps"; import { filterMenuItemsByRole, + getActiveThirdLevelItems, sidebarMenuItems, type MenuItem, } from "./sidebar-menu"; @@ -61,3 +64,14 @@ export function useSidebarMenuItems(isAdmin: boolean): MenuItem[] { [iframeApps, isAdmin], ); } + +/** + * 当前路由所在「激活二级项」的三级子项(浅色侧边栏右侧列用)。 + * 供 PageRoutes(主内容左内边距随展开自适应)与浅色侧边栏共用同一份判定逻辑。 + */ +export function useActiveThirdLevel(): MenuItem[] { + const isAdmin = getStoredAuth()?.system_role === "admin"; + const items = useSidebarMenuItems(!!isAdmin); + const { pathname } = useLocation(); + return useMemo(() => getActiveThirdLevelItems(items, pathname), [items, pathname]); +} diff --git a/frontend-web/src/core/roundtable-draft-shares/api.ts b/frontend-web/src/core/roundtable-draft-shares/api.ts index 544ef588..3535cf1c 100644 --- a/frontend-web/src/core/roundtable-draft-shares/api.ts +++ b/frontend-web/src/core/roundtable-draft-shares/api.ts @@ -37,11 +37,17 @@ export interface PublicDraftShareSnapshot { consensus_text: string; /** 各席位最终交付. */ seat_deliveries: PublicDraftSeatDelivery[]; - /** Self-contained Step 3 result report HTML (empty when no report exists). */ + /** Self-contained Step 3 result report HTML — 经典 HTML 看板 (empty when none). */ html: string; summary: string | null; generated_at: string | null; has_report: boolean; + /** 方案总结报告 (Markdown,roundtable-summary 产出). */ + summary_report_md: string; + has_summary_report: boolean; + /** 大屏多页 (模式二) 结构化数据 (report-json 字符串);前端按主题渲染成自包含 HTML。 */ + dashboard_json: string | null; + has_dashboard: boolean; created_at: string | null; } diff --git a/frontend-web/src/core/system-settings/api.ts b/frontend-web/src/core/system-settings/api.ts index 864801f1..20fd57f1 100644 --- a/frontend-web/src/core/system-settings/api.ts +++ b/frontend-web/src/core/system-settings/api.ts @@ -137,17 +137,20 @@ export async function updatePublishSquares( export type ThemeMode = "light" | "dark" | "lightblue"; export type ViewMode = "card" | "list"; +export type LayoutMode = "full" | "compact"; export interface AppearanceSettings { default_theme: ThemeMode; default_skill_view: ViewMode; default_agent_view: ViewMode; + default_layout_mode: LayoutMode; } export interface AppearanceUpdate { default_theme?: ThemeMode; default_skill_view?: ViewMode; default_agent_view?: ViewMode; + default_layout_mode?: LayoutMode; } export async function loadAppearanceSettings(): Promise { diff --git a/frontend-web/src/core/system-settings/hooks.ts b/frontend-web/src/core/system-settings/hooks.ts index 5ab38951..05bf91a0 100644 --- a/frontend-web/src/core/system-settings/hooks.ts +++ b/frontend-web/src/core/system-settings/hooks.ts @@ -21,6 +21,7 @@ import { type PrivateSquareUpdate, type PromptPrefixUpdate, type PublishSquaresUpdate, + type LayoutMode, type ViewMode, } from "./api"; @@ -143,6 +144,27 @@ export function useViewMode( return [mode, setMode]; } +/** + * Effective interface layout mode (full chrome vs compact). A user's own choice + * (persisted in local settings) wins; otherwise the admin-configured system + * default applies; otherwise `undefined` (callers fall back to the session-level + * `sidebarEnabled` flag set by portal / yUserId login). `setMode` persists the + * user's override locally. + */ +export function useLayoutMode(): [ + LayoutMode | undefined, + (mode: LayoutMode) => void, +] { + const [settings, setSettings] = useLocalSettings(); + const { data: appearance } = useAppearanceSettings(); + const userOverride = settings.display.layoutMode; + const adminDefault = appearance?.default_layout_mode; + const mode = userOverride ?? adminDefault; + const setMode = (next: LayoutMode) => + setSettings("display", { layoutMode: next }); + return [mode, setMode]; +} + export function useCitationDisplaySettings() { return useQuery({ queryKey: CITATION_DISPLAY_KEY, diff --git a/frontend-web/src/pages/ChatPage.tsx b/frontend-web/src/pages/ChatPage.tsx index e39c5f58..930dac06 100644 --- a/frontend-web/src/pages/ChatPage.tsx +++ b/frontend-web/src/pages/ChatPage.tsx @@ -42,10 +42,10 @@ import { getLastAgentId, saveLastAgentId, useKnowledgeRagEnabled, - useLocalSettings, useThreadSettings, } from "@/core/settings"; import { useThreadStream } from "@/core/threads/hooks"; +import { useLayoutMode } from "@/core/system-settings/hooks"; import { useRouteBase } from "@/core/route-base"; import { textOfMessage } from "@/core/threads/utils"; import { env } from "@/env"; @@ -96,7 +96,8 @@ export default function ChatPage() { const { open: historySidebarOpen, toggleSidebar: toggleHistorySidebar } = useSidebar(); const { t } = useI18n(); - const [localSettings] = useLocalSettings(); + // 有效界面模式:用户自选 > 管理员系统默认 > (下方再回退会话级 sidebarEnabled) + const [effectiveLayoutMode] = useLayoutMode(); const [showFollowups, setShowFollowups] = useState(false); const [hasStartedConversation, setHasStartedConversation] = useState(false); const { threadId, setThreadId, isNewThread, setIsNewThread, isMock } = @@ -418,7 +419,7 @@ export default function ChatPage() { // 优先看外观设置的「全量/简洁」,未设置过才回退到会话级 sidebarEnabled。否则用户显式 // 选「全量」时 PageRoutes 已挂出顶栏+侧栏,这里却还按旧 sidebarEnabled 判定,导致深蓝 // 主题底图(workspace-chat-bg)不生效。非紧凑(嵌入/standalone)保持原样。 - const layoutMode = localSettings.display.layoutMode; + const layoutMode = effectiveLayoutMode; const chromeByPreference = layoutMode === "full" ? true @@ -589,11 +590,6 @@ export default function ChatPage() { )} /> )} - {compactLanding && ( - - CM Assistant V1.2 · 深度推理可能会涉及数据同步 · 信息由AI自动生成,请审慎参考 - - )} {env.NEXT_PUBLIC_STATIC_WEBSITE_ONLY === "true" && ( {t.common.notAvailableInDemoMode} diff --git a/frontend-web/src/pages/LightAppListPage.tsx b/frontend-web/src/pages/LightAppListPage.tsx index b5145b83..43328404 100644 --- a/frontend-web/src/pages/LightAppListPage.tsx +++ b/frontend-web/src/pages/LightAppListPage.tsx @@ -37,6 +37,8 @@ import { cn } from "@/lib/utils"; import { useLightApps, type LightApp } from "@/strategy-components/api/light-apps"; import { buildLightAppUrl } from "@/strategy-components/lib/light-app"; +import "./light-app-pages.css"; + const PAGE_SIZE = 12; /** 卡片图标池(按 appId 哈希取一个,仅作视觉区分)。 */ @@ -107,7 +109,7 @@ export default function LightAppListPage() { }; return ( - + {/* 标题 */} diff --git a/frontend-web/src/pages/LightAppManagementPage.tsx b/frontend-web/src/pages/LightAppManagementPage.tsx index 99c6ed77..0180389d 100644 --- a/frontend-web/src/pages/LightAppManagementPage.tsx +++ b/frontend-web/src/pages/LightAppManagementPage.tsx @@ -1,3 +1,8 @@ +/** + * 应用管理页 —— 注册 / 编辑轻应用(iframe 与 window.open)。 + * + * 深蓝主题适配见 `./light-app-management.css`(《深蓝色 UI 规范》tokens.css)。 + */ import { useMemo, useState } from "react"; import { AlertCircle, @@ -43,6 +48,8 @@ import { stringifyThemeMap, } from "@/strategy-components/lib/light-app"; +import "./light-app-management.css"; + interface FormState { name: string; description: string; @@ -63,7 +70,10 @@ const PARAM_TYPE_OPTIONS: { label: string; value: "login" | "theme" }[] = [ { label: "主题参数", value: "theme" }, ]; -const DIALOG_SELECT_POPUP = { attach: () => document.body }; +const DIALOG_SELECT_POPUP = { + attach: () => document.body, + overlayClassName: "lam-select-popup", +}; const emptyForm = (): FormState => ({ name: "", @@ -213,14 +223,14 @@ export default function LightAppManagementPage() { const tableApps = useMemo(() => apps, [apps]); return ( - + - + 应用管理 - + 注册轻应用。 window.open @@ -235,14 +245,14 @@ export default function LightAppManagementPage() { 注册应用 - + setSearch(e.target.value)} placeholder="搜索应用名称 / 描述..." - className="w-full rounded-[5px] border border-neutral-100 bg-neutral-50/60 py-2 pl-10 pr-10 text-xs text-neutral-800 placeholder-neutral-400 transition-all hover:bg-white focus:border-[#F5222D]/40 focus:outline-none focus:ring-4 focus:ring-[#F5222D]/10" + className="lam-search-input w-full rounded-[5px] border border-neutral-100 bg-neutral-50/60 py-2 pl-10 pr-10 text-xs text-neutral-800 placeholder-neutral-400 transition-all hover:bg-white focus:border-[#F5222D]/40 focus:outline-none focus:ring-4 focus:ring-[#F5222D]/10" /> {search && ( - + setViewMode("table")} - className={`cursor-pointer rounded-[5px] px-4 py-1.5 text-xs font-bold transition-all ${ + className={`lam-view-toggle-btn cursor-pointer rounded-[5px] px-4 py-1.5 text-xs font-bold transition-all ${ viewMode === "table" - ? "bg-white text-[#F5222D] shadow-sm" + ? "is-active bg-white text-[#F5222D] shadow-sm" : "text-neutral-500 hover:text-neutral-700" }`} > @@ -275,9 +285,9 @@ export default function LightAppManagementPage() { setViewMode("cards")} - className={`cursor-pointer rounded-[5px] px-4 py-1.5 text-xs font-bold transition-all ${ + className={`lam-view-toggle-btn cursor-pointer rounded-[5px] px-4 py-1.5 text-xs font-bold transition-all ${ viewMode === "cards" - ? "bg-white text-[#F5222D] shadow-sm" + ? "is-active bg-white text-[#F5222D] shadow-sm" : "text-neutral-500 hover:text-neutral-700" }`} > @@ -287,14 +297,14 @@ export default function LightAppManagementPage() { - - 搜寻到 {tableApps.length}{" "} + + 搜寻到 {tableApps.length}{" "} 个注册完成的子模块轻应用 {viewMode === "table" ? ( - - + + 名称 @@ -310,19 +320,19 @@ export default function LightAppManagementPage() { {tableApps.map((app) => ( - + {app.name} {app.appType === "window_open" ? ( - + 新页跳转 ) : ( - + iframe 嵌套 @@ -330,7 +340,7 @@ export default function LightAppManagementPage() { - + handleCopyUrl(app.url)} - className="shrink-0 rounded-[5px] p-1 text-neutral-400 transition-colors hover:bg-neutral-100 hover:text-neutral-600" + className="lam-icon-btn shrink-0 rounded-[5px] p-1 text-neutral-400 transition-colors hover:bg-neutral-100 hover:text-neutral-600" title="复制网址" > @@ -352,8 +362,8 @@ export default function LightAppManagementPage() { {app.appType === "iframe" && app.mountLocation ? ( - - + + {mountLabel(app.mountLocation)} ) : ( @@ -370,7 +380,7 @@ export default function LightAppManagementPage() { activeParamPopover === app.appId ? null : app.appId ) } - className="inline-flex cursor-pointer items-center space-x-1.5 rounded-[5px] border border-neutral-200/60 bg-neutral-50/60 px-2.5 py-1 text-xs font-medium text-neutral-500 transition-colors hover:bg-neutral-100" + className="lam-param-btn inline-flex cursor-pointer items-center space-x-1.5 rounded-[5px] border border-neutral-200/60 bg-neutral-50/60 px-2.5 py-1 text-xs font-medium text-neutral-500 transition-colors hover:bg-neutral-100" > {app.routeParams.length}个 {activeParamPopover === app.appId ? ( @@ -389,14 +399,14 @@ export default function LightAppManagementPage() { className="fixed inset-0 z-40" onClick={() => setActiveParamPopover(null)} /> - - + + 注入的路由参数 setActiveParamPopover(null)} - className="text-[10px] font-semibold text-rose-700 hover:underline" + className="lam-param-popover-close text-[10px] font-semibold text-rose-700 hover:underline" > 关闭 @@ -405,11 +415,11 @@ export default function LightAppManagementPage() { {app.routeParams.map((p, idx) => ( - {p.key} - + {p.key} + {getParamSourceLabel(p)} @@ -427,15 +437,15 @@ export default function LightAppManagementPage() { {app.enabled ? ( <> - - + + - 已启用 + 已启用 > ) : ( <> - - 已停用 + + 已停用 > )} @@ -445,14 +455,14 @@ export default function LightAppManagementPage() { openEdit(app)} - className="cursor-pointer rounded-[5px] p-1.5 text-neutral-500 transition-colors hover:bg-neutral-100 hover:text-emerald-600" + className="lam-icon-btn cursor-pointer rounded-[5px] p-1.5 text-neutral-500 transition-colors hover:bg-neutral-100 hover:text-emerald-600" title="编辑配置" > handleDelete(app)} - className="cursor-pointer rounded-[5px] p-1.5 text-neutral-400 transition-colors hover:bg-rose-50 hover:text-rose-600" + className="lam-icon-btn lam-icon-btn-danger cursor-pointer rounded-[5px] p-1.5 text-neutral-400 transition-colors hover:bg-rose-50 hover:text-rose-600" title="删除配置" > @@ -466,15 +476,15 @@ export default function LightAppManagementPage() { - + - + {isLoading ? "加载中..." : "暂无匹配的组件应用"} {!isLoading && ( - + {search ? "请确认搜索关键词输入是否正确" : "点击右上角“注册应用”添加第一款轻应用"} )} @@ -487,15 +497,15 @@ export default function LightAppManagementPage() { ) : tableApps.length === 0 ? ( - - + + - + {isLoading ? "加载中..." : "暂无匹配的组件应用"} {!isLoading && ( - + {search ? "请确认搜索关键词输入是否正确" : "点击右上角“注册应用”添加第一款轻应用"} )} @@ -505,7 +515,7 @@ export default function LightAppManagementPage() { {tableApps.map((app) => ( {app.appType === "window_open" ? ( - + 新页跳转 ) : ( - + iframe 嵌套 )} - + {app.name} {app.enabled ? ( - - + + 已启用 ) : ( - - + + 已停用 )} - + {app.description || "暂无应用详细描述说明。"} - + {app.url} @@ -568,7 +578,7 @@ export default function LightAppManagementPage() { {app.routeParams.map((p, idx) => ( {p.key} @@ -577,24 +587,24 @@ export default function LightAppManagementPage() { )} {app.appType === "iframe" && app.mountLocation && ( - + 挂载分类: {mountLabel(app.mountLocation)} )} - + openEdit(app)} - className="cursor-pointer rounded-[5px] p-1.5 text-neutral-500 transition-colors hover:bg-neutral-100 hover:text-emerald-600" + className="lam-icon-btn cursor-pointer rounded-[5px] p-1.5 text-neutral-500 transition-colors hover:bg-neutral-100 hover:text-emerald-600" title="编辑配置" > handleDelete(app)} - className="cursor-pointer rounded-[5px] p-1.5 text-neutral-400 transition-colors hover:bg-rose-50 hover:text-rose-600" + className="lam-icon-btn lam-icon-btn-danger cursor-pointer rounded-[5px] p-1.5 text-neutral-400 transition-colors hover:bg-rose-50 hover:text-rose-600" title="删除配置" > @@ -607,21 +617,21 @@ export default function LightAppManagementPage() { {viewMode === "table" && ( - - + + 智能多维挂载规则: - + - • + • 当您注册 iframe 嵌套{" "} 应用时,您需要在菜单一栏指定分类(如“资源管理”),组件将直接作为新导航菜单项加入该分类。 - • + • 当路由挂载成功,点击左侧注入的子栏名称可在主区域调起{" "} 真实 Iframe 预览器 @@ -636,23 +646,23 @@ export default function LightAppManagementPage() { {dialogOpen && ( setDialogOpen(false)} /> - - + + - + - + {editing ? "修改轻应用" : "注册应用"} setDialogOpen(false)} - className="cursor-pointer rounded-[5px] p-1 text-[#A6A6A6] transition-colors hover:bg-[#FAFAFA] hover:text-[#F5222D]" + className="lam-dialog-close cursor-pointer rounded-[5px] p-1 text-[#A6A6A6] transition-colors hover:bg-[#FAFAFA] hover:text-[#F5222D]" > @@ -660,8 +670,8 @@ export default function LightAppManagementPage() { - - * + + * 应用类型 @@ -672,7 +682,7 @@ export default function LightAppManagementPage() { popupProps={DIALOG_SELECT_POPUP} onChange={(v) => patchForm({ appType: v as LightAppType })} /> - + {form.appType === "window_open" ? "在新标签页或卡片式路由容器打开。多用于外部高度复杂的系统跳转。" : "通过标准 iframe 嵌入页面,可直接挂载到左侧导航树相应分类内。"} @@ -680,8 +690,8 @@ export default function LightAppManagementPage() { - - * + + * 应用名称 @@ -689,13 +699,13 @@ export default function LightAppManagementPage() { value={form.name} onChange={(e) => patchForm({ name: e.target.value })} placeholder="如: 舆情看板" - className="h-9 rounded-[5px] border-[#DBDBDB] text-[14px]" + className="lam-input h-9 rounded-[5px] border-[#DBDBDB] text-[14px]" /> - - * + + * 应用地址 @@ -703,14 +713,14 @@ export default function LightAppManagementPage() { value={form.url} onChange={(e) => patchForm({ url: e.target.value })} placeholder="https://example.com/page 或 http://47.88.25.99:3000/" - className="h-9 rounded-[5px] border-[#DBDBDB] font-mono text-[14px]" + className="lam-input h-9 rounded-[5px] border-[#DBDBDB] font-mono text-[14px]" /> {form.appType === "iframe" && ( - - * + + * 挂载菜单 @@ -726,7 +736,7 @@ export default function LightAppManagementPage() { )} - + 应用描述 (可选) @@ -736,13 +746,13 @@ export default function LightAppManagementPage() { onChange={(e) => patchForm({ description: e.target.value })} placeholder="简要说明该应用的用途" rows={2} - className="resize-none rounded-[5px] border-[#DBDBDB] text-[14px]" + className="lam-input resize-none rounded-[5px] border-[#DBDBDB] text-[14px]" /> - + 路由参数 添加路由参数 @@ -763,14 +773,14 @@ export default function LightAppManagementPage() { {form.routeParams.length > 0 ? ( - + {form.routeParams.map((p, idx) => { const themeMap = p.paramType === "theme" ? parseThemeMap(p.themeValue) : {}; return ( @@ -779,7 +789,7 @@ export default function LightAppManagementPage() { value={p.key} onChange={(e) => patchParam(idx, { key: e.target.value })} placeholder="参数名 key" - className="h-8 w-full rounded-[5px] border border-[#DBDBDB] bg-white px-2 font-mono text-[12px] focus:border-[#F5222D] focus:outline-none focus:ring-px focus:ring-[#F5222D]" + className="lam-input h-8 w-full rounded-[5px] border border-[#DBDBDB] bg-white px-2 font-mono text-[12px] focus:border-[#F5222D] focus:outline-none focus:ring-px focus:ring-[#F5222D]" /> @@ -817,7 +827,7 @@ export default function LightAppManagementPage() { patchParam(idx, { customValue: e.target.value }) } placeholder="自定义值" - className="h-8 w-[120px] rounded-[5px] border border-[#DBDBDB] px-2 text-[12px] focus:border-[#F5222D] focus:outline-none" + className="lam-input h-8 w-[120px] rounded-[5px] border border-[#DBDBDB] px-2 text-[12px] focus:border-[#F5222D] focus:outline-none" /> )} @@ -826,7 +836,7 @@ export default function LightAppManagementPage() { removeParam(idx)} - className="cursor-pointer rounded-[5px] p-1 text-[#A6A6A6] transition-colors hover:bg-[#FEEDED] hover:text-[#F5222D]" + className="lam-icon-btn lam-icon-btn-danger cursor-pointer rounded-[5px] p-1 text-[#A6A6A6] transition-colors hover:bg-[#FEEDED] hover:text-[#F5222D]" > @@ -834,8 +844,8 @@ export default function LightAppManagementPage() { {/* 主题参数:逐主题色配置传给目标应用的值(左=系统主题色,右=目标应用值,可改) */} {p.paramType === "theme" && ( - - + + 各系统主题色跳转/嵌套时传给该应用的值(默认同名,可按对方命名修改) {APP_THEME_COLORS.map((c) => ( @@ -862,7 +872,7 @@ export default function LightAppManagementPage() { }) } placeholder={c.key} - className="h-7 flex-1 rounded-[5px] border border-[#DBDBDB] bg-white px-2 font-mono text-[12px] focus:border-[#F5222D] focus:outline-none focus:ring-px focus:ring-[#F5222D]" + className="lam-input h-7 flex-1 rounded-[5px] border border-[#DBDBDB] bg-white px-2 font-mono text-[12px] focus:border-[#F5222D] focus:outline-none focus:ring-px focus:ring-[#F5222D]" /> ))} @@ -873,19 +883,19 @@ export default function LightAppManagementPage() { })} ) : ( - + 暂无注入的路由参数,加载地址时不传递多余拼接。 )} - - 立即启用该应用 + + 立即启用该应用 patchForm({ enabled: !form.enabled })} className={`relative h-[22px] w-10 cursor-pointer rounded-full p-0.5 transition-colors duration-200 ${ - form.enabled ? "bg-[#F5222D]" : "bg-[#DBDBDB]" + form.enabled ? "lam-switch-on bg-[#F5222D]" : "lam-switch-off bg-[#DBDBDB]" }`} > - + setDialogOpen(false)} - className="h-9 rounded-[5px] border-[#DBDBDB] px-[18px] text-[14px] font-semibold" + className="lam-dialog-cancel h-9 rounded-[5px] border-[#DBDBDB] px-[18px] text-[14px] font-semibold" > 取消 @@ -910,7 +920,7 @@ export default function LightAppManagementPage() { type="button" disabled={saving} onClick={handleSave} - className="h-9 cursor-pointer rounded-[5px] bg-[#F5222D] px-[18px] text-[14px] font-bold text-white shadow-[0_2px_8px_rgba(34,34,34,0.08)] transition-colors hover:bg-[#CF1322] active:bg-[#A8071A] disabled:cursor-not-allowed disabled:opacity-70" + className="lam-dialog-save h-9 cursor-pointer rounded-[5px] bg-[#F5222D] px-[18px] text-[14px] font-bold text-white shadow-[0_2px_8px_rgba(34,34,34,0.08)] transition-colors hover:bg-[#CF1322] active:bg-[#A8071A] disabled:cursor-not-allowed disabled:opacity-70" > {saving ? "保存中..." : editing ? "保存修改" : "立即注册"} diff --git a/frontend-web/src/pages/LoginPage.tsx b/frontend-web/src/pages/LoginPage.tsx index 1d19c45b..8f33b1da 100644 --- a/frontend-web/src/pages/LoginPage.tsx +++ b/frontend-web/src/pages/LoginPage.tsx @@ -16,6 +16,7 @@ import { isWujieEnv, useWujieProps } from '@/core/embed/wujie'; import { GENERAL_QA_ROUTE, loginByYUserId, + persistAccessToken, persistToolsToken, } from '@/core/auth/yuser-login'; import { @@ -53,6 +54,8 @@ export default function LoginPage() { // 外部跳转参数:theme 直接调 setTheme;ticket/userId/env 持久化到 localStorage const yUserId = searchParams.get('yUserId')?.trim() ?? ''; const toolToken = searchParams.get('token')?.trim() ?? ''; + // 内网登录透传的访问令牌:存入 userInfo.accessToken,供 XK/PG 中任务 iframe 拼参 + const accessToken = searchParams.get('access_token')?.trim() ?? ''; // 用户名直登口令门:开启后端开关时,口令从 ?password= 透传 const gatePassword = searchParams.get('password') ?? ''; // token 换登录:?authToken= 传上游 token(与 tools_token 的 ?token= 区分开) @@ -137,6 +140,9 @@ export default function LoginPage() { setStoredAuth(auth); // 标准登录把用户名写入 userInfo,供顶栏用户区显示(已有则不覆盖) setStoredUserName(loginUserName); + // 内网透传的 access_token 合并进 userInfo(在 loginByYUserId 覆盖之后), + // XK/PG 中任务 iframe 据此取 accessToken + persistAccessToken(accessToken); persistSidebarEnabled(sidebarEnabled); setStatus('success'); router.replace(yUserId ? GENERAL_QA_ROUTE : redirectPath); @@ -161,6 +167,7 @@ export default function LoginPage() { sidebarEnabled, yUserId, toolToken, + accessToken, onlineTicket, authToken, gatePassword, diff --git a/frontend-web/src/pages/PageRoutes.tsx b/frontend-web/src/pages/PageRoutes.tsx index 6ae9372d..bb849769 100644 --- a/frontend-web/src/pages/PageRoutes.tsx +++ b/frontend-web/src/pages/PageRoutes.tsx @@ -5,9 +5,11 @@ import { PageSidebar } from "@/components/page-sidebar"; import { PageSidebarDark } from "@/components/page-sidebar-dark"; import { isChatsIframeWorkspacePath } from "@/core/page-layout/chats-iframe-route"; import { readSidebarEnabled } from "@/core/page-layout/sidebar-enabled"; +import { isWujieEnv } from "@/core/embed/wujie"; import { useTheme } from "@/contexts/ThemeContext"; import { useLocation } from "react-router-dom"; -import { useLocalSettings } from "@/core/settings"; +import { useActiveThirdLevel } from "@/core/page-layout/use-sidebar-menu"; +import { useLayoutMode } from "@/core/system-settings/hooks"; import WorkspaceRoutes from "./WorkspaceRoutes"; import StrategyRoutes from "./StrategyRoutes"; @@ -22,11 +24,16 @@ function PageLayout() { const location = useLocation(); const { theme } = useTheme(); const isDark = theme === "dark"; - const [settings] = useLocalSettings(); - // 界面模式开关(外观设置):用户显式选「全量/简洁」时以其为准;未设置过则回退到 - // 会话级 sidebarEnabled(外部门户/yUserId 登录时置位)这一旧默认。这样无论是否走门户 - // 登录,「全量↔简洁」切换都能真正控制顶栏+侧边栏的显示。 - const layoutMode = settings.display.layoutMode; + // 浅色非嵌入:侧边栏为新版「二级导航图标卡片」窄栏(96px);wujie 嵌入仍走旧版手风琴(64/256px)。 + const inWujie = useMemo(() => isWujieEnv(), []); + const lightNarrowSidebar = !isDark && !inWujie; + // 浅色窄栏在右侧展开「三级列」时整体变宽(96→236px),主内容左内边距随之过渡。 + const thirdLevel = useActiveThirdLevel(); + const lightExpanded = lightNarrowSidebar && thirdLevel.length > 0; + // 界面模式(外观设置):用户显式选「全量/简洁」时以其为准;否则跟随管理员系统默认 + // (default_layout_mode);两者都没有才回退到会话级 sidebarEnabled(外部门户/yUserId + // 登录时置位)这一旧默认。这样无论是否走门户登录,「全量↔简洁」都能真正控制顶栏+侧边栏。 + const [layoutMode] = useLayoutMode(); const chromeByPreference = layoutMode === "full" ? true : layoutMode === "compact" ? false : sidebarEnabled; const pageSidebarAllowed = @@ -61,8 +68,11 @@ function PageLayout() { // 只剩左下角浮动按钮(fixed,叠在 main 上不影响布局)。 // 不加 transition:transition + sidebar 立刻 unmount 不同步会抽搐 ? (collapsed ? "pl-0" : "pl-[118px]") - // 浅色仍保留过渡动画(pl-16 ↔ pl-64 平滑收展) - : `transition-[padding] duration-300 ${collapsed ? "pl-16" : "pl-64"}` + : lightNarrowSidebar + // 浅色新版窄栏:展开三级列时 96→236px,主内容左内边距随之过渡 + ? `transition-[padding] duration-300 ${lightExpanded ? "pl-[236px]" : "pl-[96px]"}` + // 浅色旧版手风琴(wujie 嵌入):pl-16 ↔ pl-64 平滑收展 + : `transition-[padding] duration-300 ${collapsed ? "pl-16" : "pl-64"}` }`} > diff --git a/frontend-web/src/pages/PublicRoundtableSharePage.tsx b/frontend-web/src/pages/PublicRoundtableSharePage.tsx index c64a0d70..b533f298 100644 --- a/frontend-web/src/pages/PublicRoundtableSharePage.tsx +++ b/frontend-web/src/pages/PublicRoundtableSharePage.tsx @@ -8,9 +8,11 @@ import { ChevronRight, ExternalLink, FileText, + LayoutDashboard, Loader2, + ScrollText, } from "lucide-react"; -import { useState } from "react"; +import { useMemo, useState } from "react"; import ReactMarkdown from "react-markdown"; import { useParams } from "react-router-dom"; import remarkGfm from "remark-gfm"; @@ -21,6 +23,12 @@ import { } from "@/core/roundtable-draft-shares/api"; import { usePublicBackground } from "@/core/embed/background"; import { markdownComponents } from "@/roundtable-planning/components/markdown-components"; +import { DashboardApp } from "@/roundtable-planning/dashboard/DashboardApp"; +import { adaptReportData } from "@/roundtable-planning/dashboard/data-context"; +import { parseReportJson } from "@/roundtable-planning/lib/report-template"; + +/** 该分享页支持的三种视图:落地汇总 / 结果绘制(经典 HTML) / 大屏多页(模式二)。 */ +export type RoundtableShareView = "landing" | "report" | "dashboard"; /** 各席位最终交付 —— 折叠手风琴(仿 HighFidelityReport 第二区块)。 */ function SeatDeliveries({ seats }: { seats: PublicDraftSeatDelivery[] }) { @@ -81,20 +89,22 @@ function SeatDeliveries({ seats }: { seats: PublicDraftSeatDelivery[] }) { * Public, read-only roundtable-result viewer. * * Reachable at `#/roundtable-share/` with NO authentication — anyone with - * the link can open it. Two views, driven by the `report` prop / route: + * the link can open it. Three views, driven by the `view` prop / route: * * - landing (`#/roundtable-share/`): a read-only result page mirroring the - * in-app Step 3「最终方案展示」— 总控最终结论 + 各席位最终交付 + a 「查看结果绘制」 - * button that opens the full report in a NEW TAB (when one was generated). - * - report (`#/roundtable-share//report`): the Step 3 report (a + * in-app Step 3「最终方案展示」— 结果绘制/大屏多页入口 + 方案总结报告 + 总控最终 + * 结论 + 各席位最终交付. + * - report (`#/roundtable-share//report`): the Step 3 经典 HTML 看板 (a * self-contained HTML document) rendered full-screen inside a sandboxed - * `iframe` (no `allow-scripts`), so the agent-generated HTML can never execute - * code in the app origin. + * `iframe` (no `allow-scripts`). + * - dashboard (`#/roundtable-share//dashboard`): the Step 3 大屏多页 (模式二) + * — rendered on the fly from the stored structured data via `renderDashboardHtml` + * into the same sandboxed `iframe`. */ export default function PublicRoundtableSharePage({ - report = false, + view = "landing", }: { - report?: boolean; + view?: RoundtableShareView; }) { const { shareCode } = useParams<{ shareCode: string }>(); // 嵌套方可用 ?bg=/?text= 或 postMessage 覆盖背景/文字色。 @@ -108,6 +118,17 @@ export default function PublicRoundtableSharePage({ refetchOnWindowFocus: false, }); + // 大屏多页:用与应用内一致的 React 大屏(DashboardApp)渲染,而非自包含 HTML 模板。 + // 把存储的结构化数据(report-json)解析并 adapt 成 DashboardApp 的入参;与 + // Step3DashboardMode 的「应用渲染」、DashboardStandalonePage 的「新窗口打开」同源。 + const dashboardData = useMemo(() => { + const json = data?.dashboard_json; + if (!json) return undefined; + const parsed = parseReportJson(json); + if (!parsed) return undefined; + return adaptReportData(parsed, data?.generated_at ?? ""); + }, [data?.dashboard_json, data?.generated_at]); + if (isLoading) { return ( @@ -128,8 +149,24 @@ export default function PublicRoundtableSharePage({ ); } - // ── 报告页:整页沙箱 iframe 展示第 3 步结果绘制 ────────────────────────────── - if (report) { + // ── 大屏多页:整屏渲染应用内 React 大屏(DashboardApp),与应用渲染/新窗口打开一致 ── + if (view === "dashboard") { + return ( + + {dashboardData ? ( + + ) : ( + + 暂无大屏多页 + 这次会商还没有生成第三步「大屏多页」的方案报告。 + + )} + + ); + } + + // ── 结果绘制(经典 HTML 看板):整页沙箱 iframe 展示自包含报告 ───────────────── + if (view === "report") { return ( @@ -161,8 +198,10 @@ export default function PublicRoundtableSharePage({ ); } - // ── 落地页:总控最终结论 + 各席位最终交付 + 查看结果绘制(新标签打开报告页)───── - const reportUrl = `${window.location.origin}${window.location.pathname}#/roundtable-share/${shareCode}/report`; + // ── 落地页:结果绘制/大屏多页入口 + 方案总结报告 + 总控最终结论 + 各席位最终交付 ── + const shareBase = `${window.location.origin}${window.location.pathname}#/roundtable-share/${shareCode}`; + const reportUrl = `${shareBase}/report`; + const dashboardUrl = `${shareBase}/dashboard`; return ( @@ -182,23 +221,39 @@ export default function PublicRoundtableSharePage({ - {/* 1. 结果绘制 —— 新标签打开报告页 */} + {/* 1. 结果绘制 / 大屏多页 —— 新标签打开整页报告 */} 结果绘制 - {data.has_report ? ( - - - 查看结果绘制 - - + {data.has_report || data.has_dashboard ? ( + + {data.has_report && ( + + + 查看结果绘制 + + + )} + {data.has_dashboard && ( + + + 查看大屏多页 + + + )} + ) : ( 这次会商还没有生成第三步「结果绘制」的方案报告。 @@ -206,7 +261,22 @@ export default function PublicRoundtableSharePage({ )} - {/* 2. 总控最终结论 */} + {/* 2. 方案总结报告(Markdown) */} + {data.has_summary_report && ( + + + + 方案总结报告 + + + + {data.summary_report_md} + + + + )} + + {/* 3. 总控最终结论 */} @@ -223,7 +293,7 @@ export default function PublicRoundtableSharePage({ )} - {/* 3. 各席位最终交付 */} + {/* 4. 各席位最终交付 */} diff --git a/frontend-web/src/pages/light-app-management.css b/frontend-web/src/pages/light-app-management.css new file mode 100644 index 00000000..bc3eafc0 --- /dev/null +++ b/frontend-web/src/pages/light-app-management.css @@ -0,0 +1,570 @@ +@import "../../docs/ui规范-深蓝/tokens.css"; +@import "./light-app-pages.css"; + +/** + * 应用管理页 —— 深蓝色主题适配(《深蓝色 UI 规范》) + * 浅色样式仍由组件内 Tailwind 承担;.dark 下本文件覆盖为 ZS-004 / ZS-002 令牌。 + */ +.dark .light-app-management-page { + color: var(--color-text); +} + +/* ── 页头 / 文案 ─────────────────────────────────────────────────────── */ +.dark .light-app-management-page .lam-title { + color: var(--color-text-strong); +} + +.dark .light-app-management-page .lam-desc { + color: var(--color-text-secondary); +} + +.dark .light-app-management-page .lam-desc code { + background: rgba(22, 119, 255, 0.12); + color: var(--color-link); +} + +.dark .light-app-management-page .lam-primary-btn { + background: var(--color-primary); + box-shadow: none; +} + +.dark .light-app-management-page .lam-primary-btn:hover { + background: var(--color-primary-hover); +} + +.dark .light-app-management-page .lam-primary-btn:active { + background: var(--color-primary-active); +} + +/* ── 工具条 / 搜索 / 视图切换 ─────────────────────────────────────────── */ +.dark .light-app-management-page .lam-toolbar { + background: var(--color-bg-card); + border: 1px solid var(--color-border); + box-shadow: var(--shadow-1); +} + +.dark .light-app-management-page .lam-search-input { + background: var(--color-bg-page); + border-color: var(--color-border); + color: var(--color-text); +} + +.dark .light-app-management-page .lam-search-input::placeholder { + color: var(--color-text-disabled); +} + +.dark .light-app-management-page .lam-search-input:hover { + background: var(--color-bg-card); + border-color: var(--color-link); +} + +.dark .light-app-management-page .lam-search-input:focus { + background: var(--color-bg-card); + border-color: var(--color-primary); + box-shadow: 0 0 0 2px rgba(22, 119, 255, 0.16); + outline: none; +} + +.dark .light-app-management-page .lam-view-toggle { + background: var(--color-bg-page); + border-color: var(--color-border); +} + +.dark .light-app-management-page .lam-view-toggle-btn.is-active { + background: var(--color-bg-card); + color: var(--color-link); + box-shadow: var(--shadow-1); +} + +.dark .light-app-management-page .lam-view-toggle-btn:not(.is-active) { + color: var(--color-text-secondary); +} + +.dark .light-app-management-page .lam-view-toggle-btn:not(.is-active):hover { + color: var(--color-text); +} + +.dark .light-app-management-page .lam-count { + color: var(--color-text-secondary); +} + +.dark .light-app-management-page .lam-count strong { + color: var(--color-link); +} + +/* ── 表格(§28)──────────────────────────────────────────────────────── */ +.dark .light-app-management-page .lam-table-wrap { + background: var(--color-bg-page); + border: 1px solid var(--color-border); + box-shadow: var(--shadow-1); +} + +.dark .light-app-management-page .lam-table thead tr { + background: var(--color-bg-card); + border-color: var(--color-border); + color: var(--color-text); +} + +.dark .light-app-management-page .lam-table tbody { + border-color: var(--color-border); +} + +.dark .light-app-management-page .lam-table tbody tr { + border-color: var(--color-border); +} + +.dark .light-app-management-page .lam-table tbody tr:hover { + background: rgba(22, 119, 255, 0.08); +} + +.dark .light-app-management-page .lam-row-name { + color: var(--color-text); +} + +.dark .light-app-management-page .lam-table tbody tr:hover .lam-row-name { + color: var(--color-link); +} + +.dark .light-app-management-page .lam-tag-window { + border-color: rgba(245, 34, 45, 0.35); + background: var(--func-error-bg); + color: #ff7875; +} + +.dark .light-app-management-page .lam-tag-iframe { + border-color: rgba(22, 119, 255, 0.35); + background: var(--func-info-bg); + color: var(--color-link); +} + +.dark .light-app-management-page .lam-url { + color: var(--color-text-secondary); +} + +.dark .light-app-management-page .lam-table tbody tr:hover .lam-url { + color: var(--color-text); +} + +.dark .light-app-management-page .lam-url a { + color: inherit; +} + +.dark .light-app-management-page .lam-url a:hover { + color: var(--color-link); +} + +.dark .light-app-management-page .lam-mount-tag { + background: var(--func-info-bg); + color: var(--color-link); +} + +.dark .light-app-management-page .lam-mount-dot { + background: var(--color-primary); +} + +.dark .light-app-management-page .lam-param-btn { + border-color: var(--color-border); + background: var(--color-bg-page); + color: var(--color-text-secondary); +} + +.dark .light-app-management-page .lam-param-btn:hover { + background: rgba(22, 119, 255, 0.08); + color: var(--color-text); +} + +.dark .light-app-management-page .lam-param-popover { + border-color: var(--color-border); + background: var(--color-bg-card); + box-shadow: var(--shadow-3); +} + +.dark .light-app-management-page .lam-param-popover-head { + border-color: var(--color-border); + color: var(--color-text-secondary); +} + +.dark .light-app-management-page .lam-param-popover-close { + color: var(--color-link); +} + +.dark .light-app-management-page .lam-param-item { + border-color: var(--color-border); + background: var(--color-bg-page); +} + +.dark .light-app-management-page .lam-param-key { + color: #ff7875; +} + +.dark .light-app-management-page .lam-param-val { + color: var(--color-text-secondary); +} + +.dark .light-app-management-page .lam-status-on { + color: var(--func-success); +} + +.dark .light-app-management-page .lam-status-on-dot { + background: var(--func-success); +} + +.dark .light-app-management-page .lam-status-on-ping { + background: var(--func-success); +} + +.dark .light-app-management-page .lam-status-off { + color: var(--color-text-disabled); +} + +.dark .light-app-management-page .lam-status-off-dot { + background: var(--color-text-disabled); +} + +.dark .light-app-management-page .lam-icon-btn { + color: var(--color-text-secondary); +} + +.dark .light-app-management-page .lam-icon-btn:hover { + background: rgba(22, 119, 255, 0.08); + color: var(--color-link); +} + +.dark .light-app-management-page .lam-icon-btn-danger:hover { + background: var(--func-error-bg); + color: var(--func-error); +} + +.dark .light-app-management-page .lam-empty-icon { + background: var(--color-bg-page); + border: 1px solid var(--color-border); + color: var(--color-text-disabled); +} + +.dark .light-app-management-page .lam-empty-title { + color: var(--color-text-secondary); +} + +.dark .light-app-management-page .lam-empty-sub { + color: var(--color-text-disabled); +} + +/* ── 卡片视图(§22 / §43)────────────────────────────────────────────── */ +.dark .light-app-management-page .lam-card { + background: var(--color-bg-card); + border: 1px solid var(--color-border); + box-shadow: var(--shadow-1); +} + +.dark .light-app-management-page .lam-card:hover { + box-shadow: var(--shadow-2); +} + +.dark .light-app-management-page .lam-card-title { + color: var(--color-text-strong); +} + +.dark .light-app-management-page .lam-card:hover .lam-card-title { + color: var(--color-link); +} + +.dark .light-app-management-page .lam-card-desc { + color: var(--color-text-secondary); +} + +.dark .light-app-management-page .lam-card-url-box { + background: var(--color-bg-page); +} + +.dark .light-app-management-page .lam-card-param { + border-color: var(--color-border); + background: var(--color-bg-page); + color: var(--color-text-secondary); +} + +.dark .light-app-management-page .lam-card-mount { + background: var(--func-info-bg); + color: var(--color-link); +} + +.dark .light-app-management-page .lam-card-footer { + border-color: var(--color-border); + background: rgba(12, 24, 54, 0.5); +} + +/* ── 底部提示(§33 警告)────────────────────────────────────────────── */ +.dark .light-app-management-page .lam-tip-box { + background: var(--func-warning-bg); + border: 1px solid rgba(250, 173, 20, 0.25); + box-shadow: none; +} + +.dark .light-app-management-page .lam-tip-head { + border-color: rgba(250, 173, 20, 0.2); + color: var(--func-warning); +} + +.dark .light-app-management-page .lam-tip-list { + color: var(--color-text-secondary); +} + +.dark .light-app-management-page .lam-tip-list strong { + color: var(--color-text); +} + +.dark .light-app-management-page .lam-tip-bullet { + color: var(--func-warning); +} + +/* ── 弹窗(§21 Modal)────────────────────────────────────────────────── */ +.dark .light-app-management-page .lam-dialog-overlay { + background: rgba(6, 14, 36, 0.6); +} + +.dark .light-app-management-page .lam-dialog { + border-color: var(--color-border); + background: linear-gradient(180deg, var(--color-bg-card), var(--color-bg-page)); + box-shadow: var(--shadow-3); + color: var(--color-text); +} + +.dark .light-app-management-page .lam-dialog-header { + border-color: var(--color-border); + border-bottom: none; + padding-bottom: 0; +} + +.dark .light-app-management-page .lam-dialog-header::after { + content: ""; + display: block; + height: 1px; + margin-top: 16px; + margin-bottom: 0; + background: var(--gradient-title-line); +} + +.dark .light-app-management-page .lam-dialog-icon { + background: var(--func-info-bg); + color: var(--color-link); +} + +.dark .light-app-management-page .lam-dialog-title { + color: var(--color-text-strong); +} + +.dark .light-app-management-page .lam-dialog-close { + color: var(--color-text-secondary); +} + +.dark .light-app-management-page .lam-dialog-close:hover { + background: rgba(22, 119, 255, 0.08); + color: var(--color-link); +} + +.dark .light-app-management-page .lam-form-label { + color: var(--color-text); +} + +.dark .light-app-management-page .lam-form-label .lam-required { + color: var(--func-error); +} + +.dark .light-app-management-page .lam-form-label svg { + color: var(--color-text-secondary); +} + +.dark .light-app-management-page .lam-form-hint { + color: var(--color-text-secondary); +} + +.dark .light-app-management-page .lam-dialog .lam-input, +.dark .light-app-management-page .lam-dialog input[type="text"] { + background: var(--color-bg-page); + border-color: var(--color-border); + color: var(--color-text); +} + +.dark .light-app-management-page .lam-dialog .lam-input::placeholder, +.dark .light-app-management-page .lam-dialog input[type="text"]::placeholder { + color: var(--color-text-disabled); +} + +.dark .light-app-management-page .lam-dialog .lam-input:focus, +.dark .light-app-management-page .lam-dialog input[type="text"]:focus { + border-color: var(--color-primary); + box-shadow: 0 0 0 2px rgba(22, 119, 255, 0.16); + outline: none; +} + +.dark .light-app-management-page .lam-param-panel { + border-color: var(--color-border); + background: var(--color-bg-page); +} + +.dark .light-app-management-page .lam-param-row { + border-color: var(--color-border); + background: var(--color-bg-card); +} + +.dark .light-app-management-page .lam-param-theme-box { + background: var(--color-bg-page); +} + +.dark .light-app-management-page .lam-param-empty { + border-color: var(--color-border); + background: var(--color-bg-page); + color: var(--color-text-secondary); +} + +.dark .light-app-management-page .lam-add-param-btn { + border-color: var(--color-border-strong); + background: var(--color-bg-card); + color: var(--color-text); +} + +.dark .light-app-management-page .lam-add-param-btn:hover { + border-color: var(--color-primary); + color: var(--color-link); +} + +.dark .light-app-management-page .lam-dialog-footer { + border-color: var(--color-border); +} + +.dark .light-app-management-page .lam-dialog-cancel { + border-color: var(--color-border-strong); + background: transparent; + color: var(--color-text); +} + +.dark .light-app-management-page .lam-dialog-cancel:hover { + border-color: var(--color-link); + color: var(--color-link); +} + +.dark .light-app-management-page .lam-dialog-save { + background: var(--color-primary); + box-shadow: none; +} + +.dark .light-app-management-page .lam-dialog-save:hover { + background: var(--color-primary-hover); +} + +.dark .light-app-management-page .lam-dialog-save:active { + background: var(--color-primary-active); +} + +.dark .light-app-management-page .lam-switch-on { + background: var(--color-primary); +} + +.dark .light-app-management-page .lam-switch-off { + background: var(--color-text-disabled); +} + +/* ── TDesign Select(§15)──────────────────────────────────────────── */ +.dark .light-app-management-page .t-select .t-input, +.dark .light-app-management-page .t-select .t-input__wrap { + background: var(--color-bg-page); + border-color: var(--color-border); + border-radius: var(--radius-input); +} + +.dark .light-app-management-page .t-select .t-input__inner { + color: var(--color-text); +} + +.dark .light-app-management-page .t-select .t-input__inner::placeholder { + color: var(--color-text-disabled); +} + +.dark .light-app-management-page .t-select:hover .t-input, +.dark .light-app-management-page .t-select:hover .t-input__wrap { + border-color: var(--color-link); +} + +.dark .light-app-management-page .t-select.t-is-focused .t-input, +.dark .light-app-management-page .t-select.t-is-focused .t-input__wrap { + border-color: var(--color-primary); + box-shadow: 0 0 0 2px rgba(22, 119, 255, 0.16); +} + +.dark .light-app-management-page .t-select .t-fake-arrow { + color: var(--color-text-secondary); +} + +.dark .light-app-management-page .t-select__dropdown { + background: var(--color-bg-card); + border-color: var(--color-border); + box-shadow: var(--shadow-2); +} + +.dark .light-app-management-page .t-select-option { + color: var(--color-text); +} + +.dark .light-app-management-page .t-select-option:hover { + background: rgba(22, 119, 255, 0.08); +} + +.dark .light-app-management-page .t-select-option.t-is-selected { + background: rgba(22, 119, 255, 0.16); + color: var(--color-link); +} + +/* shadcn Input / Textarea in dialog */ +.dark .light-app-management-page .lam-dialog [data-slot="input"], +.dark .light-app-management-page .lam-dialog textarea { + background: var(--color-bg-page); + border-color: var(--color-border); + color: var(--color-text); +} + +.dark .light-app-management-page .lam-dialog [data-slot="input"]:focus-visible, +.dark .light-app-management-page .lam-dialog textarea:focus-visible { + border-color: var(--color-primary); + box-shadow: 0 0 0 2px rgba(22, 119, 255, 0.16); +} + +.dark .light-app-management-page .lam-param-theme-box span { + color: var(--color-text-secondary); +} + +/* TDesign 下拉挂载到 body 时的深蓝样式 */ +.dark .lam-select-popup.t-popup { + background: var(--color-bg-card); + border-color: var(--color-border); + box-shadow: var(--shadow-2); +} + +.dark .lam-select-popup .t-select-option { + color: var(--color-text); +} + +.dark .lam-select-popup .t-select-option:hover { + background: rgba(22, 119, 255, 0.08); +} + +.dark .lam-select-popup .t-select-option.t-is-selected { + background: rgba(22, 119, 255, 0.16); + color: var(--color-link); +} + +.dark .light-app-management-page .lam-search-input + button, +.dark .light-app-management-page .lam-toolbar .absolute.text-neutral-400 { + color: var(--color-text-secondary); +} + +.dark .light-app-management-page .lam-table .text-neutral-300 { + color: var(--color-text-disabled); +} + +.dark .light-app-management-page .lam-card-url-box button { + color: var(--color-text-secondary); +} + +.dark .light-app-management-page .lam-card-url-box button:hover { + background: rgba(22, 119, 255, 0.08); + color: var(--color-link); +} diff --git a/frontend-web/src/pages/light-app-pages.css b/frontend-web/src/pages/light-app-pages.css new file mode 100644 index 00000000..459be997 --- /dev/null +++ b/frontend-web/src/pages/light-app-pages.css @@ -0,0 +1,12 @@ +@import "../../docs/ui规范-深蓝/tokens.css"; + +/** 轻应用中心列表 / 管理页 —— 深色模式共用页面底图(与 workspace 深蓝主题一致) */ +.dark .light-app-management-page, +.dark .light-app-list-page { + background-color: var(--color-bg-page); + background-image: url("../assets/img/layout-bg-theme-dark-blue.svg"); + background-size: cover; + background-position: center top; + background-repeat: no-repeat; + background-attachment: fixed; +} diff --git a/frontend-web/src/roundtable-planning/components/HighFidelityReport.tsx b/frontend-web/src/roundtable-planning/components/HighFidelityReport.tsx index 533f8ba9..f12a0bb5 100644 --- a/frontend-web/src/roundtable-planning/components/HighFidelityReport.tsx +++ b/frontend-web/src/roundtable-planning/components/HighFidelityReport.tsx @@ -1009,6 +1009,7 @@ export const HighFidelityReport: React.FC = ({ filepath={fileSandboxVirtualUrl} threadId={fileSandboxTarget.threadId} onClose={handleCloseFileSandbox} + allowVirtualMarkdownEdit /> )} diff --git a/frontend-web/src/roundtable-planning/components/Step2SandboxLayout.tsx b/frontend-web/src/roundtable-planning/components/Step2SandboxLayout.tsx index 1d69dea8..59585fa9 100644 --- a/frontend-web/src/roundtable-planning/components/Step2SandboxLayout.tsx +++ b/frontend-web/src/roundtable-planning/components/Step2SandboxLayout.tsx @@ -115,6 +115,7 @@ export function Step2SandboxLayout({ filepath={selectedArtifact} threadId={previewThreadId} onClose={artifactsFill && onArtifactsClose ? handleArtifactsClose : undefined} + allowVirtualMarkdownEdit /> ) : ( diff --git a/frontend-web/src/strategy-components/Header.tsx b/frontend-web/src/strategy-components/Header.tsx index 7e4f144f..b27f18a5 100644 --- a/frontend-web/src/strategy-components/Header.tsx +++ b/frontend-web/src/strategy-components/Header.tsx @@ -139,6 +139,10 @@ const Header = ({ collapsed }: HeaderProps) => { return email ? email.split("@")[0] : "用户"; }, [auth?.email]); const showTopNav = isDark && !collapsed && !isWujieEnv(); + // 浅色(红/蓝)顶栏:一级导航正常横排进顶栏,不做深色那种围绕基座的特殊处理。 + // 与 collapsed 解耦(折叠只影响侧边栏,一级导航始终可见);wujie 嵌入下隐藏, + // 此时一级导航回落到侧边栏(见 page-sidebar.tsx)。 + const showLightTopNav = !isDark && !isWujieEnv(); const hasActiveChild = (item: MenuItem): boolean => { if (item.path && pathname === item.path) { @@ -194,6 +198,20 @@ const Header = ({ collapsed }: HeaderProps) => { ); }; + const renderLightTopItem = (item: MenuItem) => { + const isSelected = item.path ? pathname === item.path : hasActiveChild(item); + return ( + handleNavItemClick(item)} + > + {item.label} + + ); + }; + // 右侧操作区(换肤/通知/用户)—— 深浅两套布局共用 const actionCluster = ( <> @@ -309,6 +327,27 @@ const Header = ({ collapsed }: HeaderProps) => { > + ) : showLightTopNav ? ( + <> + {/* 浅色顶栏:logo + 一级导航横排(白字落在 banner 上) */} + + + + + + + CM助手 + + + + {visibleMenuItems.map(renderLightTopItem)} + + + + + {actionCluster} + + > ) : ( <> @@ -335,6 +374,7 @@ const Header = ({ collapsed }: HeaderProps) => { > )} + {/* 三级菜单横条仅深色用;浅色三级已改到侧边栏右侧列(见 page-sidebar.tsx) */} {showTopNav && thirdSubNavItems.length ? ( diff --git a/frontend-web/src/strategy-components/lib/light-app.ts b/frontend-web/src/strategy-components/lib/light-app.ts index 481b838b..ab796bf6 100644 --- a/frontend-web/src/strategy-components/lib/light-app.ts +++ b/frontend-web/src/strategy-components/lib/light-app.ts @@ -19,11 +19,17 @@ export function mountLabel(value: string | null | undefined): string { return MOUNT_OPTIONS.find((o) => o.value === value)?.label ?? value; } -/** 登录参数可选来源;值均可在 userInfo 中取到,"other" 表示自填。 */ +/** + * 登录参数可选来源;值均可在 userInfo 中取到,"other" 表示自填。 + * accessToken / yUserId 取登录路由透传值(写入 userInfo 的同名键); + * userName / gateDepartName / typeName 取 consumer 接口 userDetailByYuserId 的返回值。 + */ export const LOGIN_SOURCE_OPTIONS: { label: string; value: string }[] = [ { label: "accessToken", value: "accessToken" }, { label: "yUserId", value: "yUserId" }, { label: "userName", value: "userName" }, + { label: "gateDepartName", value: "gateDepartName" }, + { label: "typeName", value: "typeName" }, { label: "其他(自定义值)", value: "other" }, ]; diff --git a/frontend-web/src/styles/globals.css b/frontend-web/src/styles/globals.css index 7ebd17de..3494b65c 100644 --- a/frontend-web/src/styles/globals.css +++ b/frontend-web/src/styles/globals.css @@ -932,7 +932,7 @@ body { .strategy-app-header { background-color: transparent; background-image: url("../assets/img/red-header.png"); - background-size: 100% 100%; + background-size: cover; background-repeat: no-repeat; background-position: center; } diff --git a/offline-backend-20260512/backend/CLAUDE.md b/offline-backend-20260512/backend/CLAUDE.md index 18e7823d..f6812132 100644 --- a/offline-backend-20260512/backend/CLAUDE.md +++ b/offline-backend-20260512/backend/CLAUDE.md @@ -1,759 +1,759 @@ -# CLAUDE.md -?? -This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. - -## Project Overview - -DeerFlow is a LangGraph-based AI super agent system with a full-stack architecture. The backend provides a "super agent" with sandbox execution, persistent memory, subagent delegation, and extensible tool integration - all operating in per-thread isolated environments. - -**Architecture**: -- **Gateway API** (port 8001): REST API plus embedded LangGraph-compatible agent runtime -- **Frontend** (port 3000): Next.js web interface -- **Nginx** (port 2026): Unified reverse proxy entry point -- **Provisioner** (port 8002, optional in Docker dev): Started only when sandbox is configured for provisioner/Kubernetes mode - -**Runtime**: -- `make dev`, Docker dev, and production all run the agent runtime in Gateway via `RunManager` + `run_agent()` + `StreamBridge` (`packages/harness/deerflow/runtime/`). Nginx exposes that runtime at `/api/langgraph/*` and rewrites it to Gateway's native `/api/*` routers. - -**Project Structure**: -``` -deer-flow/ -??? Makefile # Root commands (check, install, dev, stop) -??? config.yaml # Main application configuration -??? extensions_config.json # MCP servers and skills configuration -??? backend/ # Backend application (this directory) -? ??? Makefile # Backend-only commands (dev, gateway, lint) -? ??? langgraph.json # LangGraph Studio graph configuration -? ??? packages/ -? ? ??? harness/ # deerflow-harness package (import: deerflow.*) -? ? ??? pyproject.toml -? ? ??? deerflow/ -? ? ??? agents/ # LangGraph agent system -? ? ? ??? lead_agent/ # Main agent (factory + system prompt) -? ? ? ??? middlewares/ # 10 middleware components -? ? ? ??? memory/ # Memory V2: provider/manager/builtin/hindsight -? ? ? ??? thread_state.py # ThreadState schema -? ? ??? sandbox/ # Sandbox execution system -? ? ? ??? local/ # Local filesystem provider -? ? ? ??? sandbox.py # Abstract Sandbox interface -? ? ? ??? tools.py # bash, ls, read/write/str_replace -? ? ? ??? middleware.py # Sandbox lifecycle management -? ? ??? subagents/ # Subagent delegation system -? ? ? ??? builtins/ # general-purpose, bash agents -? ? ? ??? executor.py # Background execution engine -? ? ? ??? registry.py # Agent registry -? ? ??? tools/builtins/ # Built-in tools (present_files, ask_clarification, view_image) -? ? ??? mcp/ # MCP integration (tools, cache, client) -? ? ??? models/ # Model factory with thinking/vision support -? ? ??? skills/ # Skills discovery, loading, parsing -? ? ??? config/ # Configuration system (app, model, sandbox, tool, etc.) -? ? ??? community/ # Community tools (tavily, jina_ai, firecrawl, image_search, aio_sandbox) -? ? ??? reflection/ # Dynamic module loading (resolve_variable, resolve_class) -? ? ??? utils/ # Utilities (network, readability) -? ? ??? client.py # Embedded Python client (DeerFlowClient) -? ??? app/ # Application layer (import: app.*) -? ? ??? gateway/ # FastAPI Gateway API -? ? ? ??? app.py # FastAPI application -? ? ? ??? routers/ # FastAPI route modules (models, mcp, memory, skills, uploads, threads, artifacts, agents, suggestions, channels) -? ? ??? channels/ # IM platform integrations -? ??? tests/ # Test suite -? ??? docs/ # Documentation -??? frontend/ # Next.js frontend application -??? skills/ # Agent skills directory - ??? public/ # Public skills (committed) - ??? custom/ # Custom skills (gitignored) -``` - -## Important Development Guidelines - -### Documentation Update Policy -**CRITICAL: Always update README.md and CLAUDE.md after every code change** - -When making code changes, you MUST update the relevant documentation: -- Update `README.md` for user-facing changes (features, setup, usage instructions) -- Update `CLAUDE.md` for development changes (architecture, commands, workflows, internal systems) -- Keep documentation synchronized with the codebase at all times -- Ensure accuracy and timeliness of all documentation - -## Commands - -**Root directory** (for full application): -```bash -make check # Check system requirements -make install # Install all dependencies (frontend + backend) -make dev # Start all services (Gateway + Frontend + Nginx), with config.yaml preflight -make start # Start production services locally -make stop # Stop all services -``` - -**Backend directory** (for backend development only): -```bash -make install # Install backend dependencies -make dev # Run Gateway API with reload (port 8001) -make gateway # Run Gateway API only (port 8001) -make test # Run all backend tests -make lint # Lint with ruff -make format # Format code with ruff -``` - -Regression tests related to Docker/provisioner behavior: -- `tests/test_docker_sandbox_mode_detection.py` (mode detection from `config.yaml`) -- `tests/test_provisioner_kubeconfig.py` (kubeconfig file/directory handling) - -Boundary check (harness ? app import firewall): -- `tests/test_harness_boundary.py` ? ensures `packages/harness/deerflow/` never imports from `app.*` - -CI runs these regression tests for every pull request via [.github/workflows/backend-unit-tests.yml](../.github/workflows/backend-unit-tests.yml). - -## Architecture - -### Harness / App Split - -The backend is split into two layers with a strict dependency direction: - -- **Harness** (`packages/harness/deerflow/`): Publishable agent framework package (`deerflow-harness`). Import prefix: `deerflow.*`. Contains agent orchestration, tools, sandbox, models, MCP, skills, config ? everything needed to build and run agents. -- **App** (`app/`): Unpublished application code. Import prefix: `app.*`. Contains the FastAPI Gateway API and IM channel integrations (Feishu, Slack, Telegram, DingTalk). - -**Dependency rule**: App imports deerflow, but deerflow never imports app. This boundary is enforced by `tests/test_harness_boundary.py` which runs in CI. - -**Import conventions**: -```python -# Harness internal -from deerflow.agents import make_lead_agent -from deerflow.models import create_chat_model - -# App internal -from app.gateway.app import app -from app.channels.service import start_channel_service - -# App ? Harness (allowed) -from deerflow.config import get_app_config - -# Harness ? App (FORBIDDEN ? enforced by test_harness_boundary.py) -# from app.gateway.routers.uploads import ... # ? will fail CI -``` - -### Agent System - -**Lead Agent** (`packages/harness/deerflow/agents/lead_agent/agent.py`): -- Entry point: `make_lead_agent(config: RunnableConfig)` registered in `langgraph.json` -- Dynamic model selection via `create_chat_model()` with thinking/vision support -- Tools loaded via `get_available_tools()` - combines sandbox, built-in, MCP, community, and subagent tools -- System prompt generated by `apply_prompt_template()` with skills, memory, and subagent instructions - -**ThreadState** (`packages/harness/deerflow/agents/thread_state.py`): -- Extends `AgentState` with: `sandbox`, `thread_data`, `title`, `artifacts`, `todos`, `uploaded_files`, `viewed_images` -- Uses custom reducers: `merge_artifacts` (deduplicate), `merge_viewed_images` (merge/clear) - -**Runtime Configuration** (via `config.configurable`): -- `thinking_enabled` - Enable model's extended thinking -- `model_name` - Select specific LLM model -- `is_plan_mode` - Enable TodoList middleware -- `subagent_enabled` - Enable task delegation tool - -### Middleware Chain - -Lead-agent middlewares are assembled in strict append order across `packages/harness/deerflow/agents/middlewares/tool_error_handling_middleware.py` (`build_lead_runtime_middlewares`) and `packages/harness/deerflow/agents/lead_agent/agent.py` (`_build_middlewares`): - -1. **ThreadDataMiddleware** - Creates per-thread directories under the user's isolation scope (`backend/.deer-flow/users/{user_id}/threads/{thread_id}/user-data/{workspace,uploads,outputs}`); resolves `user_id` via `get_effective_user_id()` (falls back to `"default"` in no-auth mode); Web UI thread deletion now follows LangGraph thread removal with Gateway cleanup of the local thread directory -2. **UploadsMiddleware** - Tracks and injects newly uploaded files into conversation -3. **SandboxMiddleware** - Acquires sandbox, stores `sandbox_id` in state -4. **DanglingToolCallMiddleware** - Injects placeholder ToolMessages for AIMessage tool_calls that lack responses (e.g., due to user interruption), including raw provider tool-call payloads preserved only in `additional_kwargs["tool_calls"]` -5. **LLMErrorHandlingMiddleware** - Normalizes provider/model invocation failures into recoverable assistant-facing errors before later middleware/tool stages run -6. **GuardrailMiddleware** - Pre-tool-call authorization via pluggable `GuardrailProvider` protocol (optional, if `guardrails.enabled` in config). Evaluates each tool call and returns error ToolMessage on deny. Three provider options: built-in `AllowlistProvider` (zero deps), OAP policy providers (e.g. `aport-agent-guardrails`), or custom providers. See [docs/GUARDRAILS.md](docs/GUARDRAILS.md) for setup, usage, and how to implement a provider. -7. **SandboxAuditMiddleware** - Audits sandboxed shell/file operations for security logging before tool execution continues -8. **ToolErrorHandlingMiddleware** - Converts tool exceptions into error `ToolMessage`s so the run can continue instead of aborting -9. **SummarizationMiddleware** (`DeerFlowSummarizationMiddleware`) - Context reduction when approaching token limits (optional, if enabled). **Non-destructive**: overrides the stock `before_model` (which persists a `RemoveMessage(REMOVE_ALL_MESSAGES)`) to a no-op and instead compresses **transiently** in `wrap_model_call` ? the model request is replaced with `[summary, *recent]` while the full original conversation stays in state (so the frontend keeps rendering the user's real Q&A). The `summary` message is `hide_from_ui` and never persisted; summaries are cached per-thread (bounded, sticky boundary) to avoid an LLM call on every model invocation. Tests: `tests/test_summarization_middleware.py` -10. **TodoListMiddleware** - Task tracking with `write_todos` tool (optional, if plan_mode) -11. **TokenUsageMiddleware** - Records token usage metrics when token tracking is enabled (optional) -12. **TitleMiddleware** - Two-phase auto-title (never shows "untitled"): `before_model` sets an instant question-derived *provisional* title the moment the first user message arrives (no LLM, zero latency), then `after_model` asynchronously generates the final LLM title and replaces it (tracked via the `title_provisional` state flag). Normalizes structured message content before prompting the title model -13. **MemoryMiddleware** - Memory V2: `wrap_model_call` injects Hindsight recall transiently; `after_agent` auto-retains the turn to Hindsight -14. **ViewImageMiddleware** - Injects base64 image data before LLM call (conditional on vision support) -15. **DeferredToolFilterMiddleware** - Hides deferred tool schemas from the bound model until tool search is enabled (optional) -16. **SubagentLimitMiddleware** - Truncates excess `task` tool calls from model response to enforce `MAX_CONCURRENT_SUBAGENTS` limit (optional, if `subagent_enabled`) -17. **LoopDetectionMiddleware** - Detects repeated tool-call loops; hard-stop responses clear both structured `tool_calls` and raw provider tool-call metadata before forcing a final text answer -18. **ClarificationMiddleware** - Intercepts `ask_clarification` tool calls, interrupts via `Command(goto=END)` (must be last) -8a. **ToolMetricsMiddleware** - Appended immediately after `ToolErrorHandlingMiddleware` in `_build_runtime_middlewares`, so it sits *deepest* in the `wrap_tool_call` chain. Records one `tool_call_metrics` row per tool/skill/MCP invocation ? timing duration and bucketing failures into a coarse `error_category` (rate_limited / ip_blocked / auth_error / timeout / network_error / not_found / empty_result / tool_error). It catches both raised exceptions (before #8 converts them) and *self-handled* errors returned as error `ToolMessage`s (search tools that swallow HTTP 429/403). It also captures the targeted Agent Skill into `tool_call_metrics.skill_name`: the lead agent invokes a skill by `read_file`-ing its `/mnt/skills///SKILL.md` path (per the `` prompt block ? there is no dedicated "use skill" tool), so `skill_name` is recovered by scanning every tool call's string arguments for the `skills/{public,custom}/{name}/` path shape (`_SKILL_PATH_RE`), plus the explicit `name` argument of `skill_view`/`skill_manage`/`skill_archive`. `skill_view`-specific success detection handles its swallowed failures (`Skill 'x' not found.` returned as a plain string rather than raised). Regression coverage: `tests/test_tool_metrics_middleware.py`. Writes are best-effort fire-and-forget. **Per-run skill merging**: `SqlToolMetricsStore.record` upserts skill rows by `(run_id, skill_name)` instead of inserting one row per call ? a skill touched by several tool calls / retries inside one run collapses to a single row. Any success in the run wins (a skill that failed then finally succeeded is recorded as one `success`); a skill that only ever failed yields one `error` row whose `error_message` accumulates every attempt's message; durations are summed. Non-skill tool calls, and skill calls with no `run_id`, keep one row each. The read?merge?write is guarded by a per-event-loop `asyncio.Lock`. Surfaced by the admin `GET /api/admin/tool-metrics` router (which has a `kind=skill` filter and a per-skill `/summary` breakdown). -9. **PromptPrefixMiddleware** - Injects the admin-configured prompt prefix into the model request only. The prefix is tagged onto the first human message's `additional_kwargs['prompt_prefix']` by the Gateway (`app.gateway.services`) and prepended to content here at call time, so the persisted/displayed message stays prefix-free -10. **SummarizationMiddleware** (`DeerFlowSummarizationMiddleware`) - Context reduction when approaching token limits (optional, if enabled). **Non-destructive**: overrides the stock `before_model` (which persists a `RemoveMessage(REMOVE_ALL_MESSAGES)`) to a no-op and instead compresses **transiently** in `wrap_model_call` ? the model request is replaced with `[summary, *recent]` while the full original conversation stays in state (so the frontend keeps rendering the user's real Q&A). The `summary` message is `hide_from_ui` and never persisted; summaries are cached per-thread (bounded, sticky boundary) to avoid an LLM call on every model invocation. Tests: `tests/test_summarization_middleware.py` -11. **TodoListMiddleware** - Task tracking with `write_todos` tool (optional, if plan_mode) -12. **TokenUsageMiddleware** - Records token usage metrics when token tracking is enabled (optional) -13. **TitleMiddleware** - Two-phase auto-title (never shows "untitled"): `before_model` sets an instant question-derived *provisional* title the moment the first user message arrives (no LLM, zero latency), then `after_model` asynchronously generates the final LLM title and replaces it (tracked via the `title_provisional` state flag). Normalizes structured message content before prompting the title model -14. **MemoryMiddleware** - Queues conversations for async memory update (filters to user + final AI responses) -15. **ViewImageMiddleware** - Injects base64 image data before LLM call (conditional on vision support) -16. **DeferredToolFilterMiddleware** - Hides deferred tool schemas from the bound model until tool search is enabled (optional) -17. **SubagentLimitMiddleware** - Truncates excess `task` tool calls from model response to enforce `MAX_CONCURRENT_SUBAGENTS` limit (optional, if `subagent_enabled`) -18. **LoopDetectionMiddleware** - Detects repeated tool-call loops; hard-stop responses clear both structured `tool_calls` and raw provider tool-call metadata before forcing a final text answer -19. **ClarificationMiddleware** - Intercepts `ask_clarification` tool calls, interrupts via `Command(goto=END)` (must be last) - -### Configuration System - -**Main Configuration** (`config.yaml`): - -Setup: Copy `config.example.yaml` to `config.yaml` in the **project root** directory. - -**Config Versioning**: `config.example.yaml` has a `config_version` field. On startup, `AppConfig.from_file()` compares user version vs example version and emits a warning if outdated. Missing `config_version` = version 0. Run `make config-upgrade` to auto-merge missing fields. When changing the config schema, bump `config_version` in `config.example.yaml`. - -**Config Caching**: `get_app_config()` caches the parsed config, but automatically reloads it when the resolved config path changes or the file's mtime increases. This keeps Gateway and LangGraph reads aligned with `config.yaml` edits without requiring a manual process restart. - -Configuration priority: -1. Explicit `config_path` argument -2. `DEER_FLOW_CONFIG_PATH` environment variable -3. `config.yaml` in current directory (backend/) -4. `config.yaml` in parent directory (project root - **recommended location**) - -Config values starting with `$` are resolved as environment variables (e.g., `$OPENAI_API_KEY`). -`ModelConfig` also declares `use_responses_api` and `output_version` so OpenAI `/v1/responses` can be enabled explicitly while still using `langchain_openai:ChatOpenAI`. - -**Extensions Configuration** (`extensions_config.json`): - -MCP servers and skills are configured together in `extensions_config.json` in project root: - -Configuration priority: -1. Explicit `config_path` argument -2. `DEER_FLOW_EXTENSIONS_CONFIG_PATH` environment variable -3. `extensions_config.json` in current directory (backend/) -4. `extensions_config.json` in parent directory (project root - **recommended location**) - -### Gateway API (`app/gateway/`) - -FastAPI application on port 8001 with health check at `GET /health`. Set `GATEWAY_ENABLE_DOCS=false` to disable `/docs`, `/redoc`, and `/openapi.json` in production (default: enabled). - -**Routers**: - -| Router | Endpoints | -|--------|-----------| -| **Models** (`/api/models`) | `GET /` - list models; `GET /{name}` - model details | -| **MCP** (`/api/mcp`) | `GET /config` - get config; `PUT /config` - update config (saves to extensions_config.json) | -| **Skills** (`/api/skills`) | `GET /` - list skills; `GET /{name}` - details; `PUT /{name}` - update enabled; `POST /install` - install from .skill archive (accepts standard optional frontmatter like `version`, `author`, `compatibility`) | -| **Memory V2** (`/api/memory/v2`) | `GET /` - builtin data (USER.md/MEMORY.md); `POST/PUT/DELETE /entries` - add/replace/remove entry; `GET/PATCH/DELETE /config` - effective config + per-user overrides; `POST /search` - Hindsight recall/reflect proxy | -| **Agents** (`/api/agents`) | `GET /` - list built-in, owned, and published agents; `POST /` - create user-owned agent; `GET/PUT/DELETE /{id}` - read/update/delete by stable agent id; `GET /check` - validate non-empty display name (duplicates allowed) | -| **Skills** (`/api/skills`) | `GET /` - list skills (optional `search` fuzzy-name + `tag_id` filters; each item carries `tags`); `GET /{name}` - details; `PUT /{name}` - update enabled; `POST /install` - install from .skill archive (accepts standard optional frontmatter like `version`, `author`, `compatibility`) | -| **Skills** (`/api/skills`) | `GET /` - list skills (optional `search` fuzzy-name + `tag_id` filters; each item carries `tags` + owner-authored `detail`); `GET /{name}` - details; `PUT /{name}` - update enabled; `PUT /{name}/detail` - set the long-form `detail` text (custom skill: owner or admin; built-in/public: admin only ? materializes a `skills` row on demand); `POST /install` - install from .skill archive (accepts standard optional frontmatter like `version`, `author`, `compatibility`) | -| **Skills** (`/api/skills`) | `GET /` - list skills (optional `search` fuzzy-name + `tag_id` filters; each item carries `tags` + owner-authored `detail`); `GET /{name}` - details; `PUT /{name}` - update enabled; `PUT /{name}/detail` - set the long-form `detail` text (custom skill: owner or admin; built-in/public: admin only ? materializes a `skills` row on demand); `GET /{name}/content` - raw SKILL.md source, restricted to an admin or the publisher (custom-skill owner); other users get 403 and see only description + detail; `POST /install` - install from .skill archive (accepts standard optional frontmatter like `version`, `author`, `compatibility`) | -| **Skills** (`/api/skills`) | `GET /` - list skills (optional `search` fuzzy-name + repeatable `tag_ids` multi-select filter, OR-matched; each item carries `tags` + owner-authored `detail`); `GET /{name}` - details; `PUT /{name}` - update enabled; `PUT /{name}/detail` - set the long-form `detail` text (custom skill: owner or admin; built-in/public: admin only ? materializes a `skills` row on demand); `GET /{name}/content` - raw SKILL.md source, restricted to an admin or the publisher (custom-skill owner); other users get 403 and see only description + detail; `POST /install` - install from .skill archive (accepts standard optional frontmatter like `version`, `author`, `compatibility`) | -| **Memory** (`/api/memory`) | `GET /` - memory data; `POST /reload` - force reload; `GET /config` - config; `GET /status` - config + data | -| **Agents** (`/api/agents`) | `GET /` - list built-in, owned, and published agents (optional `search` + repeatable `tag_ids` multi-select filter, OR-matched; each item carries `tags` and `featured_order`); `POST /` - create user-owned agent; `GET/PUT/DELETE /{id}` - read/update/delete by stable agent id; `GET /check` - validate non-empty display name (duplicates allowed); `GET /selector` - homepage agent picker (single admin-curated list, visibility-filtered, sorted by `featured_order` asc); `PUT /{id}/featured` - **admin** sets/clears the homepage `featured_order` (must be `published=true` to feature; passing `order: null` clears unconditionally); `PUT /featured/order` - **admin** bulk-reorders the homepage list (body `{agent_ids: [...]}` ? indices become 0..N-1) | -| **Tags** (`/api/tags`) | `GET /` - list tags (optional `tag_type` filter; open to all); `POST /` - create tag (admin); `PUT /{id}` / `DELETE /{id}` - update/delete tag (admin); `GET /assignments/{target_type}/{target_id}` - tags on a resource; `PUT /assignments/{target_type}/{target_id}` - replace a resource's tags (admin). Tags are typed `agent`/`skill`/`scheduled_task` and may only tag resources of their own type. | -| **Uploads** (`/api/threads/{id}/uploads`) | `POST /` - upload files (auto-converts PDF/PPT/Excel/Word); `GET /list` - list; `DELETE /{filename}` - delete | -| **Threads** (`/api/threads/{id}`) | `DELETE /` - remove DeerFlow-managed local thread data after LangGraph thread deletion; unexpected failures are logged server-side and return a generic 500 detail | -| **Threads search** (`POST /threads/search`, LangGraph-compat) | Conversation listing. **System threads are excluded by default**: rows whose `metadata.system` is truthy (scheduler runs, roundtable workers) never crowd the user's list; a caller whose `metadata` filter explicitly references `thread_type`/`system` (e.g. studio notebooks) opts out of the exclusion. New `query` field = case-insensitive title (`display_name`) substring search, used by the chats page's server-side pagination. `POST /threads/count` takes the same request body (limit/offset ignored) and returns `{total}` under identical filtering/exclusion semantics ? powers the chats page's ??/??? display without fetching rows (`ThreadMetaStore.count`). `ThreadMetaStore.search` gained `exclude_system`/`query` params; JSON-blob filters batch-scan until the requested `limit`/`offset` window is full (so pagination over a heavily-filtered set never under-fills), with a `thread_id` tie-breaker on the `updated_at DESC` ordering for stable OFFSET paging. Tests: `tests/test_thread_meta_search.py` | -| **Artifacts** (`/api/threads/{id}/artifacts`) | `GET /{path}` - serve artifacts; active content types (`text/html`, `application/xhtml+xml`, `image/svg+xml`) are always forced as download attachments to reduce XSS risk; `?download=true` still forces download for other file types | -| **Suggestions** (`/api/threads/{id}/suggestions`) | `POST /` - generate follow-up questions; rich list/block model content is normalized before JSON parsing | -| **Thread Runs** (`/api/threads/{id}/runs`) | `POST /` - create background run; `POST /stream` - create + SSE stream; `POST /wait` - create + block; `GET /` - list runs; `GET /{rid}` - run details; `POST /{rid}/cancel` - cancel; `GET /{rid}/join` - join SSE; `GET /{rid}/messages` - paginated messages `{data, has_more}`; `GET /{rid}/events` - full event stream; `GET /../messages` - thread messages with feedback; `GET /../token-usage` - aggregate tokens | -| **Feedback** (`/api/threads/{id}/runs/{rid}/feedback`) | `PUT /` - upsert feedback; `DELETE /` - delete user feedback; `POST /` - create feedback; `GET /` - list feedback; `GET /stats` - aggregate stats; `DELETE /{fid}` - delete specific | -| **Runs** (`/api/runs`) | `POST /stream` - stateless run + SSE; `POST /wait` - stateless run + block; `GET /{rid}/messages` - paginated messages by run_id `{data, has_more}` (cursor: `after_seq`/`before_seq`); `GET /{rid}/feedback` - list feedback by run_id | -| **Thread Shares** (`/api/thread-shares`) | Per-conversation sharing with two modes (`thread_shares.mode`). **`import`**: `POST /` `{thread_id, mode}` - create (or reuse) a revocable share code; `GET /{code}/preview` + `POST /{code}/import` - copy that conversation into the caller's account (fresh `thread_id` via `app/gateway/thread_copy.py` + new `threads_meta` row + `user-data` copy; dedup on `metadata.shared_origin`). **`view`**: a public, read-only static page ? see Public Shares below. Both modes: `GET /` - list my codes; `DELETE /{code}` - revoke. | -| **Public Shares** (`/api/public/shares`) | **Unauthenticated** (the `/api/public/` prefix is whitelisted in `auth_middleware._PUBLIC_PATH_PREFIXES`). `GET /{code}` - read-only conversation snapshot (title + serialized messages + artifacts) for a `view`-mode share; messages are read straight from the latest checkpoint. `GET /{code}/files/{path}` - serves a file referenced by that conversation (images, Markdown, text, PDF, ?) so the shared page's links are openable; path resolution is confined to the owner's thread directory. Active content (`text/html`, `application/xhtml+xml`, `image/svg+xml`) is forced to download (`_public_share_disposition`); everything else is served inline. Missing/revoked/non-`view` codes all return 404. | -| **Roundtable Drafts** (`/api/roundtable-drafts`) | Per-user persistence for the roundtable-planning page (replaces the old browser `localStorage`). `GET /` - list drafts (lightweight meta); `POST /` - create; `GET/PUT/DELETE /{id}` - full read / partial update / delete. A draft = one full session (Step 1 intent + Step 2 roundtable), step snapshots stored as JSON in `PortableLongText`. Recommendation history bound to a draft: `GET /{id}/recommendations` - list newest-first; `POST /{id}/recommendations` - append one run (objective/status/rationale/picks/candidates). Backed by `deerflow.persistence.roundtable_drafts` (tables `roundtable_drafts`, `roundtable_recommend_history`); store wired in `deps.py` as `app.state.roundtable_draft_store`. | -| **Roundtable Draft Shares** (`/api/roundtable-draft-shares`) | Per-draft sharing for the roundtable-planning page, mirroring Thread Shares but the shared resource is a `roundtable_drafts` row. Two modes (`roundtable_draft_shares.mode`). **`import`**: `POST /` `{draft_id, mode}` - create (or reuse) a revocable code; `GET /{code}/preview` + `POST /{code}/import` - copy that draft (step1/2/3) into the caller's account under a deterministic per-(code,recipient) id (`imp_` ? re-import is a no-op). **`view`**: a public, read-only result page ? see Public Roundtable Shares. Both: `GET /` - list my codes; `DELETE /{code}` - revoke. Backed by `deerflow.persistence.roundtable_draft_shares` (table `roundtable_draft_shares`, migration `20260606_01`); store wired in `deps.py` as `app.state.roundtable_draft_share_store`. Tests: `tests/test_roundtable_draft_shares.py`. | -| **Public Roundtable Shares** (`/api/public/roundtable-shares`) | **Unauthenticated** (`/api/public/` whitelisted). `GET /{code}` - read-only snapshot for a `view`-mode draft share: returns the Step 3??????report HTML (`step3.html`, a self-contained document) + title/summary/`has_report`. The frontend renders it in a sandboxed `iframe` (no `allow-scripts`) at `#/roundtable-share/`. Missing/revoked/non-`view` codes all return 404. | -| **Scheduled Tasks** (`/api/scheduled-tasks`) | Per-user cron-like agent tasks. `GET/POST /` - list/create owned tasks; `PATCH/DELETE /{id}` - update/delete (owner); `POST /{id}/{pause,resume,trigger}`; `GET /{id}/runs`, `GET /runs/{rid}`, run-file preview/edit/download; `GET /runs/{rid}/messages` - agent messages for the run's underlying agent run (used to inspect a stuck/running run; authorized via `_load_run`); `GET /runs/{rid}/html` - generated HTML document for an `html_page` run; `DELETE /runs/{rid}` - owner-scoped, with an admin fallback so admins can delete any user's run; publish/subscribe under `/{id}/{publish,unpublish,subscribe}` and `GET /published`. **Admin (cross-user):** `GET /admin/all` - every task; `PATCH/DELETE /admin/{id}` - edit/delete any task; `POST /admin/{id}/trigger`; `GET /admin/{id}/runs`. Admin routes require `system_role == "admin"`. | -| **HTML Page Favorites** (`/api/html-page-favorites`) | Bookmarked generated HTML pages, owner-scoped. `GET /` - list; `POST /` `{page_id, title?, description?, tags?}` - bookmark a `scheduled_html_pages` row (extracts a reusable style/layout summary via GLM5 at favorite time); a page can only be favorited once per user (`DuplicateFavoriteError` ? HTTP 409). `GET/DELETE /{id}` - read/delete (the create-task reference picker deletes favorites here). A favorite can be selected as a style reference when creating a new `html_page` task. | -| **Public Scheduled Tasks** (`/api/public/scheduled-tasks`) | **Unauthenticated** mirror for embedding. Adds `GET /runs/{rid}/html` and `GET /{task_id}/latest-html` for the `html_page` preview; the run result keeps `result.html` (only file paths are redacted). | -| **Light Apps** (`/api/light-apps`) | Light-app registry (????? / ????), **global/shared** (no user scoping; `created_by`/`updated_by` audit-only). Two kinds: `window_open` (launched in a new tab from the ???? card grid) and `iframe` (embedded under a parent sidebar menu via `mount_location` and dynamically injected into the sidebar). Both carry `route_params` ? a list of `{key, paramType: login\|theme, source, customValue, themeValue}` descriptors the **frontend** resolves at launch time (login params read from `localStorage.userInfo`; theme params use a literal value) and appends to `url` as query string. `GET /` - list (filters `app_type`/`mount_location`/`enabled_only`/`search`); `GET /{app_id}` - read one (both open to any authenticated user); `POST /`, `PUT /{app_id}`, `DELETE /{app_id}` - **admin only**; iframe apps must carry a `mount_location` (400 otherwise); the unique `name` collides as HTTP 409. Backed by `deerflow.persistence.light_apps` (table `light_apps`, auto-created by `create_all`); store wired in `deps.py` as `app.state.light_app_store`. Tests: `tests/test_light_apps.py`. | -| **Knowledge** (`/api/knowledge`) | Product knowledge base, **global/shared** (no user isolation; writes record `created_by`/`updated_by` for audit). `POST /ingest/thread/{thread_id}` - sediment one thread into a note (reads messages from the checkpointer, `threads:read` owner-checked); `POST /ingest/threads/batch` - batch-sediment the caller's recent threads (`skip_existing` via `.manifest.json` dedup). **Optional model selection**: both ingest endpoints accept `model_name` (overrides `knowledge.extract_model_name` for that capture; threaded through `KnowledgeService.capture_thread` ? `extract_thread_llm_multi`). **Model-error surfacing**: when the chosen model produces nothing usable (timeout / API error / non-JSON) capture still **falls back to rule-based extraction and lands a draft** (`llm_fallback:true`); the single-thread (non-background) response and the batch response (`llm_fallback` count) carry this so the UI tells the user "???????". The "??????" chat button stays background/fire-and-forget (passes `model_name`, no per-save error toast); the batch-import dialog is synchronous and shows the fallback count. `POST /ingest/file` - **import an uploaded file** (multipart: `file` + optional `title`/`tags`/`status`/`model_name`) into wiki note(s): the file is converted to Markdown (md/txt read directly, pdf/word/ppt/excel via `convert_file_to_markdown`), distilled by the same wiki-capture LLM pipeline as thread capture (`source_type="file"`), and falls back to a verbatim **draft** note when the model returns nothing usable (`llm_fallback:true`); `POST /ingest/search` - sediment search/tool results as a reference note; `POST /notes` - manual note; `GET /notes` (keyword/tag/source_type/status + pagination); `GET/PUT/DELETE /notes/{id}` (DELETE is soft = archive, `?hard=true` removes the vault file); `POST /search` - `mode=keyword\|vector\|hybrid` (vector?keyword fallback when no embedding endpoint); `GET /notes/{id}/versions` - edit history; `GET /duplicates` + `POST /merge` - dedup; `POST /reindex` - rebuild vector index; `GET /graph` - product graph data (notes + entities, graph-blacklist filtered); `GET/POST /graph/blacklist` + `DELETE /graph/blacklist/{id}` - graph node blacklist (labels = entity names / note titles, case-insensitive match; blacklisted nodes and their edges are dropped from `/graph` and the wiki-export artifacts; table `knowledge_graph_blacklist`, auto-created); `POST /export/graph` + `GET /export/files/{graph.json\|graph.html\|graph.graphml\|cypher.txt}` - obsidian-wiki `wiki-export` artifacts (self-contained `graph.html` for iframe); `POST /resolve` `{targets:[?]}` + `GET /notes/{id}/wikilinks` - resolve `[[wikilink]]` targets to a note id / vault page / missing (powers clickable wikilinks); `GET /vault-page?path=` - read a raw vault meta/hub Markdown page (no DB note). Backed by the `deerflow.knowledge` module (see below). | - -Proxied through nginx: `/api/langgraph/*` ? LangGraph, all other `/api/*` ? Gateway. - -### AI Writing Built-in Functional Agents (????????) - -The AI-writing graph (`packages/harness/deerflow/agents/ai_writing/`) has four stage "brains" ? ?????? / ????? / ?? / ?? ? that can each run in one of two modes, switched per-stage by `config.yaml ? ai_writing.use_builtin_agents.{researcher,outliner,writer,editor}` (default **false**, mtime hot-reloaded, new sessions pick up changes immediately): - -- **Legacy (flag off)**: hard-coded prompts in `prompts/*.py`, direct LLM call (unchanged behavior). -- **Builtin-agent (flag on)**: the node loads the corresponding functional agent from `.deer-flow/agents/ai-writing-/` ? `SOUL.md` becomes the system prompt and `config.yaml`'s `model` overrides the stage model (priority: `agent.model > node explicit fallback (keyword_model/rank_model) > state.model_name`). Admins edit SOUL/model from the agent management page. Runner: `nodes/_agent_runner.py` (`run_writing_agent` / `stream_writing_agent` / `parse_marker_json`). Output contracts (marker + ```json``` blocks, intent.py-style): researcher `[KEYWORDS_READY]` / `[MATERIALS_RANKED]`, outliner `[OUTLINE_READY]`, editor (per-dimension) `[REVIEW_READY]`; writer emits raw section Markdown (keeps per-section streaming) with the legacy `[[SECTION_BLOCKED]]` sentinel in strict mode. **Orchestration stays in the nodes** (search fan-out, per-section loop, 3-dimension review loop, all pause points) ? the agent only does single structured generations, and a missing/empty SOUL silently falls back to the legacy path (`WritingAgentUnavailable`). - -Seeding mirrors the roundtable pattern: verified `config.yaml` + `SOUL.md` ship in `app/gateway/routers/_ai_writing_seed_assets//`; `_ai_writing_seed.py::ensure_ai_writing_functional_agents()` copies them to `.deer-flow/agents/` when missing (never overwrites local edits), called from the startup lifespan (before `_sync_legacy_agents`, so the agents appear in `/api/agents` on first boot) and self-heals from `GET /api/ai-writing/sessions` + `GET /api/ai-writing/article-types`. Tests: `tests/test_ai_writing_seed.py`, `tests/test_ai_writing_agent_runner.py`. - -**Skill-driven retrieval (检索类型).** `material_source` has exactly two values: `general` (通用检索, default) and `notebook` (我的空间); legacy `knowledge_base`/`skill` are normalized to `general` by the researcher node. General retrieval is orchestrated from the **skills configured on the `ai-writing-researcher` agent** (`config.yaml → skills`, editable from the agent management page): the built-in `knowledge-base-search` skill (知识库检索) runs the native keyword fan-out web/intranet search (the old default path), while any other configured skill searches with a `description`-targeted query; skills run in configured order with early-stop at `max_materials`, and an empty/unresolvable skills config falls back to the built-in knowledge search so retrieval always works. The skill itself is seeded from `_ai_writing_seed_assets/skills/knowledge-base-search/SKILL.md` into `skills/public/` (never overwrites), and a pre-existing researcher `config.yaml` lacking the `skills` key is patched in place with the default (explicit `skills: []` is respected). **PAUSE-1 input append flow**: when the user types natural language into the material-confirm input (`re_search`/legacy `enable_skill_search` + `user_query`), the node matches the most relevant configured skill via `SkillPicker(candidates=…)` (skipped when only one skill is configured), searches with the user's words, and **appends** the new materials to the existing package (URL-dedup, original order preserved, no LLM re-rank/truncation, 100-item hard cap). Tests: `tests/test_ai_writing_material_source.py`. - -### Admin Leaderboard Snapshots (`/api/admin/users/leaderboard*`) - -The admin ????? is served from **precomputed per-day snapshots** in `admin_leaderboard_daily_stats` (keyed by `(stat_date, settings_hash)`), not by aggregating raw rows on every request. A background scheduler inside the Gateway (`_leaderboard_snapshot_scheduler_loop` in `app/gateway/app.py`, singleton file-locked) ticks every `ADMIN_LEADERBOARD_BACKGROUND_INTERVAL_SECONDS` (default 300s): it prewarms recent days, then builds any `queued`/`error`/stale-`running` day. `GET /leaderboard` merges the per-day snapshots for the requested range. **Today** is rebuilt whenever its snapshot is older than `ADMIN_LEADERBOARD_TODAY_TTL_SECONDS` (default 43200s = half a day, so the leaderboard refreshes ~twice daily); a past day, once `ready`, is treated as fresh forever. - -Two on-demand/automatic refresh paths layer on top: -- **Manual "????"** ? `POST /leaderboard/refresh-today` force-rebuilds *today's* snapshot **synchronously** (bounded by the same semaphore/timeout the scheduler uses), then returns the merged range. Lets an admin pull the latest real-time records into the stats without waiting for the TTL. Frontend: a "????" button on `AdminUserLeaderboardPage` ? `useRefreshTodayLeaderboard` (`core/admin/hooks.ts`), which primes + invalidates the `["admin-activity","leaderboard"]` query. -- **Daily finalize of the previous day** ? once per day, on/after `ADMIN_LEADERBOARD_FINALIZE_HOUR` (local Beijing, default `1` = ?? 1 ?), the scheduler tick force-recomputes *yesterday* a single time so runs/tool-calls that only landed near midnight are fully captured. The decision is the pure `deerflow.persistence.admin_stats.finalize.should_finalize_previous_day` (status/generated_at/now/hour) ? the rebuild stamps `generated_at` to today, which self-debounces it for the rest of the day (no extra state file). Tests: `tests/test_leaderboard_finalize.py`. - -### Scheduled Task Runtime - -`ScheduledTaskService` (`deerflow/runtime/scheduler/service.py`) polls due tasks every `poll_interval_seconds` and executes each in its own throwaway thread, serialized by a shared `_agent_run_lock`. - -**Stuck-run recovery**: each execution attempt is bounded by `_RUN_ATTEMPT_TIMEOUT_SECONDS` (30 min) via `asyncio.wait_for`. On timeout the run is retried up to `_MAX_RUN_RETRIES` (3) times, then marked `failed`. A watchdog (`_reconcile_stuck_runs`, run every poll) recovers runs left in `running` with no in-process owner ? e.g. orphaned by a crash or restart ? re-driving them through the same retry budget so a task can never stay perpetually "executing". The `scheduled_task_runs.retry_count` column tracks consumed retries. - -**Unique task names**: task names are unique per user (`uq_scheduled_tasks_user_name`). `create_task`/`update_task`/`admin_update_task` proactively check via `_assert_name_available()` and raise `DuplicateTaskNameError` on a collision; the router translates that into HTTP `409` instead of letting the raw DB `IntegrityError` surface as a `500`. - -**HTML page tasks (`task_kind == "html_page"`)**: a second task kind that renders a full HTML document each run instead of a Markdown report. The kind + its config (`html_config`: `page_prompt`, `negative_prompt`, `reference_favorite_id`, `model_name`) live inside `execution_context` ? no schema change to `scheduled_tasks`. Data gathering is delegated to the agent's own search skills (driven by `page_prompt`), so there is no explicit search-query/data-source field; the render model is user-selectable (`model_name`, default `zai-org/GLM-5-FP8`). `_run_task_attempt()` dispatches by kind; `_run_html_page_attempt()` runs the pipeline: gather real data via the lead agent (its search tools) ? optionally load a reference favorite's reusable style/layout ? render with the chosen model (thinking off) ? fault-tolerant JSON parse ? sanitize (BeautifulSoup, strips `
+ 当前系统默认: + {globalLayoutMode === "full" ? "全量模式" : "简洁模式"} +
+
注册轻应用。 window.open @@ -235,14 +245,14 @@ export default function LightAppManagementPage() { 注册应用
window.open @@ -235,14 +245,14 @@ export default function LightAppManagementPage() { 注册应用
{isLoading ? "加载中..." : "暂无匹配的组件应用"}
{search ? "请确认搜索关键词输入是否正确" : "点击右上角“注册应用”添加第一款轻应用"}
{app.description || "暂无应用详细描述说明。"}
{mountLabel(app.mountLocation)}
{form.appType === "window_open" ? "在新标签页或卡片式路由容器打开。多用于外部高度复杂的系统跳转。" : "通过标准 iframe 嵌入页面,可直接挂载到左侧导航树相应分类内。"} @@ -680,8 +690,8 @@ export default function LightAppManagementPage() {
各系统主题色跳转/嵌套时传给该应用的值(默认同名,可按对方命名修改)
` with NO authentication — anyone with - * the link can open it. Two views, driven by the `report` prop / route: + * the link can open it. Three views, driven by the `view` prop / route: * * - landing (`#/roundtable-share/`): a read-only result page mirroring the - * in-app Step 3「最终方案展示」— 总控最终结论 + 各席位最终交付 + a 「查看结果绘制」 - * button that opens the full report in a NEW TAB (when one was generated). - * - report (`#/roundtable-share//report`): the Step 3 report (a + * in-app Step 3「最终方案展示」— 结果绘制/大屏多页入口 + 方案总结报告 + 总控最终 + * 结论 + 各席位最终交付. + * - report (`#/roundtable-share//report`): the Step 3 经典 HTML 看板 (a * self-contained HTML document) rendered full-screen inside a sandboxed - * `iframe` (no `allow-scripts`), so the agent-generated HTML can never execute - * code in the app origin. + * `iframe` (no `allow-scripts`). + * - dashboard (`#/roundtable-share//dashboard`): the Step 3 大屏多页 (模式二) + * — rendered on the fly from the stored structured data via `renderDashboardHtml` + * into the same sandboxed `iframe`. */ export default function PublicRoundtableSharePage({ - report = false, + view = "landing", }: { - report?: boolean; + view?: RoundtableShareView; }) { const { shareCode } = useParams<{ shareCode: string }>(); // 嵌套方可用 ?bg=/?text= 或 postMessage 覆盖背景/文字色。 @@ -108,6 +118,17 @@ export default function PublicRoundtableSharePage({ refetchOnWindowFocus: false, }); + // 大屏多页:用与应用内一致的 React 大屏(DashboardApp)渲染,而非自包含 HTML 模板。 + // 把存储的结构化数据(report-json)解析并 adapt 成 DashboardApp 的入参;与 + // Step3DashboardMode 的「应用渲染」、DashboardStandalonePage 的「新窗口打开」同源。 + const dashboardData = useMemo(() => { + const json = data?.dashboard_json; + if (!json) return undefined; + const parsed = parseReportJson(json); + if (!parsed) return undefined; + return adaptReportData(parsed, data?.generated_at ?? ""); + }, [data?.dashboard_json, data?.generated_at]); + if (isLoading) { return ( @@ -128,8 +149,24 @@ export default function PublicRoundtableSharePage({ ); } - // ── 报告页:整页沙箱 iframe 展示第 3 步结果绘制 ────────────────────────────── - if (report) { + // ── 大屏多页:整屏渲染应用内 React 大屏(DashboardApp),与应用渲染/新窗口打开一致 ── + if (view === "dashboard") { + return ( + + {dashboardData ? ( + + ) : ( + + 暂无大屏多页 + 这次会商还没有生成第三步「大屏多页」的方案报告。 + + )} + + ); + } + + // ── 结果绘制(经典 HTML 看板):整页沙箱 iframe 展示自包含报告 ───────────────── + if (view === "report") { return ( @@ -161,8 +198,10 @@ export default function PublicRoundtableSharePage({ ); } - // ── 落地页:总控最终结论 + 各席位最终交付 + 查看结果绘制(新标签打开报告页)───── - const reportUrl = `${window.location.origin}${window.location.pathname}#/roundtable-share/${shareCode}/report`; + // ── 落地页:结果绘制/大屏多页入口 + 方案总结报告 + 总控最终结论 + 各席位最终交付 ── + const shareBase = `${window.location.origin}${window.location.pathname}#/roundtable-share/${shareCode}`; + const reportUrl = `${shareBase}/report`; + const dashboardUrl = `${shareBase}/dashboard`; return ( @@ -182,23 +221,39 @@ export default function PublicRoundtableSharePage({ - {/* 1. 结果绘制 —— 新标签打开报告页 */} + {/* 1. 结果绘制 / 大屏多页 —— 新标签打开整页报告 */} 结果绘制 - {data.has_report ? ( - - - 查看结果绘制 - - + {data.has_report || data.has_dashboard ? ( + + {data.has_report && ( + + + 查看结果绘制 + + + )} + {data.has_dashboard && ( + + + 查看大屏多页 + + + )} + ) : ( 这次会商还没有生成第三步「结果绘制」的方案报告。 @@ -206,7 +261,22 @@ export default function PublicRoundtableSharePage({ )} - {/* 2. 总控最终结论 */} + {/* 2. 方案总结报告(Markdown) */} + {data.has_summary_report && ( + + + + 方案总结报告 + + + + {data.summary_report_md} + + + + )} + + {/* 3. 总控最终结论 */} @@ -223,7 +293,7 @@ export default function PublicRoundtableSharePage({ )} - {/* 3. 各席位最终交付 */} + {/* 4. 各席位最终交付 */} diff --git a/frontend-web/src/pages/light-app-management.css b/frontend-web/src/pages/light-app-management.css new file mode 100644 index 00000000..bc3eafc0 --- /dev/null +++ b/frontend-web/src/pages/light-app-management.css @@ -0,0 +1,570 @@ +@import "../../docs/ui规范-深蓝/tokens.css"; +@import "./light-app-pages.css"; + +/** + * 应用管理页 —— 深蓝色主题适配(《深蓝色 UI 规范》) + * 浅色样式仍由组件内 Tailwind 承担;.dark 下本文件覆盖为 ZS-004 / ZS-002 令牌。 + */ +.dark .light-app-management-page { + color: var(--color-text); +} + +/* ── 页头 / 文案 ─────────────────────────────────────────────────────── */ +.dark .light-app-management-page .lam-title { + color: var(--color-text-strong); +} + +.dark .light-app-management-page .lam-desc { + color: var(--color-text-secondary); +} + +.dark .light-app-management-page .lam-desc code { + background: rgba(22, 119, 255, 0.12); + color: var(--color-link); +} + +.dark .light-app-management-page .lam-primary-btn { + background: var(--color-primary); + box-shadow: none; +} + +.dark .light-app-management-page .lam-primary-btn:hover { + background: var(--color-primary-hover); +} + +.dark .light-app-management-page .lam-primary-btn:active { + background: var(--color-primary-active); +} + +/* ── 工具条 / 搜索 / 视图切换 ─────────────────────────────────────────── */ +.dark .light-app-management-page .lam-toolbar { + background: var(--color-bg-card); + border: 1px solid var(--color-border); + box-shadow: var(--shadow-1); +} + +.dark .light-app-management-page .lam-search-input { + background: var(--color-bg-page); + border-color: var(--color-border); + color: var(--color-text); +} + +.dark .light-app-management-page .lam-search-input::placeholder { + color: var(--color-text-disabled); +} + +.dark .light-app-management-page .lam-search-input:hover { + background: var(--color-bg-card); + border-color: var(--color-link); +} + +.dark .light-app-management-page .lam-search-input:focus { + background: var(--color-bg-card); + border-color: var(--color-primary); + box-shadow: 0 0 0 2px rgba(22, 119, 255, 0.16); + outline: none; +} + +.dark .light-app-management-page .lam-view-toggle { + background: var(--color-bg-page); + border-color: var(--color-border); +} + +.dark .light-app-management-page .lam-view-toggle-btn.is-active { + background: var(--color-bg-card); + color: var(--color-link); + box-shadow: var(--shadow-1); +} + +.dark .light-app-management-page .lam-view-toggle-btn:not(.is-active) { + color: var(--color-text-secondary); +} + +.dark .light-app-management-page .lam-view-toggle-btn:not(.is-active):hover { + color: var(--color-text); +} + +.dark .light-app-management-page .lam-count { + color: var(--color-text-secondary); +} + +.dark .light-app-management-page .lam-count strong { + color: var(--color-link); +} + +/* ── 表格(§28)──────────────────────────────────────────────────────── */ +.dark .light-app-management-page .lam-table-wrap { + background: var(--color-bg-page); + border: 1px solid var(--color-border); + box-shadow: var(--shadow-1); +} + +.dark .light-app-management-page .lam-table thead tr { + background: var(--color-bg-card); + border-color: var(--color-border); + color: var(--color-text); +} + +.dark .light-app-management-page .lam-table tbody { + border-color: var(--color-border); +} + +.dark .light-app-management-page .lam-table tbody tr { + border-color: var(--color-border); +} + +.dark .light-app-management-page .lam-table tbody tr:hover { + background: rgba(22, 119, 255, 0.08); +} + +.dark .light-app-management-page .lam-row-name { + color: var(--color-text); +} + +.dark .light-app-management-page .lam-table tbody tr:hover .lam-row-name { + color: var(--color-link); +} + +.dark .light-app-management-page .lam-tag-window { + border-color: rgba(245, 34, 45, 0.35); + background: var(--func-error-bg); + color: #ff7875; +} + +.dark .light-app-management-page .lam-tag-iframe { + border-color: rgba(22, 119, 255, 0.35); + background: var(--func-info-bg); + color: var(--color-link); +} + +.dark .light-app-management-page .lam-url { + color: var(--color-text-secondary); +} + +.dark .light-app-management-page .lam-table tbody tr:hover .lam-url { + color: var(--color-text); +} + +.dark .light-app-management-page .lam-url a { + color: inherit; +} + +.dark .light-app-management-page .lam-url a:hover { + color: var(--color-link); +} + +.dark .light-app-management-page .lam-mount-tag { + background: var(--func-info-bg); + color: var(--color-link); +} + +.dark .light-app-management-page .lam-mount-dot { + background: var(--color-primary); +} + +.dark .light-app-management-page .lam-param-btn { + border-color: var(--color-border); + background: var(--color-bg-page); + color: var(--color-text-secondary); +} + +.dark .light-app-management-page .lam-param-btn:hover { + background: rgba(22, 119, 255, 0.08); + color: var(--color-text); +} + +.dark .light-app-management-page .lam-param-popover { + border-color: var(--color-border); + background: var(--color-bg-card); + box-shadow: var(--shadow-3); +} + +.dark .light-app-management-page .lam-param-popover-head { + border-color: var(--color-border); + color: var(--color-text-secondary); +} + +.dark .light-app-management-page .lam-param-popover-close { + color: var(--color-link); +} + +.dark .light-app-management-page .lam-param-item { + border-color: var(--color-border); + background: var(--color-bg-page); +} + +.dark .light-app-management-page .lam-param-key { + color: #ff7875; +} + +.dark .light-app-management-page .lam-param-val { + color: var(--color-text-secondary); +} + +.dark .light-app-management-page .lam-status-on { + color: var(--func-success); +} + +.dark .light-app-management-page .lam-status-on-dot { + background: var(--func-success); +} + +.dark .light-app-management-page .lam-status-on-ping { + background: var(--func-success); +} + +.dark .light-app-management-page .lam-status-off { + color: var(--color-text-disabled); +} + +.dark .light-app-management-page .lam-status-off-dot { + background: var(--color-text-disabled); +} + +.dark .light-app-management-page .lam-icon-btn { + color: var(--color-text-secondary); +} + +.dark .light-app-management-page .lam-icon-btn:hover { + background: rgba(22, 119, 255, 0.08); + color: var(--color-link); +} + +.dark .light-app-management-page .lam-icon-btn-danger:hover { + background: var(--func-error-bg); + color: var(--func-error); +} + +.dark .light-app-management-page .lam-empty-icon { + background: var(--color-bg-page); + border: 1px solid var(--color-border); + color: var(--color-text-disabled); +} + +.dark .light-app-management-page .lam-empty-title { + color: var(--color-text-secondary); +} + +.dark .light-app-management-page .lam-empty-sub { + color: var(--color-text-disabled); +} + +/* ── 卡片视图(§22 / §43)────────────────────────────────────────────── */ +.dark .light-app-management-page .lam-card { + background: var(--color-bg-card); + border: 1px solid var(--color-border); + box-shadow: var(--shadow-1); +} + +.dark .light-app-management-page .lam-card:hover { + box-shadow: var(--shadow-2); +} + +.dark .light-app-management-page .lam-card-title { + color: var(--color-text-strong); +} + +.dark .light-app-management-page .lam-card:hover .lam-card-title { + color: var(--color-link); +} + +.dark .light-app-management-page .lam-card-desc { + color: var(--color-text-secondary); +} + +.dark .light-app-management-page .lam-card-url-box { + background: var(--color-bg-page); +} + +.dark .light-app-management-page .lam-card-param { + border-color: var(--color-border); + background: var(--color-bg-page); + color: var(--color-text-secondary); +} + +.dark .light-app-management-page .lam-card-mount { + background: var(--func-info-bg); + color: var(--color-link); +} + +.dark .light-app-management-page .lam-card-footer { + border-color: var(--color-border); + background: rgba(12, 24, 54, 0.5); +} + +/* ── 底部提示(§33 警告)────────────────────────────────────────────── */ +.dark .light-app-management-page .lam-tip-box { + background: var(--func-warning-bg); + border: 1px solid rgba(250, 173, 20, 0.25); + box-shadow: none; +} + +.dark .light-app-management-page .lam-tip-head { + border-color: rgba(250, 173, 20, 0.2); + color: var(--func-warning); +} + +.dark .light-app-management-page .lam-tip-list { + color: var(--color-text-secondary); +} + +.dark .light-app-management-page .lam-tip-list strong { + color: var(--color-text); +} + +.dark .light-app-management-page .lam-tip-bullet { + color: var(--func-warning); +} + +/* ── 弹窗(§21 Modal)────────────────────────────────────────────────── */ +.dark .light-app-management-page .lam-dialog-overlay { + background: rgba(6, 14, 36, 0.6); +} + +.dark .light-app-management-page .lam-dialog { + border-color: var(--color-border); + background: linear-gradient(180deg, var(--color-bg-card), var(--color-bg-page)); + box-shadow: var(--shadow-3); + color: var(--color-text); +} + +.dark .light-app-management-page .lam-dialog-header { + border-color: var(--color-border); + border-bottom: none; + padding-bottom: 0; +} + +.dark .light-app-management-page .lam-dialog-header::after { + content: ""; + display: block; + height: 1px; + margin-top: 16px; + margin-bottom: 0; + background: var(--gradient-title-line); +} + +.dark .light-app-management-page .lam-dialog-icon { + background: var(--func-info-bg); + color: var(--color-link); +} + +.dark .light-app-management-page .lam-dialog-title { + color: var(--color-text-strong); +} + +.dark .light-app-management-page .lam-dialog-close { + color: var(--color-text-secondary); +} + +.dark .light-app-management-page .lam-dialog-close:hover { + background: rgba(22, 119, 255, 0.08); + color: var(--color-link); +} + +.dark .light-app-management-page .lam-form-label { + color: var(--color-text); +} + +.dark .light-app-management-page .lam-form-label .lam-required { + color: var(--func-error); +} + +.dark .light-app-management-page .lam-form-label svg { + color: var(--color-text-secondary); +} + +.dark .light-app-management-page .lam-form-hint { + color: var(--color-text-secondary); +} + +.dark .light-app-management-page .lam-dialog .lam-input, +.dark .light-app-management-page .lam-dialog input[type="text"] { + background: var(--color-bg-page); + border-color: var(--color-border); + color: var(--color-text); +} + +.dark .light-app-management-page .lam-dialog .lam-input::placeholder, +.dark .light-app-management-page .lam-dialog input[type="text"]::placeholder { + color: var(--color-text-disabled); +} + +.dark .light-app-management-page .lam-dialog .lam-input:focus, +.dark .light-app-management-page .lam-dialog input[type="text"]:focus { + border-color: var(--color-primary); + box-shadow: 0 0 0 2px rgba(22, 119, 255, 0.16); + outline: none; +} + +.dark .light-app-management-page .lam-param-panel { + border-color: var(--color-border); + background: var(--color-bg-page); +} + +.dark .light-app-management-page .lam-param-row { + border-color: var(--color-border); + background: var(--color-bg-card); +} + +.dark .light-app-management-page .lam-param-theme-box { + background: var(--color-bg-page); +} + +.dark .light-app-management-page .lam-param-empty { + border-color: var(--color-border); + background: var(--color-bg-page); + color: var(--color-text-secondary); +} + +.dark .light-app-management-page .lam-add-param-btn { + border-color: var(--color-border-strong); + background: var(--color-bg-card); + color: var(--color-text); +} + +.dark .light-app-management-page .lam-add-param-btn:hover { + border-color: var(--color-primary); + color: var(--color-link); +} + +.dark .light-app-management-page .lam-dialog-footer { + border-color: var(--color-border); +} + +.dark .light-app-management-page .lam-dialog-cancel { + border-color: var(--color-border-strong); + background: transparent; + color: var(--color-text); +} + +.dark .light-app-management-page .lam-dialog-cancel:hover { + border-color: var(--color-link); + color: var(--color-link); +} + +.dark .light-app-management-page .lam-dialog-save { + background: var(--color-primary); + box-shadow: none; +} + +.dark .light-app-management-page .lam-dialog-save:hover { + background: var(--color-primary-hover); +} + +.dark .light-app-management-page .lam-dialog-save:active { + background: var(--color-primary-active); +} + +.dark .light-app-management-page .lam-switch-on { + background: var(--color-primary); +} + +.dark .light-app-management-page .lam-switch-off { + background: var(--color-text-disabled); +} + +/* ── TDesign Select(§15)──────────────────────────────────────────── */ +.dark .light-app-management-page .t-select .t-input, +.dark .light-app-management-page .t-select .t-input__wrap { + background: var(--color-bg-page); + border-color: var(--color-border); + border-radius: var(--radius-input); +} + +.dark .light-app-management-page .t-select .t-input__inner { + color: var(--color-text); +} + +.dark .light-app-management-page .t-select .t-input__inner::placeholder { + color: var(--color-text-disabled); +} + +.dark .light-app-management-page .t-select:hover .t-input, +.dark .light-app-management-page .t-select:hover .t-input__wrap { + border-color: var(--color-link); +} + +.dark .light-app-management-page .t-select.t-is-focused .t-input, +.dark .light-app-management-page .t-select.t-is-focused .t-input__wrap { + border-color: var(--color-primary); + box-shadow: 0 0 0 2px rgba(22, 119, 255, 0.16); +} + +.dark .light-app-management-page .t-select .t-fake-arrow { + color: var(--color-text-secondary); +} + +.dark .light-app-management-page .t-select__dropdown { + background: var(--color-bg-card); + border-color: var(--color-border); + box-shadow: var(--shadow-2); +} + +.dark .light-app-management-page .t-select-option { + color: var(--color-text); +} + +.dark .light-app-management-page .t-select-option:hover { + background: rgba(22, 119, 255, 0.08); +} + +.dark .light-app-management-page .t-select-option.t-is-selected { + background: rgba(22, 119, 255, 0.16); + color: var(--color-link); +} + +/* shadcn Input / Textarea in dialog */ +.dark .light-app-management-page .lam-dialog [data-slot="input"], +.dark .light-app-management-page .lam-dialog textarea { + background: var(--color-bg-page); + border-color: var(--color-border); + color: var(--color-text); +} + +.dark .light-app-management-page .lam-dialog [data-slot="input"]:focus-visible, +.dark .light-app-management-page .lam-dialog textarea:focus-visible { + border-color: var(--color-primary); + box-shadow: 0 0 0 2px rgba(22, 119, 255, 0.16); +} + +.dark .light-app-management-page .lam-param-theme-box span { + color: var(--color-text-secondary); +} + +/* TDesign 下拉挂载到 body 时的深蓝样式 */ +.dark .lam-select-popup.t-popup { + background: var(--color-bg-card); + border-color: var(--color-border); + box-shadow: var(--shadow-2); +} + +.dark .lam-select-popup .t-select-option { + color: var(--color-text); +} + +.dark .lam-select-popup .t-select-option:hover { + background: rgba(22, 119, 255, 0.08); +} + +.dark .lam-select-popup .t-select-option.t-is-selected { + background: rgba(22, 119, 255, 0.16); + color: var(--color-link); +} + +.dark .light-app-management-page .lam-search-input + button, +.dark .light-app-management-page .lam-toolbar .absolute.text-neutral-400 { + color: var(--color-text-secondary); +} + +.dark .light-app-management-page .lam-table .text-neutral-300 { + color: var(--color-text-disabled); +} + +.dark .light-app-management-page .lam-card-url-box button { + color: var(--color-text-secondary); +} + +.dark .light-app-management-page .lam-card-url-box button:hover { + background: rgba(22, 119, 255, 0.08); + color: var(--color-link); +} diff --git a/frontend-web/src/pages/light-app-pages.css b/frontend-web/src/pages/light-app-pages.css new file mode 100644 index 00000000..459be997 --- /dev/null +++ b/frontend-web/src/pages/light-app-pages.css @@ -0,0 +1,12 @@ +@import "../../docs/ui规范-深蓝/tokens.css"; + +/** 轻应用中心列表 / 管理页 —— 深色模式共用页面底图(与 workspace 深蓝主题一致) */ +.dark .light-app-management-page, +.dark .light-app-list-page { + background-color: var(--color-bg-page); + background-image: url("../assets/img/layout-bg-theme-dark-blue.svg"); + background-size: cover; + background-position: center top; + background-repeat: no-repeat; + background-attachment: fixed; +} diff --git a/frontend-web/src/roundtable-planning/components/HighFidelityReport.tsx b/frontend-web/src/roundtable-planning/components/HighFidelityReport.tsx index 533f8ba9..f12a0bb5 100644 --- a/frontend-web/src/roundtable-planning/components/HighFidelityReport.tsx +++ b/frontend-web/src/roundtable-planning/components/HighFidelityReport.tsx @@ -1009,6 +1009,7 @@ export const HighFidelityReport: React.FC = ({ filepath={fileSandboxVirtualUrl} threadId={fileSandboxTarget.threadId} onClose={handleCloseFileSandbox} + allowVirtualMarkdownEdit /> )} diff --git a/frontend-web/src/roundtable-planning/components/Step2SandboxLayout.tsx b/frontend-web/src/roundtable-planning/components/Step2SandboxLayout.tsx index 1d69dea8..59585fa9 100644 --- a/frontend-web/src/roundtable-planning/components/Step2SandboxLayout.tsx +++ b/frontend-web/src/roundtable-planning/components/Step2SandboxLayout.tsx @@ -115,6 +115,7 @@ export function Step2SandboxLayout({ filepath={selectedArtifact} threadId={previewThreadId} onClose={artifactsFill && onArtifactsClose ? handleArtifactsClose : undefined} + allowVirtualMarkdownEdit /> ) : ( diff --git a/frontend-web/src/strategy-components/Header.tsx b/frontend-web/src/strategy-components/Header.tsx index 7e4f144f..b27f18a5 100644 --- a/frontend-web/src/strategy-components/Header.tsx +++ b/frontend-web/src/strategy-components/Header.tsx @@ -139,6 +139,10 @@ const Header = ({ collapsed }: HeaderProps) => { return email ? email.split("@")[0] : "用户"; }, [auth?.email]); const showTopNav = isDark && !collapsed && !isWujieEnv(); + // 浅色(红/蓝)顶栏:一级导航正常横排进顶栏,不做深色那种围绕基座的特殊处理。 + // 与 collapsed 解耦(折叠只影响侧边栏,一级导航始终可见);wujie 嵌入下隐藏, + // 此时一级导航回落到侧边栏(见 page-sidebar.tsx)。 + const showLightTopNav = !isDark && !isWujieEnv(); const hasActiveChild = (item: MenuItem): boolean => { if (item.path && pathname === item.path) { @@ -194,6 +198,20 @@ const Header = ({ collapsed }: HeaderProps) => { ); }; + const renderLightTopItem = (item: MenuItem) => { + const isSelected = item.path ? pathname === item.path : hasActiveChild(item); + return ( + handleNavItemClick(item)} + > + {item.label} + + ); + }; + // 右侧操作区(换肤/通知/用户)—— 深浅两套布局共用 const actionCluster = ( <> @@ -309,6 +327,27 @@ const Header = ({ collapsed }: HeaderProps) => { > + ) : showLightTopNav ? ( + <> + {/* 浅色顶栏:logo + 一级导航横排(白字落在 banner 上) */} + + + + + + + CM助手 + + + + {visibleMenuItems.map(renderLightTopItem)} + + + + + {actionCluster} + + > ) : ( <> @@ -335,6 +374,7 @@ const Header = ({ collapsed }: HeaderProps) => { > )} + {/* 三级菜单横条仅深色用;浅色三级已改到侧边栏右侧列(见 page-sidebar.tsx) */} {showTopNav && thirdSubNavItems.length ? ( diff --git a/frontend-web/src/strategy-components/lib/light-app.ts b/frontend-web/src/strategy-components/lib/light-app.ts index 481b838b..ab796bf6 100644 --- a/frontend-web/src/strategy-components/lib/light-app.ts +++ b/frontend-web/src/strategy-components/lib/light-app.ts @@ -19,11 +19,17 @@ export function mountLabel(value: string | null | undefined): string { return MOUNT_OPTIONS.find((o) => o.value === value)?.label ?? value; } -/** 登录参数可选来源;值均可在 userInfo 中取到,"other" 表示自填。 */ +/** + * 登录参数可选来源;值均可在 userInfo 中取到,"other" 表示自填。 + * accessToken / yUserId 取登录路由透传值(写入 userInfo 的同名键); + * userName / gateDepartName / typeName 取 consumer 接口 userDetailByYuserId 的返回值。 + */ export const LOGIN_SOURCE_OPTIONS: { label: string; value: string }[] = [ { label: "accessToken", value: "accessToken" }, { label: "yUserId", value: "yUserId" }, { label: "userName", value: "userName" }, + { label: "gateDepartName", value: "gateDepartName" }, + { label: "typeName", value: "typeName" }, { label: "其他(自定义值)", value: "other" }, ]; diff --git a/frontend-web/src/styles/globals.css b/frontend-web/src/styles/globals.css index 7ebd17de..3494b65c 100644 --- a/frontend-web/src/styles/globals.css +++ b/frontend-web/src/styles/globals.css @@ -932,7 +932,7 @@ body { .strategy-app-header { background-color: transparent; background-image: url("../assets/img/red-header.png"); - background-size: 100% 100%; + background-size: cover; background-repeat: no-repeat; background-position: center; } diff --git a/offline-backend-20260512/backend/CLAUDE.md b/offline-backend-20260512/backend/CLAUDE.md index 18e7823d..f6812132 100644 --- a/offline-backend-20260512/backend/CLAUDE.md +++ b/offline-backend-20260512/backend/CLAUDE.md @@ -1,759 +1,759 @@ -# CLAUDE.md -?? -This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. - -## Project Overview - -DeerFlow is a LangGraph-based AI super agent system with a full-stack architecture. The backend provides a "super agent" with sandbox execution, persistent memory, subagent delegation, and extensible tool integration - all operating in per-thread isolated environments. - -**Architecture**: -- **Gateway API** (port 8001): REST API plus embedded LangGraph-compatible agent runtime -- **Frontend** (port 3000): Next.js web interface -- **Nginx** (port 2026): Unified reverse proxy entry point -- **Provisioner** (port 8002, optional in Docker dev): Started only when sandbox is configured for provisioner/Kubernetes mode - -**Runtime**: -- `make dev`, Docker dev, and production all run the agent runtime in Gateway via `RunManager` + `run_agent()` + `StreamBridge` (`packages/harness/deerflow/runtime/`). Nginx exposes that runtime at `/api/langgraph/*` and rewrites it to Gateway's native `/api/*` routers. - -**Project Structure**: -``` -deer-flow/ -??? Makefile # Root commands (check, install, dev, stop) -??? config.yaml # Main application configuration -??? extensions_config.json # MCP servers and skills configuration -??? backend/ # Backend application (this directory) -? ??? Makefile # Backend-only commands (dev, gateway, lint) -? ??? langgraph.json # LangGraph Studio graph configuration -? ??? packages/ -? ? ??? harness/ # deerflow-harness package (import: deerflow.*) -? ? ??? pyproject.toml -? ? ??? deerflow/ -? ? ??? agents/ # LangGraph agent system -? ? ? ??? lead_agent/ # Main agent (factory + system prompt) -? ? ? ??? middlewares/ # 10 middleware components -? ? ? ??? memory/ # Memory V2: provider/manager/builtin/hindsight -? ? ? ??? thread_state.py # ThreadState schema -? ? ??? sandbox/ # Sandbox execution system -? ? ? ??? local/ # Local filesystem provider -? ? ? ??? sandbox.py # Abstract Sandbox interface -? ? ? ??? tools.py # bash, ls, read/write/str_replace -? ? ? ??? middleware.py # Sandbox lifecycle management -? ? ??? subagents/ # Subagent delegation system -? ? ? ??? builtins/ # general-purpose, bash agents -? ? ? ??? executor.py # Background execution engine -? ? ? ??? registry.py # Agent registry -? ? ??? tools/builtins/ # Built-in tools (present_files, ask_clarification, view_image) -? ? ??? mcp/ # MCP integration (tools, cache, client) -? ? ??? models/ # Model factory with thinking/vision support -? ? ??? skills/ # Skills discovery, loading, parsing -? ? ??? config/ # Configuration system (app, model, sandbox, tool, etc.) -? ? ??? community/ # Community tools (tavily, jina_ai, firecrawl, image_search, aio_sandbox) -? ? ??? reflection/ # Dynamic module loading (resolve_variable, resolve_class) -? ? ??? utils/ # Utilities (network, readability) -? ? ??? client.py # Embedded Python client (DeerFlowClient) -? ??? app/ # Application layer (import: app.*) -? ? ??? gateway/ # FastAPI Gateway API -? ? ? ??? app.py # FastAPI application -? ? ? ??? routers/ # FastAPI route modules (models, mcp, memory, skills, uploads, threads, artifacts, agents, suggestions, channels) -? ? ??? channels/ # IM platform integrations -? ??? tests/ # Test suite -? ??? docs/ # Documentation -??? frontend/ # Next.js frontend application -??? skills/ # Agent skills directory - ??? public/ # Public skills (committed) - ??? custom/ # Custom skills (gitignored) -``` - -## Important Development Guidelines - -### Documentation Update Policy -**CRITICAL: Always update README.md and CLAUDE.md after every code change** - -When making code changes, you MUST update the relevant documentation: -- Update `README.md` for user-facing changes (features, setup, usage instructions) -- Update `CLAUDE.md` for development changes (architecture, commands, workflows, internal systems) -- Keep documentation synchronized with the codebase at all times -- Ensure accuracy and timeliness of all documentation - -## Commands - -**Root directory** (for full application): -```bash -make check # Check system requirements -make install # Install all dependencies (frontend + backend) -make dev # Start all services (Gateway + Frontend + Nginx), with config.yaml preflight -make start # Start production services locally -make stop # Stop all services -``` - -**Backend directory** (for backend development only): -```bash -make install # Install backend dependencies -make dev # Run Gateway API with reload (port 8001) -make gateway # Run Gateway API only (port 8001) -make test # Run all backend tests -make lint # Lint with ruff -make format # Format code with ruff -``` - -Regression tests related to Docker/provisioner behavior: -- `tests/test_docker_sandbox_mode_detection.py` (mode detection from `config.yaml`) -- `tests/test_provisioner_kubeconfig.py` (kubeconfig file/directory handling) - -Boundary check (harness ? app import firewall): -- `tests/test_harness_boundary.py` ? ensures `packages/harness/deerflow/` never imports from `app.*` - -CI runs these regression tests for every pull request via [.github/workflows/backend-unit-tests.yml](../.github/workflows/backend-unit-tests.yml). - -## Architecture - -### Harness / App Split - -The backend is split into two layers with a strict dependency direction: - -- **Harness** (`packages/harness/deerflow/`): Publishable agent framework package (`deerflow-harness`). Import prefix: `deerflow.*`. Contains agent orchestration, tools, sandbox, models, MCP, skills, config ? everything needed to build and run agents. -- **App** (`app/`): Unpublished application code. Import prefix: `app.*`. Contains the FastAPI Gateway API and IM channel integrations (Feishu, Slack, Telegram, DingTalk). - -**Dependency rule**: App imports deerflow, but deerflow never imports app. This boundary is enforced by `tests/test_harness_boundary.py` which runs in CI. - -**Import conventions**: -```python -# Harness internal -from deerflow.agents import make_lead_agent -from deerflow.models import create_chat_model - -# App internal -from app.gateway.app import app -from app.channels.service import start_channel_service - -# App ? Harness (allowed) -from deerflow.config import get_app_config - -# Harness ? App (FORBIDDEN ? enforced by test_harness_boundary.py) -# from app.gateway.routers.uploads import ... # ? will fail CI -``` - -### Agent System - -**Lead Agent** (`packages/harness/deerflow/agents/lead_agent/agent.py`): -- Entry point: `make_lead_agent(config: RunnableConfig)` registered in `langgraph.json` -- Dynamic model selection via `create_chat_model()` with thinking/vision support -- Tools loaded via `get_available_tools()` - combines sandbox, built-in, MCP, community, and subagent tools -- System prompt generated by `apply_prompt_template()` with skills, memory, and subagent instructions - -**ThreadState** (`packages/harness/deerflow/agents/thread_state.py`): -- Extends `AgentState` with: `sandbox`, `thread_data`, `title`, `artifacts`, `todos`, `uploaded_files`, `viewed_images` -- Uses custom reducers: `merge_artifacts` (deduplicate), `merge_viewed_images` (merge/clear) - -**Runtime Configuration** (via `config.configurable`): -- `thinking_enabled` - Enable model's extended thinking -- `model_name` - Select specific LLM model -- `is_plan_mode` - Enable TodoList middleware -- `subagent_enabled` - Enable task delegation tool - -### Middleware Chain - -Lead-agent middlewares are assembled in strict append order across `packages/harness/deerflow/agents/middlewares/tool_error_handling_middleware.py` (`build_lead_runtime_middlewares`) and `packages/harness/deerflow/agents/lead_agent/agent.py` (`_build_middlewares`): - -1. **ThreadDataMiddleware** - Creates per-thread directories under the user's isolation scope (`backend/.deer-flow/users/{user_id}/threads/{thread_id}/user-data/{workspace,uploads,outputs}`); resolves `user_id` via `get_effective_user_id()` (falls back to `"default"` in no-auth mode); Web UI thread deletion now follows LangGraph thread removal with Gateway cleanup of the local thread directory -2. **UploadsMiddleware** - Tracks and injects newly uploaded files into conversation -3. **SandboxMiddleware** - Acquires sandbox, stores `sandbox_id` in state -4. **DanglingToolCallMiddleware** - Injects placeholder ToolMessages for AIMessage tool_calls that lack responses (e.g., due to user interruption), including raw provider tool-call payloads preserved only in `additional_kwargs["tool_calls"]` -5. **LLMErrorHandlingMiddleware** - Normalizes provider/model invocation failures into recoverable assistant-facing errors before later middleware/tool stages run -6. **GuardrailMiddleware** - Pre-tool-call authorization via pluggable `GuardrailProvider` protocol (optional, if `guardrails.enabled` in config). Evaluates each tool call and returns error ToolMessage on deny. Three provider options: built-in `AllowlistProvider` (zero deps), OAP policy providers (e.g. `aport-agent-guardrails`), or custom providers. See [docs/GUARDRAILS.md](docs/GUARDRAILS.md) for setup, usage, and how to implement a provider. -7. **SandboxAuditMiddleware** - Audits sandboxed shell/file operations for security logging before tool execution continues -8. **ToolErrorHandlingMiddleware** - Converts tool exceptions into error `ToolMessage`s so the run can continue instead of aborting -9. **SummarizationMiddleware** (`DeerFlowSummarizationMiddleware`) - Context reduction when approaching token limits (optional, if enabled). **Non-destructive**: overrides the stock `before_model` (which persists a `RemoveMessage(REMOVE_ALL_MESSAGES)`) to a no-op and instead compresses **transiently** in `wrap_model_call` ? the model request is replaced with `[summary, *recent]` while the full original conversation stays in state (so the frontend keeps rendering the user's real Q&A). The `summary` message is `hide_from_ui` and never persisted; summaries are cached per-thread (bounded, sticky boundary) to avoid an LLM call on every model invocation. Tests: `tests/test_summarization_middleware.py` -10. **TodoListMiddleware** - Task tracking with `write_todos` tool (optional, if plan_mode) -11. **TokenUsageMiddleware** - Records token usage metrics when token tracking is enabled (optional) -12. **TitleMiddleware** - Two-phase auto-title (never shows "untitled"): `before_model` sets an instant question-derived *provisional* title the moment the first user message arrives (no LLM, zero latency), then `after_model` asynchronously generates the final LLM title and replaces it (tracked via the `title_provisional` state flag). Normalizes structured message content before prompting the title model -13. **MemoryMiddleware** - Memory V2: `wrap_model_call` injects Hindsight recall transiently; `after_agent` auto-retains the turn to Hindsight -14. **ViewImageMiddleware** - Injects base64 image data before LLM call (conditional on vision support) -15. **DeferredToolFilterMiddleware** - Hides deferred tool schemas from the bound model until tool search is enabled (optional) -16. **SubagentLimitMiddleware** - Truncates excess `task` tool calls from model response to enforce `MAX_CONCURRENT_SUBAGENTS` limit (optional, if `subagent_enabled`) -17. **LoopDetectionMiddleware** - Detects repeated tool-call loops; hard-stop responses clear both structured `tool_calls` and raw provider tool-call metadata before forcing a final text answer -18. **ClarificationMiddleware** - Intercepts `ask_clarification` tool calls, interrupts via `Command(goto=END)` (must be last) -8a. **ToolMetricsMiddleware** - Appended immediately after `ToolErrorHandlingMiddleware` in `_build_runtime_middlewares`, so it sits *deepest* in the `wrap_tool_call` chain. Records one `tool_call_metrics` row per tool/skill/MCP invocation ? timing duration and bucketing failures into a coarse `error_category` (rate_limited / ip_blocked / auth_error / timeout / network_error / not_found / empty_result / tool_error). It catches both raised exceptions (before #8 converts them) and *self-handled* errors returned as error `ToolMessage`s (search tools that swallow HTTP 429/403). It also captures the targeted Agent Skill into `tool_call_metrics.skill_name`: the lead agent invokes a skill by `read_file`-ing its `/mnt/skills///SKILL.md` path (per the `` prompt block ? there is no dedicated "use skill" tool), so `skill_name` is recovered by scanning every tool call's string arguments for the `skills/{public,custom}/{name}/` path shape (`_SKILL_PATH_RE`), plus the explicit `name` argument of `skill_view`/`skill_manage`/`skill_archive`. `skill_view`-specific success detection handles its swallowed failures (`Skill 'x' not found.` returned as a plain string rather than raised). Regression coverage: `tests/test_tool_metrics_middleware.py`. Writes are best-effort fire-and-forget. **Per-run skill merging**: `SqlToolMetricsStore.record` upserts skill rows by `(run_id, skill_name)` instead of inserting one row per call ? a skill touched by several tool calls / retries inside one run collapses to a single row. Any success in the run wins (a skill that failed then finally succeeded is recorded as one `success`); a skill that only ever failed yields one `error` row whose `error_message` accumulates every attempt's message; durations are summed. Non-skill tool calls, and skill calls with no `run_id`, keep one row each. The read?merge?write is guarded by a per-event-loop `asyncio.Lock`. Surfaced by the admin `GET /api/admin/tool-metrics` router (which has a `kind=skill` filter and a per-skill `/summary` breakdown). -9. **PromptPrefixMiddleware** - Injects the admin-configured prompt prefix into the model request only. The prefix is tagged onto the first human message's `additional_kwargs['prompt_prefix']` by the Gateway (`app.gateway.services`) and prepended to content here at call time, so the persisted/displayed message stays prefix-free -10. **SummarizationMiddleware** (`DeerFlowSummarizationMiddleware`) - Context reduction when approaching token limits (optional, if enabled). **Non-destructive**: overrides the stock `before_model` (which persists a `RemoveMessage(REMOVE_ALL_MESSAGES)`) to a no-op and instead compresses **transiently** in `wrap_model_call` ? the model request is replaced with `[summary, *recent]` while the full original conversation stays in state (so the frontend keeps rendering the user's real Q&A). The `summary` message is `hide_from_ui` and never persisted; summaries are cached per-thread (bounded, sticky boundary) to avoid an LLM call on every model invocation. Tests: `tests/test_summarization_middleware.py` -11. **TodoListMiddleware** - Task tracking with `write_todos` tool (optional, if plan_mode) -12. **TokenUsageMiddleware** - Records token usage metrics when token tracking is enabled (optional) -13. **TitleMiddleware** - Two-phase auto-title (never shows "untitled"): `before_model` sets an instant question-derived *provisional* title the moment the first user message arrives (no LLM, zero latency), then `after_model` asynchronously generates the final LLM title and replaces it (tracked via the `title_provisional` state flag). Normalizes structured message content before prompting the title model -14. **MemoryMiddleware** - Queues conversations for async memory update (filters to user + final AI responses) -15. **ViewImageMiddleware** - Injects base64 image data before LLM call (conditional on vision support) -16. **DeferredToolFilterMiddleware** - Hides deferred tool schemas from the bound model until tool search is enabled (optional) -17. **SubagentLimitMiddleware** - Truncates excess `task` tool calls from model response to enforce `MAX_CONCURRENT_SUBAGENTS` limit (optional, if `subagent_enabled`) -18. **LoopDetectionMiddleware** - Detects repeated tool-call loops; hard-stop responses clear both structured `tool_calls` and raw provider tool-call metadata before forcing a final text answer -19. **ClarificationMiddleware** - Intercepts `ask_clarification` tool calls, interrupts via `Command(goto=END)` (must be last) - -### Configuration System - -**Main Configuration** (`config.yaml`): - -Setup: Copy `config.example.yaml` to `config.yaml` in the **project root** directory. - -**Config Versioning**: `config.example.yaml` has a `config_version` field. On startup, `AppConfig.from_file()` compares user version vs example version and emits a warning if outdated. Missing `config_version` = version 0. Run `make config-upgrade` to auto-merge missing fields. When changing the config schema, bump `config_version` in `config.example.yaml`. - -**Config Caching**: `get_app_config()` caches the parsed config, but automatically reloads it when the resolved config path changes or the file's mtime increases. This keeps Gateway and LangGraph reads aligned with `config.yaml` edits without requiring a manual process restart. - -Configuration priority: -1. Explicit `config_path` argument -2. `DEER_FLOW_CONFIG_PATH` environment variable -3. `config.yaml` in current directory (backend/) -4. `config.yaml` in parent directory (project root - **recommended location**) - -Config values starting with `$` are resolved as environment variables (e.g., `$OPENAI_API_KEY`). -`ModelConfig` also declares `use_responses_api` and `output_version` so OpenAI `/v1/responses` can be enabled explicitly while still using `langchain_openai:ChatOpenAI`. - -**Extensions Configuration** (`extensions_config.json`): - -MCP servers and skills are configured together in `extensions_config.json` in project root: - -Configuration priority: -1. Explicit `config_path` argument -2. `DEER_FLOW_EXTENSIONS_CONFIG_PATH` environment variable -3. `extensions_config.json` in current directory (backend/) -4. `extensions_config.json` in parent directory (project root - **recommended location**) - -### Gateway API (`app/gateway/`) - -FastAPI application on port 8001 with health check at `GET /health`. Set `GATEWAY_ENABLE_DOCS=false` to disable `/docs`, `/redoc`, and `/openapi.json` in production (default: enabled). - -**Routers**: - -| Router | Endpoints | -|--------|-----------| -| **Models** (`/api/models`) | `GET /` - list models; `GET /{name}` - model details | -| **MCP** (`/api/mcp`) | `GET /config` - get config; `PUT /config` - update config (saves to extensions_config.json) | -| **Skills** (`/api/skills`) | `GET /` - list skills; `GET /{name}` - details; `PUT /{name}` - update enabled; `POST /install` - install from .skill archive (accepts standard optional frontmatter like `version`, `author`, `compatibility`) | -| **Memory V2** (`/api/memory/v2`) | `GET /` - builtin data (USER.md/MEMORY.md); `POST/PUT/DELETE /entries` - add/replace/remove entry; `GET/PATCH/DELETE /config` - effective config + per-user overrides; `POST /search` - Hindsight recall/reflect proxy | -| **Agents** (`/api/agents`) | `GET /` - list built-in, owned, and published agents; `POST /` - create user-owned agent; `GET/PUT/DELETE /{id}` - read/update/delete by stable agent id; `GET /check` - validate non-empty display name (duplicates allowed) | -| **Skills** (`/api/skills`) | `GET /` - list skills (optional `search` fuzzy-name + `tag_id` filters; each item carries `tags`); `GET /{name}` - details; `PUT /{name}` - update enabled; `POST /install` - install from .skill archive (accepts standard optional frontmatter like `version`, `author`, `compatibility`) | -| **Skills** (`/api/skills`) | `GET /` - list skills (optional `search` fuzzy-name + `tag_id` filters; each item carries `tags` + owner-authored `detail`); `GET /{name}` - details; `PUT /{name}` - update enabled; `PUT /{name}/detail` - set the long-form `detail` text (custom skill: owner or admin; built-in/public: admin only ? materializes a `skills` row on demand); `POST /install` - install from .skill archive (accepts standard optional frontmatter like `version`, `author`, `compatibility`) | -| **Skills** (`/api/skills`) | `GET /` - list skills (optional `search` fuzzy-name + `tag_id` filters; each item carries `tags` + owner-authored `detail`); `GET /{name}` - details; `PUT /{name}` - update enabled; `PUT /{name}/detail` - set the long-form `detail` text (custom skill: owner or admin; built-in/public: admin only ? materializes a `skills` row on demand); `GET /{name}/content` - raw SKILL.md source, restricted to an admin or the publisher (custom-skill owner); other users get 403 and see only description + detail; `POST /install` - install from .skill archive (accepts standard optional frontmatter like `version`, `author`, `compatibility`) | -| **Skills** (`/api/skills`) | `GET /` - list skills (optional `search` fuzzy-name + repeatable `tag_ids` multi-select filter, OR-matched; each item carries `tags` + owner-authored `detail`); `GET /{name}` - details; `PUT /{name}` - update enabled; `PUT /{name}/detail` - set the long-form `detail` text (custom skill: owner or admin; built-in/public: admin only ? materializes a `skills` row on demand); `GET /{name}/content` - raw SKILL.md source, restricted to an admin or the publisher (custom-skill owner); other users get 403 and see only description + detail; `POST /install` - install from .skill archive (accepts standard optional frontmatter like `version`, `author`, `compatibility`) | -| **Memory** (`/api/memory`) | `GET /` - memory data; `POST /reload` - force reload; `GET /config` - config; `GET /status` - config + data | -| **Agents** (`/api/agents`) | `GET /` - list built-in, owned, and published agents (optional `search` + repeatable `tag_ids` multi-select filter, OR-matched; each item carries `tags` and `featured_order`); `POST /` - create user-owned agent; `GET/PUT/DELETE /{id}` - read/update/delete by stable agent id; `GET /check` - validate non-empty display name (duplicates allowed); `GET /selector` - homepage agent picker (single admin-curated list, visibility-filtered, sorted by `featured_order` asc); `PUT /{id}/featured` - **admin** sets/clears the homepage `featured_order` (must be `published=true` to feature; passing `order: null` clears unconditionally); `PUT /featured/order` - **admin** bulk-reorders the homepage list (body `{agent_ids: [...]}` ? indices become 0..N-1) | -| **Tags** (`/api/tags`) | `GET /` - list tags (optional `tag_type` filter; open to all); `POST /` - create tag (admin); `PUT /{id}` / `DELETE /{id}` - update/delete tag (admin); `GET /assignments/{target_type}/{target_id}` - tags on a resource; `PUT /assignments/{target_type}/{target_id}` - replace a resource's tags (admin). Tags are typed `agent`/`skill`/`scheduled_task` and may only tag resources of their own type. | -| **Uploads** (`/api/threads/{id}/uploads`) | `POST /` - upload files (auto-converts PDF/PPT/Excel/Word); `GET /list` - list; `DELETE /{filename}` - delete | -| **Threads** (`/api/threads/{id}`) | `DELETE /` - remove DeerFlow-managed local thread data after LangGraph thread deletion; unexpected failures are logged server-side and return a generic 500 detail | -| **Threads search** (`POST /threads/search`, LangGraph-compat) | Conversation listing. **System threads are excluded by default**: rows whose `metadata.system` is truthy (scheduler runs, roundtable workers) never crowd the user's list; a caller whose `metadata` filter explicitly references `thread_type`/`system` (e.g. studio notebooks) opts out of the exclusion. New `query` field = case-insensitive title (`display_name`) substring search, used by the chats page's server-side pagination. `POST /threads/count` takes the same request body (limit/offset ignored) and returns `{total}` under identical filtering/exclusion semantics ? powers the chats page's ??/??? display without fetching rows (`ThreadMetaStore.count`). `ThreadMetaStore.search` gained `exclude_system`/`query` params; JSON-blob filters batch-scan until the requested `limit`/`offset` window is full (so pagination over a heavily-filtered set never under-fills), with a `thread_id` tie-breaker on the `updated_at DESC` ordering for stable OFFSET paging. Tests: `tests/test_thread_meta_search.py` | -| **Artifacts** (`/api/threads/{id}/artifacts`) | `GET /{path}` - serve artifacts; active content types (`text/html`, `application/xhtml+xml`, `image/svg+xml`) are always forced as download attachments to reduce XSS risk; `?download=true` still forces download for other file types | -| **Suggestions** (`/api/threads/{id}/suggestions`) | `POST /` - generate follow-up questions; rich list/block model content is normalized before JSON parsing | -| **Thread Runs** (`/api/threads/{id}/runs`) | `POST /` - create background run; `POST /stream` - create + SSE stream; `POST /wait` - create + block; `GET /` - list runs; `GET /{rid}` - run details; `POST /{rid}/cancel` - cancel; `GET /{rid}/join` - join SSE; `GET /{rid}/messages` - paginated messages `{data, has_more}`; `GET /{rid}/events` - full event stream; `GET /../messages` - thread messages with feedback; `GET /../token-usage` - aggregate tokens | -| **Feedback** (`/api/threads/{id}/runs/{rid}/feedback`) | `PUT /` - upsert feedback; `DELETE /` - delete user feedback; `POST /` - create feedback; `GET /` - list feedback; `GET /stats` - aggregate stats; `DELETE /{fid}` - delete specific | -| **Runs** (`/api/runs`) | `POST /stream` - stateless run + SSE; `POST /wait` - stateless run + block; `GET /{rid}/messages` - paginated messages by run_id `{data, has_more}` (cursor: `after_seq`/`before_seq`); `GET /{rid}/feedback` - list feedback by run_id | -| **Thread Shares** (`/api/thread-shares`) | Per-conversation sharing with two modes (`thread_shares.mode`). **`import`**: `POST /` `{thread_id, mode}` - create (or reuse) a revocable share code; `GET /{code}/preview` + `POST /{code}/import` - copy that conversation into the caller's account (fresh `thread_id` via `app/gateway/thread_copy.py` + new `threads_meta` row + `user-data` copy; dedup on `metadata.shared_origin`). **`view`**: a public, read-only static page ? see Public Shares below. Both modes: `GET /` - list my codes; `DELETE /{code}` - revoke. | -| **Public Shares** (`/api/public/shares`) | **Unauthenticated** (the `/api/public/` prefix is whitelisted in `auth_middleware._PUBLIC_PATH_PREFIXES`). `GET /{code}` - read-only conversation snapshot (title + serialized messages + artifacts) for a `view`-mode share; messages are read straight from the latest checkpoint. `GET /{code}/files/{path}` - serves a file referenced by that conversation (images, Markdown, text, PDF, ?) so the shared page's links are openable; path resolution is confined to the owner's thread directory. Active content (`text/html`, `application/xhtml+xml`, `image/svg+xml`) is forced to download (`_public_share_disposition`); everything else is served inline. Missing/revoked/non-`view` codes all return 404. | -| **Roundtable Drafts** (`/api/roundtable-drafts`) | Per-user persistence for the roundtable-planning page (replaces the old browser `localStorage`). `GET /` - list drafts (lightweight meta); `POST /` - create; `GET/PUT/DELETE /{id}` - full read / partial update / delete. A draft = one full session (Step 1 intent + Step 2 roundtable), step snapshots stored as JSON in `PortableLongText`. Recommendation history bound to a draft: `GET /{id}/recommendations` - list newest-first; `POST /{id}/recommendations` - append one run (objective/status/rationale/picks/candidates). Backed by `deerflow.persistence.roundtable_drafts` (tables `roundtable_drafts`, `roundtable_recommend_history`); store wired in `deps.py` as `app.state.roundtable_draft_store`. | -| **Roundtable Draft Shares** (`/api/roundtable-draft-shares`) | Per-draft sharing for the roundtable-planning page, mirroring Thread Shares but the shared resource is a `roundtable_drafts` row. Two modes (`roundtable_draft_shares.mode`). **`import`**: `POST /` `{draft_id, mode}` - create (or reuse) a revocable code; `GET /{code}/preview` + `POST /{code}/import` - copy that draft (step1/2/3) into the caller's account under a deterministic per-(code,recipient) id (`imp_` ? re-import is a no-op). **`view`**: a public, read-only result page ? see Public Roundtable Shares. Both: `GET /` - list my codes; `DELETE /{code}` - revoke. Backed by `deerflow.persistence.roundtable_draft_shares` (table `roundtable_draft_shares`, migration `20260606_01`); store wired in `deps.py` as `app.state.roundtable_draft_share_store`. Tests: `tests/test_roundtable_draft_shares.py`. | -| **Public Roundtable Shares** (`/api/public/roundtable-shares`) | **Unauthenticated** (`/api/public/` whitelisted). `GET /{code}` - read-only snapshot for a `view`-mode draft share: returns the Step 3??????report HTML (`step3.html`, a self-contained document) + title/summary/`has_report`. The frontend renders it in a sandboxed `iframe` (no `allow-scripts`) at `#/roundtable-share/`. Missing/revoked/non-`view` codes all return 404. | -| **Scheduled Tasks** (`/api/scheduled-tasks`) | Per-user cron-like agent tasks. `GET/POST /` - list/create owned tasks; `PATCH/DELETE /{id}` - update/delete (owner); `POST /{id}/{pause,resume,trigger}`; `GET /{id}/runs`, `GET /runs/{rid}`, run-file preview/edit/download; `GET /runs/{rid}/messages` - agent messages for the run's underlying agent run (used to inspect a stuck/running run; authorized via `_load_run`); `GET /runs/{rid}/html` - generated HTML document for an `html_page` run; `DELETE /runs/{rid}` - owner-scoped, with an admin fallback so admins can delete any user's run; publish/subscribe under `/{id}/{publish,unpublish,subscribe}` and `GET /published`. **Admin (cross-user):** `GET /admin/all` - every task; `PATCH/DELETE /admin/{id}` - edit/delete any task; `POST /admin/{id}/trigger`; `GET /admin/{id}/runs`. Admin routes require `system_role == "admin"`. | -| **HTML Page Favorites** (`/api/html-page-favorites`) | Bookmarked generated HTML pages, owner-scoped. `GET /` - list; `POST /` `{page_id, title?, description?, tags?}` - bookmark a `scheduled_html_pages` row (extracts a reusable style/layout summary via GLM5 at favorite time); a page can only be favorited once per user (`DuplicateFavoriteError` ? HTTP 409). `GET/DELETE /{id}` - read/delete (the create-task reference picker deletes favorites here). A favorite can be selected as a style reference when creating a new `html_page` task. | -| **Public Scheduled Tasks** (`/api/public/scheduled-tasks`) | **Unauthenticated** mirror for embedding. Adds `GET /runs/{rid}/html` and `GET /{task_id}/latest-html` for the `html_page` preview; the run result keeps `result.html` (only file paths are redacted). | -| **Light Apps** (`/api/light-apps`) | Light-app registry (????? / ????), **global/shared** (no user scoping; `created_by`/`updated_by` audit-only). Two kinds: `window_open` (launched in a new tab from the ???? card grid) and `iframe` (embedded under a parent sidebar menu via `mount_location` and dynamically injected into the sidebar). Both carry `route_params` ? a list of `{key, paramType: login\|theme, source, customValue, themeValue}` descriptors the **frontend** resolves at launch time (login params read from `localStorage.userInfo`; theme params use a literal value) and appends to `url` as query string. `GET /` - list (filters `app_type`/`mount_location`/`enabled_only`/`search`); `GET /{app_id}` - read one (both open to any authenticated user); `POST /`, `PUT /{app_id}`, `DELETE /{app_id}` - **admin only**; iframe apps must carry a `mount_location` (400 otherwise); the unique `name` collides as HTTP 409. Backed by `deerflow.persistence.light_apps` (table `light_apps`, auto-created by `create_all`); store wired in `deps.py` as `app.state.light_app_store`. Tests: `tests/test_light_apps.py`. | -| **Knowledge** (`/api/knowledge`) | Product knowledge base, **global/shared** (no user isolation; writes record `created_by`/`updated_by` for audit). `POST /ingest/thread/{thread_id}` - sediment one thread into a note (reads messages from the checkpointer, `threads:read` owner-checked); `POST /ingest/threads/batch` - batch-sediment the caller's recent threads (`skip_existing` via `.manifest.json` dedup). **Optional model selection**: both ingest endpoints accept `model_name` (overrides `knowledge.extract_model_name` for that capture; threaded through `KnowledgeService.capture_thread` ? `extract_thread_llm_multi`). **Model-error surfacing**: when the chosen model produces nothing usable (timeout / API error / non-JSON) capture still **falls back to rule-based extraction and lands a draft** (`llm_fallback:true`); the single-thread (non-background) response and the batch response (`llm_fallback` count) carry this so the UI tells the user "???????". The "??????" chat button stays background/fire-and-forget (passes `model_name`, no per-save error toast); the batch-import dialog is synchronous and shows the fallback count. `POST /ingest/file` - **import an uploaded file** (multipart: `file` + optional `title`/`tags`/`status`/`model_name`) into wiki note(s): the file is converted to Markdown (md/txt read directly, pdf/word/ppt/excel via `convert_file_to_markdown`), distilled by the same wiki-capture LLM pipeline as thread capture (`source_type="file"`), and falls back to a verbatim **draft** note when the model returns nothing usable (`llm_fallback:true`); `POST /ingest/search` - sediment search/tool results as a reference note; `POST /notes` - manual note; `GET /notes` (keyword/tag/source_type/status + pagination); `GET/PUT/DELETE /notes/{id}` (DELETE is soft = archive, `?hard=true` removes the vault file); `POST /search` - `mode=keyword\|vector\|hybrid` (vector?keyword fallback when no embedding endpoint); `GET /notes/{id}/versions` - edit history; `GET /duplicates` + `POST /merge` - dedup; `POST /reindex` - rebuild vector index; `GET /graph` - product graph data (notes + entities, graph-blacklist filtered); `GET/POST /graph/blacklist` + `DELETE /graph/blacklist/{id}` - graph node blacklist (labels = entity names / note titles, case-insensitive match; blacklisted nodes and their edges are dropped from `/graph` and the wiki-export artifacts; table `knowledge_graph_blacklist`, auto-created); `POST /export/graph` + `GET /export/files/{graph.json\|graph.html\|graph.graphml\|cypher.txt}` - obsidian-wiki `wiki-export` artifacts (self-contained `graph.html` for iframe); `POST /resolve` `{targets:[?]}` + `GET /notes/{id}/wikilinks` - resolve `[[wikilink]]` targets to a note id / vault page / missing (powers clickable wikilinks); `GET /vault-page?path=` - read a raw vault meta/hub Markdown page (no DB note). Backed by the `deerflow.knowledge` module (see below). | - -Proxied through nginx: `/api/langgraph/*` ? LangGraph, all other `/api/*` ? Gateway. - -### AI Writing Built-in Functional Agents (????????) - -The AI-writing graph (`packages/harness/deerflow/agents/ai_writing/`) has four stage "brains" ? ?????? / ????? / ?? / ?? ? that can each run in one of two modes, switched per-stage by `config.yaml ? ai_writing.use_builtin_agents.{researcher,outliner,writer,editor}` (default **false**, mtime hot-reloaded, new sessions pick up changes immediately): - -- **Legacy (flag off)**: hard-coded prompts in `prompts/*.py`, direct LLM call (unchanged behavior). -- **Builtin-agent (flag on)**: the node loads the corresponding functional agent from `.deer-flow/agents/ai-writing-/` ? `SOUL.md` becomes the system prompt and `config.yaml`'s `model` overrides the stage model (priority: `agent.model > node explicit fallback (keyword_model/rank_model) > state.model_name`). Admins edit SOUL/model from the agent management page. Runner: `nodes/_agent_runner.py` (`run_writing_agent` / `stream_writing_agent` / `parse_marker_json`). Output contracts (marker + ```json``` blocks, intent.py-style): researcher `[KEYWORDS_READY]` / `[MATERIALS_RANKED]`, outliner `[OUTLINE_READY]`, editor (per-dimension) `[REVIEW_READY]`; writer emits raw section Markdown (keeps per-section streaming) with the legacy `[[SECTION_BLOCKED]]` sentinel in strict mode. **Orchestration stays in the nodes** (search fan-out, per-section loop, 3-dimension review loop, all pause points) ? the agent only does single structured generations, and a missing/empty SOUL silently falls back to the legacy path (`WritingAgentUnavailable`). - -Seeding mirrors the roundtable pattern: verified `config.yaml` + `SOUL.md` ship in `app/gateway/routers/_ai_writing_seed_assets//`; `_ai_writing_seed.py::ensure_ai_writing_functional_agents()` copies them to `.deer-flow/agents/` when missing (never overwrites local edits), called from the startup lifespan (before `_sync_legacy_agents`, so the agents appear in `/api/agents` on first boot) and self-heals from `GET /api/ai-writing/sessions` + `GET /api/ai-writing/article-types`. Tests: `tests/test_ai_writing_seed.py`, `tests/test_ai_writing_agent_runner.py`. - -**Skill-driven retrieval (检索类型).** `material_source` has exactly two values: `general` (通用检索, default) and `notebook` (我的空间); legacy `knowledge_base`/`skill` are normalized to `general` by the researcher node. General retrieval is orchestrated from the **skills configured on the `ai-writing-researcher` agent** (`config.yaml → skills`, editable from the agent management page): the built-in `knowledge-base-search` skill (知识库检索) runs the native keyword fan-out web/intranet search (the old default path), while any other configured skill searches with a `description`-targeted query; skills run in configured order with early-stop at `max_materials`, and an empty/unresolvable skills config falls back to the built-in knowledge search so retrieval always works. The skill itself is seeded from `_ai_writing_seed_assets/skills/knowledge-base-search/SKILL.md` into `skills/public/` (never overwrites), and a pre-existing researcher `config.yaml` lacking the `skills` key is patched in place with the default (explicit `skills: []` is respected). **PAUSE-1 input append flow**: when the user types natural language into the material-confirm input (`re_search`/legacy `enable_skill_search` + `user_query`), the node matches the most relevant configured skill via `SkillPicker(candidates=…)` (skipped when only one skill is configured), searches with the user's words, and **appends** the new materials to the existing package (URL-dedup, original order preserved, no LLM re-rank/truncation, 100-item hard cap). Tests: `tests/test_ai_writing_material_source.py`. - -### Admin Leaderboard Snapshots (`/api/admin/users/leaderboard*`) - -The admin ????? is served from **precomputed per-day snapshots** in `admin_leaderboard_daily_stats` (keyed by `(stat_date, settings_hash)`), not by aggregating raw rows on every request. A background scheduler inside the Gateway (`_leaderboard_snapshot_scheduler_loop` in `app/gateway/app.py`, singleton file-locked) ticks every `ADMIN_LEADERBOARD_BACKGROUND_INTERVAL_SECONDS` (default 300s): it prewarms recent days, then builds any `queued`/`error`/stale-`running` day. `GET /leaderboard` merges the per-day snapshots for the requested range. **Today** is rebuilt whenever its snapshot is older than `ADMIN_LEADERBOARD_TODAY_TTL_SECONDS` (default 43200s = half a day, so the leaderboard refreshes ~twice daily); a past day, once `ready`, is treated as fresh forever. - -Two on-demand/automatic refresh paths layer on top: -- **Manual "????"** ? `POST /leaderboard/refresh-today` force-rebuilds *today's* snapshot **synchronously** (bounded by the same semaphore/timeout the scheduler uses), then returns the merged range. Lets an admin pull the latest real-time records into the stats without waiting for the TTL. Frontend: a "????" button on `AdminUserLeaderboardPage` ? `useRefreshTodayLeaderboard` (`core/admin/hooks.ts`), which primes + invalidates the `["admin-activity","leaderboard"]` query. -- **Daily finalize of the previous day** ? once per day, on/after `ADMIN_LEADERBOARD_FINALIZE_HOUR` (local Beijing, default `1` = ?? 1 ?), the scheduler tick force-recomputes *yesterday* a single time so runs/tool-calls that only landed near midnight are fully captured. The decision is the pure `deerflow.persistence.admin_stats.finalize.should_finalize_previous_day` (status/generated_at/now/hour) ? the rebuild stamps `generated_at` to today, which self-debounces it for the rest of the day (no extra state file). Tests: `tests/test_leaderboard_finalize.py`. - -### Scheduled Task Runtime - -`ScheduledTaskService` (`deerflow/runtime/scheduler/service.py`) polls due tasks every `poll_interval_seconds` and executes each in its own throwaway thread, serialized by a shared `_agent_run_lock`. - -**Stuck-run recovery**: each execution attempt is bounded by `_RUN_ATTEMPT_TIMEOUT_SECONDS` (30 min) via `asyncio.wait_for`. On timeout the run is retried up to `_MAX_RUN_RETRIES` (3) times, then marked `failed`. A watchdog (`_reconcile_stuck_runs`, run every poll) recovers runs left in `running` with no in-process owner ? e.g. orphaned by a crash or restart ? re-driving them through the same retry budget so a task can never stay perpetually "executing". The `scheduled_task_runs.retry_count` column tracks consumed retries. - -**Unique task names**: task names are unique per user (`uq_scheduled_tasks_user_name`). `create_task`/`update_task`/`admin_update_task` proactively check via `_assert_name_available()` and raise `DuplicateTaskNameError` on a collision; the router translates that into HTTP `409` instead of letting the raw DB `IntegrityError` surface as a `500`. - -**HTML page tasks (`task_kind == "html_page"`)**: a second task kind that renders a full HTML document each run instead of a Markdown report. The kind + its config (`html_config`: `page_prompt`, `negative_prompt`, `reference_favorite_id`, `model_name`) live inside `execution_context` ? no schema change to `scheduled_tasks`. Data gathering is delegated to the agent's own search skills (driven by `page_prompt`), so there is no explicit search-query/data-source field; the render model is user-selectable (`model_name`, default `zai-org/GLM-5-FP8`). `_run_task_attempt()` dispatches by kind; `_run_html_page_attempt()` runs the pipeline: gather real data via the lead agent (its search tools) ? optionally load a reference favorite's reusable style/layout ? render with the chosen model (thinking off) ? fault-tolerant JSON parse ? sanitize (BeautifulSoup, strips `
`): a read-only result page mirroring the - * in-app Step 3「最终方案展示」— 总控最终结论 + 各席位最终交付 + a 「查看结果绘制」 - * button that opens the full report in a NEW TAB (when one was generated). - * - report (`#/roundtable-share//report`): the Step 3 report (a + * in-app Step 3「最终方案展示」— 结果绘制/大屏多页入口 + 方案总结报告 + 总控最终 + * 结论 + 各席位最终交付. + * - report (`#/roundtable-share//report`): the Step 3 经典 HTML 看板 (a * self-contained HTML document) rendered full-screen inside a sandboxed - * `iframe` (no `allow-scripts`), so the agent-generated HTML can never execute - * code in the app origin. + * `iframe` (no `allow-scripts`). + * - dashboard (`#/roundtable-share//dashboard`): the Step 3 大屏多页 (模式二) + * — rendered on the fly from the stored structured data via `renderDashboardHtml` + * into the same sandboxed `iframe`. */ export default function PublicRoundtableSharePage({ - report = false, + view = "landing", }: { - report?: boolean; + view?: RoundtableShareView; }) { const { shareCode } = useParams<{ shareCode: string }>(); // 嵌套方可用 ?bg=/?text= 或 postMessage 覆盖背景/文字色。 @@ -108,6 +118,17 @@ export default function PublicRoundtableSharePage({ refetchOnWindowFocus: false, }); + // 大屏多页:用与应用内一致的 React 大屏(DashboardApp)渲染,而非自包含 HTML 模板。 + // 把存储的结构化数据(report-json)解析并 adapt 成 DashboardApp 的入参;与 + // Step3DashboardMode 的「应用渲染」、DashboardStandalonePage 的「新窗口打开」同源。 + const dashboardData = useMemo(() => { + const json = data?.dashboard_json; + if (!json) return undefined; + const parsed = parseReportJson(json); + if (!parsed) return undefined; + return adaptReportData(parsed, data?.generated_at ?? ""); + }, [data?.dashboard_json, data?.generated_at]); + if (isLoading) { return ( @@ -128,8 +149,24 @@ export default function PublicRoundtableSharePage({ ); } - // ── 报告页:整页沙箱 iframe 展示第 3 步结果绘制 ────────────────────────────── - if (report) { + // ── 大屏多页:整屏渲染应用内 React 大屏(DashboardApp),与应用渲染/新窗口打开一致 ── + if (view === "dashboard") { + return ( + + {dashboardData ? ( + + ) : ( + + 暂无大屏多页 + 这次会商还没有生成第三步「大屏多页」的方案报告。 + + )} + + ); + } + + // ── 结果绘制(经典 HTML 看板):整页沙箱 iframe 展示自包含报告 ───────────────── + if (view === "report") { return ( @@ -161,8 +198,10 @@ export default function PublicRoundtableSharePage({ ); } - // ── 落地页:总控最终结论 + 各席位最终交付 + 查看结果绘制(新标签打开报告页)───── - const reportUrl = `${window.location.origin}${window.location.pathname}#/roundtable-share/${shareCode}/report`; + // ── 落地页:结果绘制/大屏多页入口 + 方案总结报告 + 总控最终结论 + 各席位最终交付 ── + const shareBase = `${window.location.origin}${window.location.pathname}#/roundtable-share/${shareCode}`; + const reportUrl = `${shareBase}/report`; + const dashboardUrl = `${shareBase}/dashboard`; return ( @@ -182,23 +221,39 @@ export default function PublicRoundtableSharePage({ - {/* 1. 结果绘制 —— 新标签打开报告页 */} + {/* 1. 结果绘制 / 大屏多页 —— 新标签打开整页报告 */} 结果绘制 - {data.has_report ? ( - - - 查看结果绘制 - - + {data.has_report || data.has_dashboard ? ( + + {data.has_report && ( + + + 查看结果绘制 + + + )} + {data.has_dashboard && ( + + + 查看大屏多页 + + + )} + ) : ( 这次会商还没有生成第三步「结果绘制」的方案报告。 @@ -206,7 +261,22 @@ export default function PublicRoundtableSharePage({ )} - {/* 2. 总控最终结论 */} + {/* 2. 方案总结报告(Markdown) */} + {data.has_summary_report && ( + + + + 方案总结报告 + + + + {data.summary_report_md} + + + + )} + + {/* 3. 总控最终结论 */} @@ -223,7 +293,7 @@ export default function PublicRoundtableSharePage({ )} - {/* 3. 各席位最终交付 */} + {/* 4. 各席位最终交付 */} diff --git a/frontend-web/src/pages/light-app-management.css b/frontend-web/src/pages/light-app-management.css new file mode 100644 index 00000000..bc3eafc0 --- /dev/null +++ b/frontend-web/src/pages/light-app-management.css @@ -0,0 +1,570 @@ +@import "../../docs/ui规范-深蓝/tokens.css"; +@import "./light-app-pages.css"; + +/** + * 应用管理页 —— 深蓝色主题适配(《深蓝色 UI 规范》) + * 浅色样式仍由组件内 Tailwind 承担;.dark 下本文件覆盖为 ZS-004 / ZS-002 令牌。 + */ +.dark .light-app-management-page { + color: var(--color-text); +} + +/* ── 页头 / 文案 ─────────────────────────────────────────────────────── */ +.dark .light-app-management-page .lam-title { + color: var(--color-text-strong); +} + +.dark .light-app-management-page .lam-desc { + color: var(--color-text-secondary); +} + +.dark .light-app-management-page .lam-desc code { + background: rgba(22, 119, 255, 0.12); + color: var(--color-link); +} + +.dark .light-app-management-page .lam-primary-btn { + background: var(--color-primary); + box-shadow: none; +} + +.dark .light-app-management-page .lam-primary-btn:hover { + background: var(--color-primary-hover); +} + +.dark .light-app-management-page .lam-primary-btn:active { + background: var(--color-primary-active); +} + +/* ── 工具条 / 搜索 / 视图切换 ─────────────────────────────────────────── */ +.dark .light-app-management-page .lam-toolbar { + background: var(--color-bg-card); + border: 1px solid var(--color-border); + box-shadow: var(--shadow-1); +} + +.dark .light-app-management-page .lam-search-input { + background: var(--color-bg-page); + border-color: var(--color-border); + color: var(--color-text); +} + +.dark .light-app-management-page .lam-search-input::placeholder { + color: var(--color-text-disabled); +} + +.dark .light-app-management-page .lam-search-input:hover { + background: var(--color-bg-card); + border-color: var(--color-link); +} + +.dark .light-app-management-page .lam-search-input:focus { + background: var(--color-bg-card); + border-color: var(--color-primary); + box-shadow: 0 0 0 2px rgba(22, 119, 255, 0.16); + outline: none; +} + +.dark .light-app-management-page .lam-view-toggle { + background: var(--color-bg-page); + border-color: var(--color-border); +} + +.dark .light-app-management-page .lam-view-toggle-btn.is-active { + background: var(--color-bg-card); + color: var(--color-link); + box-shadow: var(--shadow-1); +} + +.dark .light-app-management-page .lam-view-toggle-btn:not(.is-active) { + color: var(--color-text-secondary); +} + +.dark .light-app-management-page .lam-view-toggle-btn:not(.is-active):hover { + color: var(--color-text); +} + +.dark .light-app-management-page .lam-count { + color: var(--color-text-secondary); +} + +.dark .light-app-management-page .lam-count strong { + color: var(--color-link); +} + +/* ── 表格(§28)──────────────────────────────────────────────────────── */ +.dark .light-app-management-page .lam-table-wrap { + background: var(--color-bg-page); + border: 1px solid var(--color-border); + box-shadow: var(--shadow-1); +} + +.dark .light-app-management-page .lam-table thead tr { + background: var(--color-bg-card); + border-color: var(--color-border); + color: var(--color-text); +} + +.dark .light-app-management-page .lam-table tbody { + border-color: var(--color-border); +} + +.dark .light-app-management-page .lam-table tbody tr { + border-color: var(--color-border); +} + +.dark .light-app-management-page .lam-table tbody tr:hover { + background: rgba(22, 119, 255, 0.08); +} + +.dark .light-app-management-page .lam-row-name { + color: var(--color-text); +} + +.dark .light-app-management-page .lam-table tbody tr:hover .lam-row-name { + color: var(--color-link); +} + +.dark .light-app-management-page .lam-tag-window { + border-color: rgba(245, 34, 45, 0.35); + background: var(--func-error-bg); + color: #ff7875; +} + +.dark .light-app-management-page .lam-tag-iframe { + border-color: rgba(22, 119, 255, 0.35); + background: var(--func-info-bg); + color: var(--color-link); +} + +.dark .light-app-management-page .lam-url { + color: var(--color-text-secondary); +} + +.dark .light-app-management-page .lam-table tbody tr:hover .lam-url { + color: var(--color-text); +} + +.dark .light-app-management-page .lam-url a { + color: inherit; +} + +.dark .light-app-management-page .lam-url a:hover { + color: var(--color-link); +} + +.dark .light-app-management-page .lam-mount-tag { + background: var(--func-info-bg); + color: var(--color-link); +} + +.dark .light-app-management-page .lam-mount-dot { + background: var(--color-primary); +} + +.dark .light-app-management-page .lam-param-btn { + border-color: var(--color-border); + background: var(--color-bg-page); + color: var(--color-text-secondary); +} + +.dark .light-app-management-page .lam-param-btn:hover { + background: rgba(22, 119, 255, 0.08); + color: var(--color-text); +} + +.dark .light-app-management-page .lam-param-popover { + border-color: var(--color-border); + background: var(--color-bg-card); + box-shadow: var(--shadow-3); +} + +.dark .light-app-management-page .lam-param-popover-head { + border-color: var(--color-border); + color: var(--color-text-secondary); +} + +.dark .light-app-management-page .lam-param-popover-close { + color: var(--color-link); +} + +.dark .light-app-management-page .lam-param-item { + border-color: var(--color-border); + background: var(--color-bg-page); +} + +.dark .light-app-management-page .lam-param-key { + color: #ff7875; +} + +.dark .light-app-management-page .lam-param-val { + color: var(--color-text-secondary); +} + +.dark .light-app-management-page .lam-status-on { + color: var(--func-success); +} + +.dark .light-app-management-page .lam-status-on-dot { + background: var(--func-success); +} + +.dark .light-app-management-page .lam-status-on-ping { + background: var(--func-success); +} + +.dark .light-app-management-page .lam-status-off { + color: var(--color-text-disabled); +} + +.dark .light-app-management-page .lam-status-off-dot { + background: var(--color-text-disabled); +} + +.dark .light-app-management-page .lam-icon-btn { + color: var(--color-text-secondary); +} + +.dark .light-app-management-page .lam-icon-btn:hover { + background: rgba(22, 119, 255, 0.08); + color: var(--color-link); +} + +.dark .light-app-management-page .lam-icon-btn-danger:hover { + background: var(--func-error-bg); + color: var(--func-error); +} + +.dark .light-app-management-page .lam-empty-icon { + background: var(--color-bg-page); + border: 1px solid var(--color-border); + color: var(--color-text-disabled); +} + +.dark .light-app-management-page .lam-empty-title { + color: var(--color-text-secondary); +} + +.dark .light-app-management-page .lam-empty-sub { + color: var(--color-text-disabled); +} + +/* ── 卡片视图(§22 / §43)────────────────────────────────────────────── */ +.dark .light-app-management-page .lam-card { + background: var(--color-bg-card); + border: 1px solid var(--color-border); + box-shadow: var(--shadow-1); +} + +.dark .light-app-management-page .lam-card:hover { + box-shadow: var(--shadow-2); +} + +.dark .light-app-management-page .lam-card-title { + color: var(--color-text-strong); +} + +.dark .light-app-management-page .lam-card:hover .lam-card-title { + color: var(--color-link); +} + +.dark .light-app-management-page .lam-card-desc { + color: var(--color-text-secondary); +} + +.dark .light-app-management-page .lam-card-url-box { + background: var(--color-bg-page); +} + +.dark .light-app-management-page .lam-card-param { + border-color: var(--color-border); + background: var(--color-bg-page); + color: var(--color-text-secondary); +} + +.dark .light-app-management-page .lam-card-mount { + background: var(--func-info-bg); + color: var(--color-link); +} + +.dark .light-app-management-page .lam-card-footer { + border-color: var(--color-border); + background: rgba(12, 24, 54, 0.5); +} + +/* ── 底部提示(§33 警告)────────────────────────────────────────────── */ +.dark .light-app-management-page .lam-tip-box { + background: var(--func-warning-bg); + border: 1px solid rgba(250, 173, 20, 0.25); + box-shadow: none; +} + +.dark .light-app-management-page .lam-tip-head { + border-color: rgba(250, 173, 20, 0.2); + color: var(--func-warning); +} + +.dark .light-app-management-page .lam-tip-list { + color: var(--color-text-secondary); +} + +.dark .light-app-management-page .lam-tip-list strong { + color: var(--color-text); +} + +.dark .light-app-management-page .lam-tip-bullet { + color: var(--func-warning); +} + +/* ── 弹窗(§21 Modal)────────────────────────────────────────────────── */ +.dark .light-app-management-page .lam-dialog-overlay { + background: rgba(6, 14, 36, 0.6); +} + +.dark .light-app-management-page .lam-dialog { + border-color: var(--color-border); + background: linear-gradient(180deg, var(--color-bg-card), var(--color-bg-page)); + box-shadow: var(--shadow-3); + color: var(--color-text); +} + +.dark .light-app-management-page .lam-dialog-header { + border-color: var(--color-border); + border-bottom: none; + padding-bottom: 0; +} + +.dark .light-app-management-page .lam-dialog-header::after { + content: ""; + display: block; + height: 1px; + margin-top: 16px; + margin-bottom: 0; + background: var(--gradient-title-line); +} + +.dark .light-app-management-page .lam-dialog-icon { + background: var(--func-info-bg); + color: var(--color-link); +} + +.dark .light-app-management-page .lam-dialog-title { + color: var(--color-text-strong); +} + +.dark .light-app-management-page .lam-dialog-close { + color: var(--color-text-secondary); +} + +.dark .light-app-management-page .lam-dialog-close:hover { + background: rgba(22, 119, 255, 0.08); + color: var(--color-link); +} + +.dark .light-app-management-page .lam-form-label { + color: var(--color-text); +} + +.dark .light-app-management-page .lam-form-label .lam-required { + color: var(--func-error); +} + +.dark .light-app-management-page .lam-form-label svg { + color: var(--color-text-secondary); +} + +.dark .light-app-management-page .lam-form-hint { + color: var(--color-text-secondary); +} + +.dark .light-app-management-page .lam-dialog .lam-input, +.dark .light-app-management-page .lam-dialog input[type="text"] { + background: var(--color-bg-page); + border-color: var(--color-border); + color: var(--color-text); +} + +.dark .light-app-management-page .lam-dialog .lam-input::placeholder, +.dark .light-app-management-page .lam-dialog input[type="text"]::placeholder { + color: var(--color-text-disabled); +} + +.dark .light-app-management-page .lam-dialog .lam-input:focus, +.dark .light-app-management-page .lam-dialog input[type="text"]:focus { + border-color: var(--color-primary); + box-shadow: 0 0 0 2px rgba(22, 119, 255, 0.16); + outline: none; +} + +.dark .light-app-management-page .lam-param-panel { + border-color: var(--color-border); + background: var(--color-bg-page); +} + +.dark .light-app-management-page .lam-param-row { + border-color: var(--color-border); + background: var(--color-bg-card); +} + +.dark .light-app-management-page .lam-param-theme-box { + background: var(--color-bg-page); +} + +.dark .light-app-management-page .lam-param-empty { + border-color: var(--color-border); + background: var(--color-bg-page); + color: var(--color-text-secondary); +} + +.dark .light-app-management-page .lam-add-param-btn { + border-color: var(--color-border-strong); + background: var(--color-bg-card); + color: var(--color-text); +} + +.dark .light-app-management-page .lam-add-param-btn:hover { + border-color: var(--color-primary); + color: var(--color-link); +} + +.dark .light-app-management-page .lam-dialog-footer { + border-color: var(--color-border); +} + +.dark .light-app-management-page .lam-dialog-cancel { + border-color: var(--color-border-strong); + background: transparent; + color: var(--color-text); +} + +.dark .light-app-management-page .lam-dialog-cancel:hover { + border-color: var(--color-link); + color: var(--color-link); +} + +.dark .light-app-management-page .lam-dialog-save { + background: var(--color-primary); + box-shadow: none; +} + +.dark .light-app-management-page .lam-dialog-save:hover { + background: var(--color-primary-hover); +} + +.dark .light-app-management-page .lam-dialog-save:active { + background: var(--color-primary-active); +} + +.dark .light-app-management-page .lam-switch-on { + background: var(--color-primary); +} + +.dark .light-app-management-page .lam-switch-off { + background: var(--color-text-disabled); +} + +/* ── TDesign Select(§15)──────────────────────────────────────────── */ +.dark .light-app-management-page .t-select .t-input, +.dark .light-app-management-page .t-select .t-input__wrap { + background: var(--color-bg-page); + border-color: var(--color-border); + border-radius: var(--radius-input); +} + +.dark .light-app-management-page .t-select .t-input__inner { + color: var(--color-text); +} + +.dark .light-app-management-page .t-select .t-input__inner::placeholder { + color: var(--color-text-disabled); +} + +.dark .light-app-management-page .t-select:hover .t-input, +.dark .light-app-management-page .t-select:hover .t-input__wrap { + border-color: var(--color-link); +} + +.dark .light-app-management-page .t-select.t-is-focused .t-input, +.dark .light-app-management-page .t-select.t-is-focused .t-input__wrap { + border-color: var(--color-primary); + box-shadow: 0 0 0 2px rgba(22, 119, 255, 0.16); +} + +.dark .light-app-management-page .t-select .t-fake-arrow { + color: var(--color-text-secondary); +} + +.dark .light-app-management-page .t-select__dropdown { + background: var(--color-bg-card); + border-color: var(--color-border); + box-shadow: var(--shadow-2); +} + +.dark .light-app-management-page .t-select-option { + color: var(--color-text); +} + +.dark .light-app-management-page .t-select-option:hover { + background: rgba(22, 119, 255, 0.08); +} + +.dark .light-app-management-page .t-select-option.t-is-selected { + background: rgba(22, 119, 255, 0.16); + color: var(--color-link); +} + +/* shadcn Input / Textarea in dialog */ +.dark .light-app-management-page .lam-dialog [data-slot="input"], +.dark .light-app-management-page .lam-dialog textarea { + background: var(--color-bg-page); + border-color: var(--color-border); + color: var(--color-text); +} + +.dark .light-app-management-page .lam-dialog [data-slot="input"]:focus-visible, +.dark .light-app-management-page .lam-dialog textarea:focus-visible { + border-color: var(--color-primary); + box-shadow: 0 0 0 2px rgba(22, 119, 255, 0.16); +} + +.dark .light-app-management-page .lam-param-theme-box span { + color: var(--color-text-secondary); +} + +/* TDesign 下拉挂载到 body 时的深蓝样式 */ +.dark .lam-select-popup.t-popup { + background: var(--color-bg-card); + border-color: var(--color-border); + box-shadow: var(--shadow-2); +} + +.dark .lam-select-popup .t-select-option { + color: var(--color-text); +} + +.dark .lam-select-popup .t-select-option:hover { + background: rgba(22, 119, 255, 0.08); +} + +.dark .lam-select-popup .t-select-option.t-is-selected { + background: rgba(22, 119, 255, 0.16); + color: var(--color-link); +} + +.dark .light-app-management-page .lam-search-input + button, +.dark .light-app-management-page .lam-toolbar .absolute.text-neutral-400 { + color: var(--color-text-secondary); +} + +.dark .light-app-management-page .lam-table .text-neutral-300 { + color: var(--color-text-disabled); +} + +.dark .light-app-management-page .lam-card-url-box button { + color: var(--color-text-secondary); +} + +.dark .light-app-management-page .lam-card-url-box button:hover { + background: rgba(22, 119, 255, 0.08); + color: var(--color-link); +} diff --git a/frontend-web/src/pages/light-app-pages.css b/frontend-web/src/pages/light-app-pages.css new file mode 100644 index 00000000..459be997 --- /dev/null +++ b/frontend-web/src/pages/light-app-pages.css @@ -0,0 +1,12 @@ +@import "../../docs/ui规范-深蓝/tokens.css"; + +/** 轻应用中心列表 / 管理页 —— 深色模式共用页面底图(与 workspace 深蓝主题一致) */ +.dark .light-app-management-page, +.dark .light-app-list-page { + background-color: var(--color-bg-page); + background-image: url("../assets/img/layout-bg-theme-dark-blue.svg"); + background-size: cover; + background-position: center top; + background-repeat: no-repeat; + background-attachment: fixed; +} diff --git a/frontend-web/src/roundtable-planning/components/HighFidelityReport.tsx b/frontend-web/src/roundtable-planning/components/HighFidelityReport.tsx index 533f8ba9..f12a0bb5 100644 --- a/frontend-web/src/roundtable-planning/components/HighFidelityReport.tsx +++ b/frontend-web/src/roundtable-planning/components/HighFidelityReport.tsx @@ -1009,6 +1009,7 @@ export const HighFidelityReport: React.FC = ({ filepath={fileSandboxVirtualUrl} threadId={fileSandboxTarget.threadId} onClose={handleCloseFileSandbox} + allowVirtualMarkdownEdit /> )} diff --git a/frontend-web/src/roundtable-planning/components/Step2SandboxLayout.tsx b/frontend-web/src/roundtable-planning/components/Step2SandboxLayout.tsx index 1d69dea8..59585fa9 100644 --- a/frontend-web/src/roundtable-planning/components/Step2SandboxLayout.tsx +++ b/frontend-web/src/roundtable-planning/components/Step2SandboxLayout.tsx @@ -115,6 +115,7 @@ export function Step2SandboxLayout({ filepath={selectedArtifact} threadId={previewThreadId} onClose={artifactsFill && onArtifactsClose ? handleArtifactsClose : undefined} + allowVirtualMarkdownEdit /> ) : ( diff --git a/frontend-web/src/strategy-components/Header.tsx b/frontend-web/src/strategy-components/Header.tsx index 7e4f144f..b27f18a5 100644 --- a/frontend-web/src/strategy-components/Header.tsx +++ b/frontend-web/src/strategy-components/Header.tsx @@ -139,6 +139,10 @@ const Header = ({ collapsed }: HeaderProps) => { return email ? email.split("@")[0] : "用户"; }, [auth?.email]); const showTopNav = isDark && !collapsed && !isWujieEnv(); + // 浅色(红/蓝)顶栏:一级导航正常横排进顶栏,不做深色那种围绕基座的特殊处理。 + // 与 collapsed 解耦(折叠只影响侧边栏,一级导航始终可见);wujie 嵌入下隐藏, + // 此时一级导航回落到侧边栏(见 page-sidebar.tsx)。 + const showLightTopNav = !isDark && !isWujieEnv(); const hasActiveChild = (item: MenuItem): boolean => { if (item.path && pathname === item.path) { @@ -194,6 +198,20 @@ const Header = ({ collapsed }: HeaderProps) => { ); }; + const renderLightTopItem = (item: MenuItem) => { + const isSelected = item.path ? pathname === item.path : hasActiveChild(item); + return ( + handleNavItemClick(item)} + > + {item.label} + + ); + }; + // 右侧操作区(换肤/通知/用户)—— 深浅两套布局共用 const actionCluster = ( <> @@ -309,6 +327,27 @@ const Header = ({ collapsed }: HeaderProps) => { > + ) : showLightTopNav ? ( + <> + {/* 浅色顶栏:logo + 一级导航横排(白字落在 banner 上) */} + + + + + + + CM助手 + + + + {visibleMenuItems.map(renderLightTopItem)} + + + + + {actionCluster} + + > ) : ( <> @@ -335,6 +374,7 @@ const Header = ({ collapsed }: HeaderProps) => { > )} + {/* 三级菜单横条仅深色用;浅色三级已改到侧边栏右侧列(见 page-sidebar.tsx) */} {showTopNav && thirdSubNavItems.length ? ( diff --git a/frontend-web/src/strategy-components/lib/light-app.ts b/frontend-web/src/strategy-components/lib/light-app.ts index 481b838b..ab796bf6 100644 --- a/frontend-web/src/strategy-components/lib/light-app.ts +++ b/frontend-web/src/strategy-components/lib/light-app.ts @@ -19,11 +19,17 @@ export function mountLabel(value: string | null | undefined): string { return MOUNT_OPTIONS.find((o) => o.value === value)?.label ?? value; } -/** 登录参数可选来源;值均可在 userInfo 中取到,"other" 表示自填。 */ +/** + * 登录参数可选来源;值均可在 userInfo 中取到,"other" 表示自填。 + * accessToken / yUserId 取登录路由透传值(写入 userInfo 的同名键); + * userName / gateDepartName / typeName 取 consumer 接口 userDetailByYuserId 的返回值。 + */ export const LOGIN_SOURCE_OPTIONS: { label: string; value: string }[] = [ { label: "accessToken", value: "accessToken" }, { label: "yUserId", value: "yUserId" }, { label: "userName", value: "userName" }, + { label: "gateDepartName", value: "gateDepartName" }, + { label: "typeName", value: "typeName" }, { label: "其他(自定义值)", value: "other" }, ]; diff --git a/frontend-web/src/styles/globals.css b/frontend-web/src/styles/globals.css index 7ebd17de..3494b65c 100644 --- a/frontend-web/src/styles/globals.css +++ b/frontend-web/src/styles/globals.css @@ -932,7 +932,7 @@ body { .strategy-app-header { background-color: transparent; background-image: url("../assets/img/red-header.png"); - background-size: 100% 100%; + background-size: cover; background-repeat: no-repeat; background-position: center; } diff --git a/offline-backend-20260512/backend/CLAUDE.md b/offline-backend-20260512/backend/CLAUDE.md index 18e7823d..f6812132 100644 --- a/offline-backend-20260512/backend/CLAUDE.md +++ b/offline-backend-20260512/backend/CLAUDE.md @@ -1,759 +1,759 @@ -# CLAUDE.md -?? -This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. - -## Project Overview - -DeerFlow is a LangGraph-based AI super agent system with a full-stack architecture. The backend provides a "super agent" with sandbox execution, persistent memory, subagent delegation, and extensible tool integration - all operating in per-thread isolated environments. - -**Architecture**: -- **Gateway API** (port 8001): REST API plus embedded LangGraph-compatible agent runtime -- **Frontend** (port 3000): Next.js web interface -- **Nginx** (port 2026): Unified reverse proxy entry point -- **Provisioner** (port 8002, optional in Docker dev): Started only when sandbox is configured for provisioner/Kubernetes mode - -**Runtime**: -- `make dev`, Docker dev, and production all run the agent runtime in Gateway via `RunManager` + `run_agent()` + `StreamBridge` (`packages/harness/deerflow/runtime/`). Nginx exposes that runtime at `/api/langgraph/*` and rewrites it to Gateway's native `/api/*` routers. - -**Project Structure**: -``` -deer-flow/ -??? Makefile # Root commands (check, install, dev, stop) -??? config.yaml # Main application configuration -??? extensions_config.json # MCP servers and skills configuration -??? backend/ # Backend application (this directory) -? ??? Makefile # Backend-only commands (dev, gateway, lint) -? ??? langgraph.json # LangGraph Studio graph configuration -? ??? packages/ -? ? ??? harness/ # deerflow-harness package (import: deerflow.*) -? ? ??? pyproject.toml -? ? ??? deerflow/ -? ? ??? agents/ # LangGraph agent system -? ? ? ??? lead_agent/ # Main agent (factory + system prompt) -? ? ? ??? middlewares/ # 10 middleware components -? ? ? ??? memory/ # Memory V2: provider/manager/builtin/hindsight -? ? ? ??? thread_state.py # ThreadState schema -? ? ??? sandbox/ # Sandbox execution system -? ? ? ??? local/ # Local filesystem provider -? ? ? ??? sandbox.py # Abstract Sandbox interface -? ? ? ??? tools.py # bash, ls, read/write/str_replace -? ? ? ??? middleware.py # Sandbox lifecycle management -? ? ??? subagents/ # Subagent delegation system -? ? ? ??? builtins/ # general-purpose, bash agents -? ? ? ??? executor.py # Background execution engine -? ? ? ??? registry.py # Agent registry -? ? ??? tools/builtins/ # Built-in tools (present_files, ask_clarification, view_image) -? ? ??? mcp/ # MCP integration (tools, cache, client) -? ? ??? models/ # Model factory with thinking/vision support -? ? ??? skills/ # Skills discovery, loading, parsing -? ? ??? config/ # Configuration system (app, model, sandbox, tool, etc.) -? ? ??? community/ # Community tools (tavily, jina_ai, firecrawl, image_search, aio_sandbox) -? ? ??? reflection/ # Dynamic module loading (resolve_variable, resolve_class) -? ? ??? utils/ # Utilities (network, readability) -? ? ??? client.py # Embedded Python client (DeerFlowClient) -? ??? app/ # Application layer (import: app.*) -? ? ??? gateway/ # FastAPI Gateway API -? ? ? ??? app.py # FastAPI application -? ? ? ??? routers/ # FastAPI route modules (models, mcp, memory, skills, uploads, threads, artifacts, agents, suggestions, channels) -? ? ??? channels/ # IM platform integrations -? ??? tests/ # Test suite -? ??? docs/ # Documentation -??? frontend/ # Next.js frontend application -??? skills/ # Agent skills directory - ??? public/ # Public skills (committed) - ??? custom/ # Custom skills (gitignored) -``` - -## Important Development Guidelines - -### Documentation Update Policy -**CRITICAL: Always update README.md and CLAUDE.md after every code change** - -When making code changes, you MUST update the relevant documentation: -- Update `README.md` for user-facing changes (features, setup, usage instructions) -- Update `CLAUDE.md` for development changes (architecture, commands, workflows, internal systems) -- Keep documentation synchronized with the codebase at all times -- Ensure accuracy and timeliness of all documentation - -## Commands - -**Root directory** (for full application): -```bash -make check # Check system requirements -make install # Install all dependencies (frontend + backend) -make dev # Start all services (Gateway + Frontend + Nginx), with config.yaml preflight -make start # Start production services locally -make stop # Stop all services -``` - -**Backend directory** (for backend development only): -```bash -make install # Install backend dependencies -make dev # Run Gateway API with reload (port 8001) -make gateway # Run Gateway API only (port 8001) -make test # Run all backend tests -make lint # Lint with ruff -make format # Format code with ruff -``` - -Regression tests related to Docker/provisioner behavior: -- `tests/test_docker_sandbox_mode_detection.py` (mode detection from `config.yaml`) -- `tests/test_provisioner_kubeconfig.py` (kubeconfig file/directory handling) - -Boundary check (harness ? app import firewall): -- `tests/test_harness_boundary.py` ? ensures `packages/harness/deerflow/` never imports from `app.*` - -CI runs these regression tests for every pull request via [.github/workflows/backend-unit-tests.yml](../.github/workflows/backend-unit-tests.yml). - -## Architecture - -### Harness / App Split - -The backend is split into two layers with a strict dependency direction: - -- **Harness** (`packages/harness/deerflow/`): Publishable agent framework package (`deerflow-harness`). Import prefix: `deerflow.*`. Contains agent orchestration, tools, sandbox, models, MCP, skills, config ? everything needed to build and run agents. -- **App** (`app/`): Unpublished application code. Import prefix: `app.*`. Contains the FastAPI Gateway API and IM channel integrations (Feishu, Slack, Telegram, DingTalk). - -**Dependency rule**: App imports deerflow, but deerflow never imports app. This boundary is enforced by `tests/test_harness_boundary.py` which runs in CI. - -**Import conventions**: -```python -# Harness internal -from deerflow.agents import make_lead_agent -from deerflow.models import create_chat_model - -# App internal -from app.gateway.app import app -from app.channels.service import start_channel_service - -# App ? Harness (allowed) -from deerflow.config import get_app_config - -# Harness ? App (FORBIDDEN ? enforced by test_harness_boundary.py) -# from app.gateway.routers.uploads import ... # ? will fail CI -``` - -### Agent System - -**Lead Agent** (`packages/harness/deerflow/agents/lead_agent/agent.py`): -- Entry point: `make_lead_agent(config: RunnableConfig)` registered in `langgraph.json` -- Dynamic model selection via `create_chat_model()` with thinking/vision support -- Tools loaded via `get_available_tools()` - combines sandbox, built-in, MCP, community, and subagent tools -- System prompt generated by `apply_prompt_template()` with skills, memory, and subagent instructions - -**ThreadState** (`packages/harness/deerflow/agents/thread_state.py`): -- Extends `AgentState` with: `sandbox`, `thread_data`, `title`, `artifacts`, `todos`, `uploaded_files`, `viewed_images` -- Uses custom reducers: `merge_artifacts` (deduplicate), `merge_viewed_images` (merge/clear) - -**Runtime Configuration** (via `config.configurable`): -- `thinking_enabled` - Enable model's extended thinking -- `model_name` - Select specific LLM model -- `is_plan_mode` - Enable TodoList middleware -- `subagent_enabled` - Enable task delegation tool - -### Middleware Chain - -Lead-agent middlewares are assembled in strict append order across `packages/harness/deerflow/agents/middlewares/tool_error_handling_middleware.py` (`build_lead_runtime_middlewares`) and `packages/harness/deerflow/agents/lead_agent/agent.py` (`_build_middlewares`): - -1. **ThreadDataMiddleware** - Creates per-thread directories under the user's isolation scope (`backend/.deer-flow/users/{user_id}/threads/{thread_id}/user-data/{workspace,uploads,outputs}`); resolves `user_id` via `get_effective_user_id()` (falls back to `"default"` in no-auth mode); Web UI thread deletion now follows LangGraph thread removal with Gateway cleanup of the local thread directory -2. **UploadsMiddleware** - Tracks and injects newly uploaded files into conversation -3. **SandboxMiddleware** - Acquires sandbox, stores `sandbox_id` in state -4. **DanglingToolCallMiddleware** - Injects placeholder ToolMessages for AIMessage tool_calls that lack responses (e.g., due to user interruption), including raw provider tool-call payloads preserved only in `additional_kwargs["tool_calls"]` -5. **LLMErrorHandlingMiddleware** - Normalizes provider/model invocation failures into recoverable assistant-facing errors before later middleware/tool stages run -6. **GuardrailMiddleware** - Pre-tool-call authorization via pluggable `GuardrailProvider` protocol (optional, if `guardrails.enabled` in config). Evaluates each tool call and returns error ToolMessage on deny. Three provider options: built-in `AllowlistProvider` (zero deps), OAP policy providers (e.g. `aport-agent-guardrails`), or custom providers. See [docs/GUARDRAILS.md](docs/GUARDRAILS.md) for setup, usage, and how to implement a provider. -7. **SandboxAuditMiddleware** - Audits sandboxed shell/file operations for security logging before tool execution continues -8. **ToolErrorHandlingMiddleware** - Converts tool exceptions into error `ToolMessage`s so the run can continue instead of aborting -9. **SummarizationMiddleware** (`DeerFlowSummarizationMiddleware`) - Context reduction when approaching token limits (optional, if enabled). **Non-destructive**: overrides the stock `before_model` (which persists a `RemoveMessage(REMOVE_ALL_MESSAGES)`) to a no-op and instead compresses **transiently** in `wrap_model_call` ? the model request is replaced with `[summary, *recent]` while the full original conversation stays in state (so the frontend keeps rendering the user's real Q&A). The `summary` message is `hide_from_ui` and never persisted; summaries are cached per-thread (bounded, sticky boundary) to avoid an LLM call on every model invocation. Tests: `tests/test_summarization_middleware.py` -10. **TodoListMiddleware** - Task tracking with `write_todos` tool (optional, if plan_mode) -11. **TokenUsageMiddleware** - Records token usage metrics when token tracking is enabled (optional) -12. **TitleMiddleware** - Two-phase auto-title (never shows "untitled"): `before_model` sets an instant question-derived *provisional* title the moment the first user message arrives (no LLM, zero latency), then `after_model` asynchronously generates the final LLM title and replaces it (tracked via the `title_provisional` state flag). Normalizes structured message content before prompting the title model -13. **MemoryMiddleware** - Memory V2: `wrap_model_call` injects Hindsight recall transiently; `after_agent` auto-retains the turn to Hindsight -14. **ViewImageMiddleware** - Injects base64 image data before LLM call (conditional on vision support) -15. **DeferredToolFilterMiddleware** - Hides deferred tool schemas from the bound model until tool search is enabled (optional) -16. **SubagentLimitMiddleware** - Truncates excess `task` tool calls from model response to enforce `MAX_CONCURRENT_SUBAGENTS` limit (optional, if `subagent_enabled`) -17. **LoopDetectionMiddleware** - Detects repeated tool-call loops; hard-stop responses clear both structured `tool_calls` and raw provider tool-call metadata before forcing a final text answer -18. **ClarificationMiddleware** - Intercepts `ask_clarification` tool calls, interrupts via `Command(goto=END)` (must be last) -8a. **ToolMetricsMiddleware** - Appended immediately after `ToolErrorHandlingMiddleware` in `_build_runtime_middlewares`, so it sits *deepest* in the `wrap_tool_call` chain. Records one `tool_call_metrics` row per tool/skill/MCP invocation ? timing duration and bucketing failures into a coarse `error_category` (rate_limited / ip_blocked / auth_error / timeout / network_error / not_found / empty_result / tool_error). It catches both raised exceptions (before #8 converts them) and *self-handled* errors returned as error `ToolMessage`s (search tools that swallow HTTP 429/403). It also captures the targeted Agent Skill into `tool_call_metrics.skill_name`: the lead agent invokes a skill by `read_file`-ing its `/mnt/skills///SKILL.md` path (per the `` prompt block ? there is no dedicated "use skill" tool), so `skill_name` is recovered by scanning every tool call's string arguments for the `skills/{public,custom}/{name}/` path shape (`_SKILL_PATH_RE`), plus the explicit `name` argument of `skill_view`/`skill_manage`/`skill_archive`. `skill_view`-specific success detection handles its swallowed failures (`Skill 'x' not found.` returned as a plain string rather than raised). Regression coverage: `tests/test_tool_metrics_middleware.py`. Writes are best-effort fire-and-forget. **Per-run skill merging**: `SqlToolMetricsStore.record` upserts skill rows by `(run_id, skill_name)` instead of inserting one row per call ? a skill touched by several tool calls / retries inside one run collapses to a single row. Any success in the run wins (a skill that failed then finally succeeded is recorded as one `success`); a skill that only ever failed yields one `error` row whose `error_message` accumulates every attempt's message; durations are summed. Non-skill tool calls, and skill calls with no `run_id`, keep one row each. The read?merge?write is guarded by a per-event-loop `asyncio.Lock`. Surfaced by the admin `GET /api/admin/tool-metrics` router (which has a `kind=skill` filter and a per-skill `/summary` breakdown). -9. **PromptPrefixMiddleware** - Injects the admin-configured prompt prefix into the model request only. The prefix is tagged onto the first human message's `additional_kwargs['prompt_prefix']` by the Gateway (`app.gateway.services`) and prepended to content here at call time, so the persisted/displayed message stays prefix-free -10. **SummarizationMiddleware** (`DeerFlowSummarizationMiddleware`) - Context reduction when approaching token limits (optional, if enabled). **Non-destructive**: overrides the stock `before_model` (which persists a `RemoveMessage(REMOVE_ALL_MESSAGES)`) to a no-op and instead compresses **transiently** in `wrap_model_call` ? the model request is replaced with `[summary, *recent]` while the full original conversation stays in state (so the frontend keeps rendering the user's real Q&A). The `summary` message is `hide_from_ui` and never persisted; summaries are cached per-thread (bounded, sticky boundary) to avoid an LLM call on every model invocation. Tests: `tests/test_summarization_middleware.py` -11. **TodoListMiddleware** - Task tracking with `write_todos` tool (optional, if plan_mode) -12. **TokenUsageMiddleware** - Records token usage metrics when token tracking is enabled (optional) -13. **TitleMiddleware** - Two-phase auto-title (never shows "untitled"): `before_model` sets an instant question-derived *provisional* title the moment the first user message arrives (no LLM, zero latency), then `after_model` asynchronously generates the final LLM title and replaces it (tracked via the `title_provisional` state flag). Normalizes structured message content before prompting the title model -14. **MemoryMiddleware** - Queues conversations for async memory update (filters to user + final AI responses) -15. **ViewImageMiddleware** - Injects base64 image data before LLM call (conditional on vision support) -16. **DeferredToolFilterMiddleware** - Hides deferred tool schemas from the bound model until tool search is enabled (optional) -17. **SubagentLimitMiddleware** - Truncates excess `task` tool calls from model response to enforce `MAX_CONCURRENT_SUBAGENTS` limit (optional, if `subagent_enabled`) -18. **LoopDetectionMiddleware** - Detects repeated tool-call loops; hard-stop responses clear both structured `tool_calls` and raw provider tool-call metadata before forcing a final text answer -19. **ClarificationMiddleware** - Intercepts `ask_clarification` tool calls, interrupts via `Command(goto=END)` (must be last) - -### Configuration System - -**Main Configuration** (`config.yaml`): - -Setup: Copy `config.example.yaml` to `config.yaml` in the **project root** directory. - -**Config Versioning**: `config.example.yaml` has a `config_version` field. On startup, `AppConfig.from_file()` compares user version vs example version and emits a warning if outdated. Missing `config_version` = version 0. Run `make config-upgrade` to auto-merge missing fields. When changing the config schema, bump `config_version` in `config.example.yaml`. - -**Config Caching**: `get_app_config()` caches the parsed config, but automatically reloads it when the resolved config path changes or the file's mtime increases. This keeps Gateway and LangGraph reads aligned with `config.yaml` edits without requiring a manual process restart. - -Configuration priority: -1. Explicit `config_path` argument -2. `DEER_FLOW_CONFIG_PATH` environment variable -3. `config.yaml` in current directory (backend/) -4. `config.yaml` in parent directory (project root - **recommended location**) - -Config values starting with `$` are resolved as environment variables (e.g., `$OPENAI_API_KEY`). -`ModelConfig` also declares `use_responses_api` and `output_version` so OpenAI `/v1/responses` can be enabled explicitly while still using `langchain_openai:ChatOpenAI`. - -**Extensions Configuration** (`extensions_config.json`): - -MCP servers and skills are configured together in `extensions_config.json` in project root: - -Configuration priority: -1. Explicit `config_path` argument -2. `DEER_FLOW_EXTENSIONS_CONFIG_PATH` environment variable -3. `extensions_config.json` in current directory (backend/) -4. `extensions_config.json` in parent directory (project root - **recommended location**) - -### Gateway API (`app/gateway/`) - -FastAPI application on port 8001 with health check at `GET /health`. Set `GATEWAY_ENABLE_DOCS=false` to disable `/docs`, `/redoc`, and `/openapi.json` in production (default: enabled). - -**Routers**: - -| Router | Endpoints | -|--------|-----------| -| **Models** (`/api/models`) | `GET /` - list models; `GET /{name}` - model details | -| **MCP** (`/api/mcp`) | `GET /config` - get config; `PUT /config` - update config (saves to extensions_config.json) | -| **Skills** (`/api/skills`) | `GET /` - list skills; `GET /{name}` - details; `PUT /{name}` - update enabled; `POST /install` - install from .skill archive (accepts standard optional frontmatter like `version`, `author`, `compatibility`) | -| **Memory V2** (`/api/memory/v2`) | `GET /` - builtin data (USER.md/MEMORY.md); `POST/PUT/DELETE /entries` - add/replace/remove entry; `GET/PATCH/DELETE /config` - effective config + per-user overrides; `POST /search` - Hindsight recall/reflect proxy | -| **Agents** (`/api/agents`) | `GET /` - list built-in, owned, and published agents; `POST /` - create user-owned agent; `GET/PUT/DELETE /{id}` - read/update/delete by stable agent id; `GET /check` - validate non-empty display name (duplicates allowed) | -| **Skills** (`/api/skills`) | `GET /` - list skills (optional `search` fuzzy-name + `tag_id` filters; each item carries `tags`); `GET /{name}` - details; `PUT /{name}` - update enabled; `POST /install` - install from .skill archive (accepts standard optional frontmatter like `version`, `author`, `compatibility`) | -| **Skills** (`/api/skills`) | `GET /` - list skills (optional `search` fuzzy-name + `tag_id` filters; each item carries `tags` + owner-authored `detail`); `GET /{name}` - details; `PUT /{name}` - update enabled; `PUT /{name}/detail` - set the long-form `detail` text (custom skill: owner or admin; built-in/public: admin only ? materializes a `skills` row on demand); `POST /install` - install from .skill archive (accepts standard optional frontmatter like `version`, `author`, `compatibility`) | -| **Skills** (`/api/skills`) | `GET /` - list skills (optional `search` fuzzy-name + `tag_id` filters; each item carries `tags` + owner-authored `detail`); `GET /{name}` - details; `PUT /{name}` - update enabled; `PUT /{name}/detail` - set the long-form `detail` text (custom skill: owner or admin; built-in/public: admin only ? materializes a `skills` row on demand); `GET /{name}/content` - raw SKILL.md source, restricted to an admin or the publisher (custom-skill owner); other users get 403 and see only description + detail; `POST /install` - install from .skill archive (accepts standard optional frontmatter like `version`, `author`, `compatibility`) | -| **Skills** (`/api/skills`) | `GET /` - list skills (optional `search` fuzzy-name + repeatable `tag_ids` multi-select filter, OR-matched; each item carries `tags` + owner-authored `detail`); `GET /{name}` - details; `PUT /{name}` - update enabled; `PUT /{name}/detail` - set the long-form `detail` text (custom skill: owner or admin; built-in/public: admin only ? materializes a `skills` row on demand); `GET /{name}/content` - raw SKILL.md source, restricted to an admin or the publisher (custom-skill owner); other users get 403 and see only description + detail; `POST /install` - install from .skill archive (accepts standard optional frontmatter like `version`, `author`, `compatibility`) | -| **Memory** (`/api/memory`) | `GET /` - memory data; `POST /reload` - force reload; `GET /config` - config; `GET /status` - config + data | -| **Agents** (`/api/agents`) | `GET /` - list built-in, owned, and published agents (optional `search` + repeatable `tag_ids` multi-select filter, OR-matched; each item carries `tags` and `featured_order`); `POST /` - create user-owned agent; `GET/PUT/DELETE /{id}` - read/update/delete by stable agent id; `GET /check` - validate non-empty display name (duplicates allowed); `GET /selector` - homepage agent picker (single admin-curated list, visibility-filtered, sorted by `featured_order` asc); `PUT /{id}/featured` - **admin** sets/clears the homepage `featured_order` (must be `published=true` to feature; passing `order: null` clears unconditionally); `PUT /featured/order` - **admin** bulk-reorders the homepage list (body `{agent_ids: [...]}` ? indices become 0..N-1) | -| **Tags** (`/api/tags`) | `GET /` - list tags (optional `tag_type` filter; open to all); `POST /` - create tag (admin); `PUT /{id}` / `DELETE /{id}` - update/delete tag (admin); `GET /assignments/{target_type}/{target_id}` - tags on a resource; `PUT /assignments/{target_type}/{target_id}` - replace a resource's tags (admin). Tags are typed `agent`/`skill`/`scheduled_task` and may only tag resources of their own type. | -| **Uploads** (`/api/threads/{id}/uploads`) | `POST /` - upload files (auto-converts PDF/PPT/Excel/Word); `GET /list` - list; `DELETE /{filename}` - delete | -| **Threads** (`/api/threads/{id}`) | `DELETE /` - remove DeerFlow-managed local thread data after LangGraph thread deletion; unexpected failures are logged server-side and return a generic 500 detail | -| **Threads search** (`POST /threads/search`, LangGraph-compat) | Conversation listing. **System threads are excluded by default**: rows whose `metadata.system` is truthy (scheduler runs, roundtable workers) never crowd the user's list; a caller whose `metadata` filter explicitly references `thread_type`/`system` (e.g. studio notebooks) opts out of the exclusion. New `query` field = case-insensitive title (`display_name`) substring search, used by the chats page's server-side pagination. `POST /threads/count` takes the same request body (limit/offset ignored) and returns `{total}` under identical filtering/exclusion semantics ? powers the chats page's ??/??? display without fetching rows (`ThreadMetaStore.count`). `ThreadMetaStore.search` gained `exclude_system`/`query` params; JSON-blob filters batch-scan until the requested `limit`/`offset` window is full (so pagination over a heavily-filtered set never under-fills), with a `thread_id` tie-breaker on the `updated_at DESC` ordering for stable OFFSET paging. Tests: `tests/test_thread_meta_search.py` | -| **Artifacts** (`/api/threads/{id}/artifacts`) | `GET /{path}` - serve artifacts; active content types (`text/html`, `application/xhtml+xml`, `image/svg+xml`) are always forced as download attachments to reduce XSS risk; `?download=true` still forces download for other file types | -| **Suggestions** (`/api/threads/{id}/suggestions`) | `POST /` - generate follow-up questions; rich list/block model content is normalized before JSON parsing | -| **Thread Runs** (`/api/threads/{id}/runs`) | `POST /` - create background run; `POST /stream` - create + SSE stream; `POST /wait` - create + block; `GET /` - list runs; `GET /{rid}` - run details; `POST /{rid}/cancel` - cancel; `GET /{rid}/join` - join SSE; `GET /{rid}/messages` - paginated messages `{data, has_more}`; `GET /{rid}/events` - full event stream; `GET /../messages` - thread messages with feedback; `GET /../token-usage` - aggregate tokens | -| **Feedback** (`/api/threads/{id}/runs/{rid}/feedback`) | `PUT /` - upsert feedback; `DELETE /` - delete user feedback; `POST /` - create feedback; `GET /` - list feedback; `GET /stats` - aggregate stats; `DELETE /{fid}` - delete specific | -| **Runs** (`/api/runs`) | `POST /stream` - stateless run + SSE; `POST /wait` - stateless run + block; `GET /{rid}/messages` - paginated messages by run_id `{data, has_more}` (cursor: `after_seq`/`before_seq`); `GET /{rid}/feedback` - list feedback by run_id | -| **Thread Shares** (`/api/thread-shares`) | Per-conversation sharing with two modes (`thread_shares.mode`). **`import`**: `POST /` `{thread_id, mode}` - create (or reuse) a revocable share code; `GET /{code}/preview` + `POST /{code}/import` - copy that conversation into the caller's account (fresh `thread_id` via `app/gateway/thread_copy.py` + new `threads_meta` row + `user-data` copy; dedup on `metadata.shared_origin`). **`view`**: a public, read-only static page ? see Public Shares below. Both modes: `GET /` - list my codes; `DELETE /{code}` - revoke. | -| **Public Shares** (`/api/public/shares`) | **Unauthenticated** (the `/api/public/` prefix is whitelisted in `auth_middleware._PUBLIC_PATH_PREFIXES`). `GET /{code}` - read-only conversation snapshot (title + serialized messages + artifacts) for a `view`-mode share; messages are read straight from the latest checkpoint. `GET /{code}/files/{path}` - serves a file referenced by that conversation (images, Markdown, text, PDF, ?) so the shared page's links are openable; path resolution is confined to the owner's thread directory. Active content (`text/html`, `application/xhtml+xml`, `image/svg+xml`) is forced to download (`_public_share_disposition`); everything else is served inline. Missing/revoked/non-`view` codes all return 404. | -| **Roundtable Drafts** (`/api/roundtable-drafts`) | Per-user persistence for the roundtable-planning page (replaces the old browser `localStorage`). `GET /` - list drafts (lightweight meta); `POST /` - create; `GET/PUT/DELETE /{id}` - full read / partial update / delete. A draft = one full session (Step 1 intent + Step 2 roundtable), step snapshots stored as JSON in `PortableLongText`. Recommendation history bound to a draft: `GET /{id}/recommendations` - list newest-first; `POST /{id}/recommendations` - append one run (objective/status/rationale/picks/candidates). Backed by `deerflow.persistence.roundtable_drafts` (tables `roundtable_drafts`, `roundtable_recommend_history`); store wired in `deps.py` as `app.state.roundtable_draft_store`. | -| **Roundtable Draft Shares** (`/api/roundtable-draft-shares`) | Per-draft sharing for the roundtable-planning page, mirroring Thread Shares but the shared resource is a `roundtable_drafts` row. Two modes (`roundtable_draft_shares.mode`). **`import`**: `POST /` `{draft_id, mode}` - create (or reuse) a revocable code; `GET /{code}/preview` + `POST /{code}/import` - copy that draft (step1/2/3) into the caller's account under a deterministic per-(code,recipient) id (`imp_` ? re-import is a no-op). **`view`**: a public, read-only result page ? see Public Roundtable Shares. Both: `GET /` - list my codes; `DELETE /{code}` - revoke. Backed by `deerflow.persistence.roundtable_draft_shares` (table `roundtable_draft_shares`, migration `20260606_01`); store wired in `deps.py` as `app.state.roundtable_draft_share_store`. Tests: `tests/test_roundtable_draft_shares.py`. | -| **Public Roundtable Shares** (`/api/public/roundtable-shares`) | **Unauthenticated** (`/api/public/` whitelisted). `GET /{code}` - read-only snapshot for a `view`-mode draft share: returns the Step 3??????report HTML (`step3.html`, a self-contained document) + title/summary/`has_report`. The frontend renders it in a sandboxed `iframe` (no `allow-scripts`) at `#/roundtable-share/`. Missing/revoked/non-`view` codes all return 404. | -| **Scheduled Tasks** (`/api/scheduled-tasks`) | Per-user cron-like agent tasks. `GET/POST /` - list/create owned tasks; `PATCH/DELETE /{id}` - update/delete (owner); `POST /{id}/{pause,resume,trigger}`; `GET /{id}/runs`, `GET /runs/{rid}`, run-file preview/edit/download; `GET /runs/{rid}/messages` - agent messages for the run's underlying agent run (used to inspect a stuck/running run; authorized via `_load_run`); `GET /runs/{rid}/html` - generated HTML document for an `html_page` run; `DELETE /runs/{rid}` - owner-scoped, with an admin fallback so admins can delete any user's run; publish/subscribe under `/{id}/{publish,unpublish,subscribe}` and `GET /published`. **Admin (cross-user):** `GET /admin/all` - every task; `PATCH/DELETE /admin/{id}` - edit/delete any task; `POST /admin/{id}/trigger`; `GET /admin/{id}/runs`. Admin routes require `system_role == "admin"`. | -| **HTML Page Favorites** (`/api/html-page-favorites`) | Bookmarked generated HTML pages, owner-scoped. `GET /` - list; `POST /` `{page_id, title?, description?, tags?}` - bookmark a `scheduled_html_pages` row (extracts a reusable style/layout summary via GLM5 at favorite time); a page can only be favorited once per user (`DuplicateFavoriteError` ? HTTP 409). `GET/DELETE /{id}` - read/delete (the create-task reference picker deletes favorites here). A favorite can be selected as a style reference when creating a new `html_page` task. | -| **Public Scheduled Tasks** (`/api/public/scheduled-tasks`) | **Unauthenticated** mirror for embedding. Adds `GET /runs/{rid}/html` and `GET /{task_id}/latest-html` for the `html_page` preview; the run result keeps `result.html` (only file paths are redacted). | -| **Light Apps** (`/api/light-apps`) | Light-app registry (????? / ????), **global/shared** (no user scoping; `created_by`/`updated_by` audit-only). Two kinds: `window_open` (launched in a new tab from the ???? card grid) and `iframe` (embedded under a parent sidebar menu via `mount_location` and dynamically injected into the sidebar). Both carry `route_params` ? a list of `{key, paramType: login\|theme, source, customValue, themeValue}` descriptors the **frontend** resolves at launch time (login params read from `localStorage.userInfo`; theme params use a literal value) and appends to `url` as query string. `GET /` - list (filters `app_type`/`mount_location`/`enabled_only`/`search`); `GET /{app_id}` - read one (both open to any authenticated user); `POST /`, `PUT /{app_id}`, `DELETE /{app_id}` - **admin only**; iframe apps must carry a `mount_location` (400 otherwise); the unique `name` collides as HTTP 409. Backed by `deerflow.persistence.light_apps` (table `light_apps`, auto-created by `create_all`); store wired in `deps.py` as `app.state.light_app_store`. Tests: `tests/test_light_apps.py`. | -| **Knowledge** (`/api/knowledge`) | Product knowledge base, **global/shared** (no user isolation; writes record `created_by`/`updated_by` for audit). `POST /ingest/thread/{thread_id}` - sediment one thread into a note (reads messages from the checkpointer, `threads:read` owner-checked); `POST /ingest/threads/batch` - batch-sediment the caller's recent threads (`skip_existing` via `.manifest.json` dedup). **Optional model selection**: both ingest endpoints accept `model_name` (overrides `knowledge.extract_model_name` for that capture; threaded through `KnowledgeService.capture_thread` ? `extract_thread_llm_multi`). **Model-error surfacing**: when the chosen model produces nothing usable (timeout / API error / non-JSON) capture still **falls back to rule-based extraction and lands a draft** (`llm_fallback:true`); the single-thread (non-background) response and the batch response (`llm_fallback` count) carry this so the UI tells the user "???????". The "??????" chat button stays background/fire-and-forget (passes `model_name`, no per-save error toast); the batch-import dialog is synchronous and shows the fallback count. `POST /ingest/file` - **import an uploaded file** (multipart: `file` + optional `title`/`tags`/`status`/`model_name`) into wiki note(s): the file is converted to Markdown (md/txt read directly, pdf/word/ppt/excel via `convert_file_to_markdown`), distilled by the same wiki-capture LLM pipeline as thread capture (`source_type="file"`), and falls back to a verbatim **draft** note when the model returns nothing usable (`llm_fallback:true`); `POST /ingest/search` - sediment search/tool results as a reference note; `POST /notes` - manual note; `GET /notes` (keyword/tag/source_type/status + pagination); `GET/PUT/DELETE /notes/{id}` (DELETE is soft = archive, `?hard=true` removes the vault file); `POST /search` - `mode=keyword\|vector\|hybrid` (vector?keyword fallback when no embedding endpoint); `GET /notes/{id}/versions` - edit history; `GET /duplicates` + `POST /merge` - dedup; `POST /reindex` - rebuild vector index; `GET /graph` - product graph data (notes + entities, graph-blacklist filtered); `GET/POST /graph/blacklist` + `DELETE /graph/blacklist/{id}` - graph node blacklist (labels = entity names / note titles, case-insensitive match; blacklisted nodes and their edges are dropped from `/graph` and the wiki-export artifacts; table `knowledge_graph_blacklist`, auto-created); `POST /export/graph` + `GET /export/files/{graph.json\|graph.html\|graph.graphml\|cypher.txt}` - obsidian-wiki `wiki-export` artifacts (self-contained `graph.html` for iframe); `POST /resolve` `{targets:[?]}` + `GET /notes/{id}/wikilinks` - resolve `[[wikilink]]` targets to a note id / vault page / missing (powers clickable wikilinks); `GET /vault-page?path=` - read a raw vault meta/hub Markdown page (no DB note). Backed by the `deerflow.knowledge` module (see below). | - -Proxied through nginx: `/api/langgraph/*` ? LangGraph, all other `/api/*` ? Gateway. - -### AI Writing Built-in Functional Agents (????????) - -The AI-writing graph (`packages/harness/deerflow/agents/ai_writing/`) has four stage "brains" ? ?????? / ????? / ?? / ?? ? that can each run in one of two modes, switched per-stage by `config.yaml ? ai_writing.use_builtin_agents.{researcher,outliner,writer,editor}` (default **false**, mtime hot-reloaded, new sessions pick up changes immediately): - -- **Legacy (flag off)**: hard-coded prompts in `prompts/*.py`, direct LLM call (unchanged behavior). -- **Builtin-agent (flag on)**: the node loads the corresponding functional agent from `.deer-flow/agents/ai-writing-/` ? `SOUL.md` becomes the system prompt and `config.yaml`'s `model` overrides the stage model (priority: `agent.model > node explicit fallback (keyword_model/rank_model) > state.model_name`). Admins edit SOUL/model from the agent management page. Runner: `nodes/_agent_runner.py` (`run_writing_agent` / `stream_writing_agent` / `parse_marker_json`). Output contracts (marker + ```json``` blocks, intent.py-style): researcher `[KEYWORDS_READY]` / `[MATERIALS_RANKED]`, outliner `[OUTLINE_READY]`, editor (per-dimension) `[REVIEW_READY]`; writer emits raw section Markdown (keeps per-section streaming) with the legacy `[[SECTION_BLOCKED]]` sentinel in strict mode. **Orchestration stays in the nodes** (search fan-out, per-section loop, 3-dimension review loop, all pause points) ? the agent only does single structured generations, and a missing/empty SOUL silently falls back to the legacy path (`WritingAgentUnavailable`). - -Seeding mirrors the roundtable pattern: verified `config.yaml` + `SOUL.md` ship in `app/gateway/routers/_ai_writing_seed_assets//`; `_ai_writing_seed.py::ensure_ai_writing_functional_agents()` copies them to `.deer-flow/agents/` when missing (never overwrites local edits), called from the startup lifespan (before `_sync_legacy_agents`, so the agents appear in `/api/agents` on first boot) and self-heals from `GET /api/ai-writing/sessions` + `GET /api/ai-writing/article-types`. Tests: `tests/test_ai_writing_seed.py`, `tests/test_ai_writing_agent_runner.py`. - -**Skill-driven retrieval (检索类型).** `material_source` has exactly two values: `general` (通用检索, default) and `notebook` (我的空间); legacy `knowledge_base`/`skill` are normalized to `general` by the researcher node. General retrieval is orchestrated from the **skills configured on the `ai-writing-researcher` agent** (`config.yaml → skills`, editable from the agent management page): the built-in `knowledge-base-search` skill (知识库检索) runs the native keyword fan-out web/intranet search (the old default path), while any other configured skill searches with a `description`-targeted query; skills run in configured order with early-stop at `max_materials`, and an empty/unresolvable skills config falls back to the built-in knowledge search so retrieval always works. The skill itself is seeded from `_ai_writing_seed_assets/skills/knowledge-base-search/SKILL.md` into `skills/public/` (never overwrites), and a pre-existing researcher `config.yaml` lacking the `skills` key is patched in place with the default (explicit `skills: []` is respected). **PAUSE-1 input append flow**: when the user types natural language into the material-confirm input (`re_search`/legacy `enable_skill_search` + `user_query`), the node matches the most relevant configured skill via `SkillPicker(candidates=…)` (skipped when only one skill is configured), searches with the user's words, and **appends** the new materials to the existing package (URL-dedup, original order preserved, no LLM re-rank/truncation, 100-item hard cap). Tests: `tests/test_ai_writing_material_source.py`. - -### Admin Leaderboard Snapshots (`/api/admin/users/leaderboard*`) - -The admin ????? is served from **precomputed per-day snapshots** in `admin_leaderboard_daily_stats` (keyed by `(stat_date, settings_hash)`), not by aggregating raw rows on every request. A background scheduler inside the Gateway (`_leaderboard_snapshot_scheduler_loop` in `app/gateway/app.py`, singleton file-locked) ticks every `ADMIN_LEADERBOARD_BACKGROUND_INTERVAL_SECONDS` (default 300s): it prewarms recent days, then builds any `queued`/`error`/stale-`running` day. `GET /leaderboard` merges the per-day snapshots for the requested range. **Today** is rebuilt whenever its snapshot is older than `ADMIN_LEADERBOARD_TODAY_TTL_SECONDS` (default 43200s = half a day, so the leaderboard refreshes ~twice daily); a past day, once `ready`, is treated as fresh forever. - -Two on-demand/automatic refresh paths layer on top: -- **Manual "????"** ? `POST /leaderboard/refresh-today` force-rebuilds *today's* snapshot **synchronously** (bounded by the same semaphore/timeout the scheduler uses), then returns the merged range. Lets an admin pull the latest real-time records into the stats without waiting for the TTL. Frontend: a "????" button on `AdminUserLeaderboardPage` ? `useRefreshTodayLeaderboard` (`core/admin/hooks.ts`), which primes + invalidates the `["admin-activity","leaderboard"]` query. -- **Daily finalize of the previous day** ? once per day, on/after `ADMIN_LEADERBOARD_FINALIZE_HOUR` (local Beijing, default `1` = ?? 1 ?), the scheduler tick force-recomputes *yesterday* a single time so runs/tool-calls that only landed near midnight are fully captured. The decision is the pure `deerflow.persistence.admin_stats.finalize.should_finalize_previous_day` (status/generated_at/now/hour) ? the rebuild stamps `generated_at` to today, which self-debounces it for the rest of the day (no extra state file). Tests: `tests/test_leaderboard_finalize.py`. - -### Scheduled Task Runtime - -`ScheduledTaskService` (`deerflow/runtime/scheduler/service.py`) polls due tasks every `poll_interval_seconds` and executes each in its own throwaway thread, serialized by a shared `_agent_run_lock`. - -**Stuck-run recovery**: each execution attempt is bounded by `_RUN_ATTEMPT_TIMEOUT_SECONDS` (30 min) via `asyncio.wait_for`. On timeout the run is retried up to `_MAX_RUN_RETRIES` (3) times, then marked `failed`. A watchdog (`_reconcile_stuck_runs`, run every poll) recovers runs left in `running` with no in-process owner ? e.g. orphaned by a crash or restart ? re-driving them through the same retry budget so a task can never stay perpetually "executing". The `scheduled_task_runs.retry_count` column tracks consumed retries. - -**Unique task names**: task names are unique per user (`uq_scheduled_tasks_user_name`). `create_task`/`update_task`/`admin_update_task` proactively check via `_assert_name_available()` and raise `DuplicateTaskNameError` on a collision; the router translates that into HTTP `409` instead of letting the raw DB `IntegrityError` surface as a `500`. - -**HTML page tasks (`task_kind == "html_page"`)**: a second task kind that renders a full HTML document each run instead of a Markdown report. The kind + its config (`html_config`: `page_prompt`, `negative_prompt`, `reference_favorite_id`, `model_name`) live inside `execution_context` ? no schema change to `scheduled_tasks`. Data gathering is delegated to the agent's own search skills (driven by `page_prompt`), so there is no explicit search-query/data-source field; the render model is user-selectable (`model_name`, default `zai-org/GLM-5-FP8`). `_run_task_attempt()` dispatches by kind; `_run_html_page_attempt()` runs the pipeline: gather real data via the lead agent (its search tools) ? optionally load a reference favorite's reusable style/layout ? render with the chosen model (thinking off) ? fault-tolerant JSON parse ? sanitize (BeautifulSoup, strips `
/report`): the Step 3 report (a + * in-app Step 3「最终方案展示」— 结果绘制/大屏多页入口 + 方案总结报告 + 总控最终 + * 结论 + 各席位最终交付. + * - report (`#/roundtable-share//report`): the Step 3 经典 HTML 看板 (a * self-contained HTML document) rendered full-screen inside a sandboxed - * `iframe` (no `allow-scripts`), so the agent-generated HTML can never execute - * code in the app origin. + * `iframe` (no `allow-scripts`). + * - dashboard (`#/roundtable-share//dashboard`): the Step 3 大屏多页 (模式二) + * — rendered on the fly from the stored structured data via `renderDashboardHtml` + * into the same sandboxed `iframe`. */ export default function PublicRoundtableSharePage({ - report = false, + view = "landing", }: { - report?: boolean; + view?: RoundtableShareView; }) { const { shareCode } = useParams<{ shareCode: string }>(); // 嵌套方可用 ?bg=/?text= 或 postMessage 覆盖背景/文字色。 @@ -108,6 +118,17 @@ export default function PublicRoundtableSharePage({ refetchOnWindowFocus: false, }); + // 大屏多页:用与应用内一致的 React 大屏(DashboardApp)渲染,而非自包含 HTML 模板。 + // 把存储的结构化数据(report-json)解析并 adapt 成 DashboardApp 的入参;与 + // Step3DashboardMode 的「应用渲染」、DashboardStandalonePage 的「新窗口打开」同源。 + const dashboardData = useMemo(() => { + const json = data?.dashboard_json; + if (!json) return undefined; + const parsed = parseReportJson(json); + if (!parsed) return undefined; + return adaptReportData(parsed, data?.generated_at ?? ""); + }, [data?.dashboard_json, data?.generated_at]); + if (isLoading) { return ( @@ -128,8 +149,24 @@ export default function PublicRoundtableSharePage({ ); } - // ── 报告页:整页沙箱 iframe 展示第 3 步结果绘制 ────────────────────────────── - if (report) { + // ── 大屏多页:整屏渲染应用内 React 大屏(DashboardApp),与应用渲染/新窗口打开一致 ── + if (view === "dashboard") { + return ( + + {dashboardData ? ( + + ) : ( + + 暂无大屏多页 + 这次会商还没有生成第三步「大屏多页」的方案报告。 + + )} + + ); + } + + // ── 结果绘制(经典 HTML 看板):整页沙箱 iframe 展示自包含报告 ───────────────── + if (view === "report") { return ( @@ -161,8 +198,10 @@ export default function PublicRoundtableSharePage({ ); } - // ── 落地页:总控最终结论 + 各席位最终交付 + 查看结果绘制(新标签打开报告页)───── - const reportUrl = `${window.location.origin}${window.location.pathname}#/roundtable-share/${shareCode}/report`; + // ── 落地页:结果绘制/大屏多页入口 + 方案总结报告 + 总控最终结论 + 各席位最终交付 ── + const shareBase = `${window.location.origin}${window.location.pathname}#/roundtable-share/${shareCode}`; + const reportUrl = `${shareBase}/report`; + const dashboardUrl = `${shareBase}/dashboard`; return ( @@ -182,23 +221,39 @@ export default function PublicRoundtableSharePage({ - {/* 1. 结果绘制 —— 新标签打开报告页 */} + {/* 1. 结果绘制 / 大屏多页 —— 新标签打开整页报告 */} 结果绘制 - {data.has_report ? ( - - - 查看结果绘制 - - + {data.has_report || data.has_dashboard ? ( + + {data.has_report && ( + + + 查看结果绘制 + + + )} + {data.has_dashboard && ( + + + 查看大屏多页 + + + )} + ) : ( 这次会商还没有生成第三步「结果绘制」的方案报告。 @@ -206,7 +261,22 @@ export default function PublicRoundtableSharePage({ )} - {/* 2. 总控最终结论 */} + {/* 2. 方案总结报告(Markdown) */} + {data.has_summary_report && ( + + + + 方案总结报告 + + + + {data.summary_report_md} + + + + )} + + {/* 3. 总控最终结论 */} @@ -223,7 +293,7 @@ export default function PublicRoundtableSharePage({ )} - {/* 3. 各席位最终交付 */} + {/* 4. 各席位最终交付 */} diff --git a/frontend-web/src/pages/light-app-management.css b/frontend-web/src/pages/light-app-management.css new file mode 100644 index 00000000..bc3eafc0 --- /dev/null +++ b/frontend-web/src/pages/light-app-management.css @@ -0,0 +1,570 @@ +@import "../../docs/ui规范-深蓝/tokens.css"; +@import "./light-app-pages.css"; + +/** + * 应用管理页 —— 深蓝色主题适配(《深蓝色 UI 规范》) + * 浅色样式仍由组件内 Tailwind 承担;.dark 下本文件覆盖为 ZS-004 / ZS-002 令牌。 + */ +.dark .light-app-management-page { + color: var(--color-text); +} + +/* ── 页头 / 文案 ─────────────────────────────────────────────────────── */ +.dark .light-app-management-page .lam-title { + color: var(--color-text-strong); +} + +.dark .light-app-management-page .lam-desc { + color: var(--color-text-secondary); +} + +.dark .light-app-management-page .lam-desc code { + background: rgba(22, 119, 255, 0.12); + color: var(--color-link); +} + +.dark .light-app-management-page .lam-primary-btn { + background: var(--color-primary); + box-shadow: none; +} + +.dark .light-app-management-page .lam-primary-btn:hover { + background: var(--color-primary-hover); +} + +.dark .light-app-management-page .lam-primary-btn:active { + background: var(--color-primary-active); +} + +/* ── 工具条 / 搜索 / 视图切换 ─────────────────────────────────────────── */ +.dark .light-app-management-page .lam-toolbar { + background: var(--color-bg-card); + border: 1px solid var(--color-border); + box-shadow: var(--shadow-1); +} + +.dark .light-app-management-page .lam-search-input { + background: var(--color-bg-page); + border-color: var(--color-border); + color: var(--color-text); +} + +.dark .light-app-management-page .lam-search-input::placeholder { + color: var(--color-text-disabled); +} + +.dark .light-app-management-page .lam-search-input:hover { + background: var(--color-bg-card); + border-color: var(--color-link); +} + +.dark .light-app-management-page .lam-search-input:focus { + background: var(--color-bg-card); + border-color: var(--color-primary); + box-shadow: 0 0 0 2px rgba(22, 119, 255, 0.16); + outline: none; +} + +.dark .light-app-management-page .lam-view-toggle { + background: var(--color-bg-page); + border-color: var(--color-border); +} + +.dark .light-app-management-page .lam-view-toggle-btn.is-active { + background: var(--color-bg-card); + color: var(--color-link); + box-shadow: var(--shadow-1); +} + +.dark .light-app-management-page .lam-view-toggle-btn:not(.is-active) { + color: var(--color-text-secondary); +} + +.dark .light-app-management-page .lam-view-toggle-btn:not(.is-active):hover { + color: var(--color-text); +} + +.dark .light-app-management-page .lam-count { + color: var(--color-text-secondary); +} + +.dark .light-app-management-page .lam-count strong { + color: var(--color-link); +} + +/* ── 表格(§28)──────────────────────────────────────────────────────── */ +.dark .light-app-management-page .lam-table-wrap { + background: var(--color-bg-page); + border: 1px solid var(--color-border); + box-shadow: var(--shadow-1); +} + +.dark .light-app-management-page .lam-table thead tr { + background: var(--color-bg-card); + border-color: var(--color-border); + color: var(--color-text); +} + +.dark .light-app-management-page .lam-table tbody { + border-color: var(--color-border); +} + +.dark .light-app-management-page .lam-table tbody tr { + border-color: var(--color-border); +} + +.dark .light-app-management-page .lam-table tbody tr:hover { + background: rgba(22, 119, 255, 0.08); +} + +.dark .light-app-management-page .lam-row-name { + color: var(--color-text); +} + +.dark .light-app-management-page .lam-table tbody tr:hover .lam-row-name { + color: var(--color-link); +} + +.dark .light-app-management-page .lam-tag-window { + border-color: rgba(245, 34, 45, 0.35); + background: var(--func-error-bg); + color: #ff7875; +} + +.dark .light-app-management-page .lam-tag-iframe { + border-color: rgba(22, 119, 255, 0.35); + background: var(--func-info-bg); + color: var(--color-link); +} + +.dark .light-app-management-page .lam-url { + color: var(--color-text-secondary); +} + +.dark .light-app-management-page .lam-table tbody tr:hover .lam-url { + color: var(--color-text); +} + +.dark .light-app-management-page .lam-url a { + color: inherit; +} + +.dark .light-app-management-page .lam-url a:hover { + color: var(--color-link); +} + +.dark .light-app-management-page .lam-mount-tag { + background: var(--func-info-bg); + color: var(--color-link); +} + +.dark .light-app-management-page .lam-mount-dot { + background: var(--color-primary); +} + +.dark .light-app-management-page .lam-param-btn { + border-color: var(--color-border); + background: var(--color-bg-page); + color: var(--color-text-secondary); +} + +.dark .light-app-management-page .lam-param-btn:hover { + background: rgba(22, 119, 255, 0.08); + color: var(--color-text); +} + +.dark .light-app-management-page .lam-param-popover { + border-color: var(--color-border); + background: var(--color-bg-card); + box-shadow: var(--shadow-3); +} + +.dark .light-app-management-page .lam-param-popover-head { + border-color: var(--color-border); + color: var(--color-text-secondary); +} + +.dark .light-app-management-page .lam-param-popover-close { + color: var(--color-link); +} + +.dark .light-app-management-page .lam-param-item { + border-color: var(--color-border); + background: var(--color-bg-page); +} + +.dark .light-app-management-page .lam-param-key { + color: #ff7875; +} + +.dark .light-app-management-page .lam-param-val { + color: var(--color-text-secondary); +} + +.dark .light-app-management-page .lam-status-on { + color: var(--func-success); +} + +.dark .light-app-management-page .lam-status-on-dot { + background: var(--func-success); +} + +.dark .light-app-management-page .lam-status-on-ping { + background: var(--func-success); +} + +.dark .light-app-management-page .lam-status-off { + color: var(--color-text-disabled); +} + +.dark .light-app-management-page .lam-status-off-dot { + background: var(--color-text-disabled); +} + +.dark .light-app-management-page .lam-icon-btn { + color: var(--color-text-secondary); +} + +.dark .light-app-management-page .lam-icon-btn:hover { + background: rgba(22, 119, 255, 0.08); + color: var(--color-link); +} + +.dark .light-app-management-page .lam-icon-btn-danger:hover { + background: var(--func-error-bg); + color: var(--func-error); +} + +.dark .light-app-management-page .lam-empty-icon { + background: var(--color-bg-page); + border: 1px solid var(--color-border); + color: var(--color-text-disabled); +} + +.dark .light-app-management-page .lam-empty-title { + color: var(--color-text-secondary); +} + +.dark .light-app-management-page .lam-empty-sub { + color: var(--color-text-disabled); +} + +/* ── 卡片视图(§22 / §43)────────────────────────────────────────────── */ +.dark .light-app-management-page .lam-card { + background: var(--color-bg-card); + border: 1px solid var(--color-border); + box-shadow: var(--shadow-1); +} + +.dark .light-app-management-page .lam-card:hover { + box-shadow: var(--shadow-2); +} + +.dark .light-app-management-page .lam-card-title { + color: var(--color-text-strong); +} + +.dark .light-app-management-page .lam-card:hover .lam-card-title { + color: var(--color-link); +} + +.dark .light-app-management-page .lam-card-desc { + color: var(--color-text-secondary); +} + +.dark .light-app-management-page .lam-card-url-box { + background: var(--color-bg-page); +} + +.dark .light-app-management-page .lam-card-param { + border-color: var(--color-border); + background: var(--color-bg-page); + color: var(--color-text-secondary); +} + +.dark .light-app-management-page .lam-card-mount { + background: var(--func-info-bg); + color: var(--color-link); +} + +.dark .light-app-management-page .lam-card-footer { + border-color: var(--color-border); + background: rgba(12, 24, 54, 0.5); +} + +/* ── 底部提示(§33 警告)────────────────────────────────────────────── */ +.dark .light-app-management-page .lam-tip-box { + background: var(--func-warning-bg); + border: 1px solid rgba(250, 173, 20, 0.25); + box-shadow: none; +} + +.dark .light-app-management-page .lam-tip-head { + border-color: rgba(250, 173, 20, 0.2); + color: var(--func-warning); +} + +.dark .light-app-management-page .lam-tip-list { + color: var(--color-text-secondary); +} + +.dark .light-app-management-page .lam-tip-list strong { + color: var(--color-text); +} + +.dark .light-app-management-page .lam-tip-bullet { + color: var(--func-warning); +} + +/* ── 弹窗(§21 Modal)────────────────────────────────────────────────── */ +.dark .light-app-management-page .lam-dialog-overlay { + background: rgba(6, 14, 36, 0.6); +} + +.dark .light-app-management-page .lam-dialog { + border-color: var(--color-border); + background: linear-gradient(180deg, var(--color-bg-card), var(--color-bg-page)); + box-shadow: var(--shadow-3); + color: var(--color-text); +} + +.dark .light-app-management-page .lam-dialog-header { + border-color: var(--color-border); + border-bottom: none; + padding-bottom: 0; +} + +.dark .light-app-management-page .lam-dialog-header::after { + content: ""; + display: block; + height: 1px; + margin-top: 16px; + margin-bottom: 0; + background: var(--gradient-title-line); +} + +.dark .light-app-management-page .lam-dialog-icon { + background: var(--func-info-bg); + color: var(--color-link); +} + +.dark .light-app-management-page .lam-dialog-title { + color: var(--color-text-strong); +} + +.dark .light-app-management-page .lam-dialog-close { + color: var(--color-text-secondary); +} + +.dark .light-app-management-page .lam-dialog-close:hover { + background: rgba(22, 119, 255, 0.08); + color: var(--color-link); +} + +.dark .light-app-management-page .lam-form-label { + color: var(--color-text); +} + +.dark .light-app-management-page .lam-form-label .lam-required { + color: var(--func-error); +} + +.dark .light-app-management-page .lam-form-label svg { + color: var(--color-text-secondary); +} + +.dark .light-app-management-page .lam-form-hint { + color: var(--color-text-secondary); +} + +.dark .light-app-management-page .lam-dialog .lam-input, +.dark .light-app-management-page .lam-dialog input[type="text"] { + background: var(--color-bg-page); + border-color: var(--color-border); + color: var(--color-text); +} + +.dark .light-app-management-page .lam-dialog .lam-input::placeholder, +.dark .light-app-management-page .lam-dialog input[type="text"]::placeholder { + color: var(--color-text-disabled); +} + +.dark .light-app-management-page .lam-dialog .lam-input:focus, +.dark .light-app-management-page .lam-dialog input[type="text"]:focus { + border-color: var(--color-primary); + box-shadow: 0 0 0 2px rgba(22, 119, 255, 0.16); + outline: none; +} + +.dark .light-app-management-page .lam-param-panel { + border-color: var(--color-border); + background: var(--color-bg-page); +} + +.dark .light-app-management-page .lam-param-row { + border-color: var(--color-border); + background: var(--color-bg-card); +} + +.dark .light-app-management-page .lam-param-theme-box { + background: var(--color-bg-page); +} + +.dark .light-app-management-page .lam-param-empty { + border-color: var(--color-border); + background: var(--color-bg-page); + color: var(--color-text-secondary); +} + +.dark .light-app-management-page .lam-add-param-btn { + border-color: var(--color-border-strong); + background: var(--color-bg-card); + color: var(--color-text); +} + +.dark .light-app-management-page .lam-add-param-btn:hover { + border-color: var(--color-primary); + color: var(--color-link); +} + +.dark .light-app-management-page .lam-dialog-footer { + border-color: var(--color-border); +} + +.dark .light-app-management-page .lam-dialog-cancel { + border-color: var(--color-border-strong); + background: transparent; + color: var(--color-text); +} + +.dark .light-app-management-page .lam-dialog-cancel:hover { + border-color: var(--color-link); + color: var(--color-link); +} + +.dark .light-app-management-page .lam-dialog-save { + background: var(--color-primary); + box-shadow: none; +} + +.dark .light-app-management-page .lam-dialog-save:hover { + background: var(--color-primary-hover); +} + +.dark .light-app-management-page .lam-dialog-save:active { + background: var(--color-primary-active); +} + +.dark .light-app-management-page .lam-switch-on { + background: var(--color-primary); +} + +.dark .light-app-management-page .lam-switch-off { + background: var(--color-text-disabled); +} + +/* ── TDesign Select(§15)──────────────────────────────────────────── */ +.dark .light-app-management-page .t-select .t-input, +.dark .light-app-management-page .t-select .t-input__wrap { + background: var(--color-bg-page); + border-color: var(--color-border); + border-radius: var(--radius-input); +} + +.dark .light-app-management-page .t-select .t-input__inner { + color: var(--color-text); +} + +.dark .light-app-management-page .t-select .t-input__inner::placeholder { + color: var(--color-text-disabled); +} + +.dark .light-app-management-page .t-select:hover .t-input, +.dark .light-app-management-page .t-select:hover .t-input__wrap { + border-color: var(--color-link); +} + +.dark .light-app-management-page .t-select.t-is-focused .t-input, +.dark .light-app-management-page .t-select.t-is-focused .t-input__wrap { + border-color: var(--color-primary); + box-shadow: 0 0 0 2px rgba(22, 119, 255, 0.16); +} + +.dark .light-app-management-page .t-select .t-fake-arrow { + color: var(--color-text-secondary); +} + +.dark .light-app-management-page .t-select__dropdown { + background: var(--color-bg-card); + border-color: var(--color-border); + box-shadow: var(--shadow-2); +} + +.dark .light-app-management-page .t-select-option { + color: var(--color-text); +} + +.dark .light-app-management-page .t-select-option:hover { + background: rgba(22, 119, 255, 0.08); +} + +.dark .light-app-management-page .t-select-option.t-is-selected { + background: rgba(22, 119, 255, 0.16); + color: var(--color-link); +} + +/* shadcn Input / Textarea in dialog */ +.dark .light-app-management-page .lam-dialog [data-slot="input"], +.dark .light-app-management-page .lam-dialog textarea { + background: var(--color-bg-page); + border-color: var(--color-border); + color: var(--color-text); +} + +.dark .light-app-management-page .lam-dialog [data-slot="input"]:focus-visible, +.dark .light-app-management-page .lam-dialog textarea:focus-visible { + border-color: var(--color-primary); + box-shadow: 0 0 0 2px rgba(22, 119, 255, 0.16); +} + +.dark .light-app-management-page .lam-param-theme-box span { + color: var(--color-text-secondary); +} + +/* TDesign 下拉挂载到 body 时的深蓝样式 */ +.dark .lam-select-popup.t-popup { + background: var(--color-bg-card); + border-color: var(--color-border); + box-shadow: var(--shadow-2); +} + +.dark .lam-select-popup .t-select-option { + color: var(--color-text); +} + +.dark .lam-select-popup .t-select-option:hover { + background: rgba(22, 119, 255, 0.08); +} + +.dark .lam-select-popup .t-select-option.t-is-selected { + background: rgba(22, 119, 255, 0.16); + color: var(--color-link); +} + +.dark .light-app-management-page .lam-search-input + button, +.dark .light-app-management-page .lam-toolbar .absolute.text-neutral-400 { + color: var(--color-text-secondary); +} + +.dark .light-app-management-page .lam-table .text-neutral-300 { + color: var(--color-text-disabled); +} + +.dark .light-app-management-page .lam-card-url-box button { + color: var(--color-text-secondary); +} + +.dark .light-app-management-page .lam-card-url-box button:hover { + background: rgba(22, 119, 255, 0.08); + color: var(--color-link); +} diff --git a/frontend-web/src/pages/light-app-pages.css b/frontend-web/src/pages/light-app-pages.css new file mode 100644 index 00000000..459be997 --- /dev/null +++ b/frontend-web/src/pages/light-app-pages.css @@ -0,0 +1,12 @@ +@import "../../docs/ui规范-深蓝/tokens.css"; + +/** 轻应用中心列表 / 管理页 —— 深色模式共用页面底图(与 workspace 深蓝主题一致) */ +.dark .light-app-management-page, +.dark .light-app-list-page { + background-color: var(--color-bg-page); + background-image: url("../assets/img/layout-bg-theme-dark-blue.svg"); + background-size: cover; + background-position: center top; + background-repeat: no-repeat; + background-attachment: fixed; +} diff --git a/frontend-web/src/roundtable-planning/components/HighFidelityReport.tsx b/frontend-web/src/roundtable-planning/components/HighFidelityReport.tsx index 533f8ba9..f12a0bb5 100644 --- a/frontend-web/src/roundtable-planning/components/HighFidelityReport.tsx +++ b/frontend-web/src/roundtable-planning/components/HighFidelityReport.tsx @@ -1009,6 +1009,7 @@ export const HighFidelityReport: React.FC = ({ filepath={fileSandboxVirtualUrl} threadId={fileSandboxTarget.threadId} onClose={handleCloseFileSandbox} + allowVirtualMarkdownEdit /> )} diff --git a/frontend-web/src/roundtable-planning/components/Step2SandboxLayout.tsx b/frontend-web/src/roundtable-planning/components/Step2SandboxLayout.tsx index 1d69dea8..59585fa9 100644 --- a/frontend-web/src/roundtable-planning/components/Step2SandboxLayout.tsx +++ b/frontend-web/src/roundtable-planning/components/Step2SandboxLayout.tsx @@ -115,6 +115,7 @@ export function Step2SandboxLayout({ filepath={selectedArtifact} threadId={previewThreadId} onClose={artifactsFill && onArtifactsClose ? handleArtifactsClose : undefined} + allowVirtualMarkdownEdit /> ) : ( diff --git a/frontend-web/src/strategy-components/Header.tsx b/frontend-web/src/strategy-components/Header.tsx index 7e4f144f..b27f18a5 100644 --- a/frontend-web/src/strategy-components/Header.tsx +++ b/frontend-web/src/strategy-components/Header.tsx @@ -139,6 +139,10 @@ const Header = ({ collapsed }: HeaderProps) => { return email ? email.split("@")[0] : "用户"; }, [auth?.email]); const showTopNav = isDark && !collapsed && !isWujieEnv(); + // 浅色(红/蓝)顶栏:一级导航正常横排进顶栏,不做深色那种围绕基座的特殊处理。 + // 与 collapsed 解耦(折叠只影响侧边栏,一级导航始终可见);wujie 嵌入下隐藏, + // 此时一级导航回落到侧边栏(见 page-sidebar.tsx)。 + const showLightTopNav = !isDark && !isWujieEnv(); const hasActiveChild = (item: MenuItem): boolean => { if (item.path && pathname === item.path) { @@ -194,6 +198,20 @@ const Header = ({ collapsed }: HeaderProps) => { ); }; + const renderLightTopItem = (item: MenuItem) => { + const isSelected = item.path ? pathname === item.path : hasActiveChild(item); + return ( + handleNavItemClick(item)} + > + {item.label} + + ); + }; + // 右侧操作区(换肤/通知/用户)—— 深浅两套布局共用 const actionCluster = ( <> @@ -309,6 +327,27 @@ const Header = ({ collapsed }: HeaderProps) => { > + ) : showLightTopNav ? ( + <> + {/* 浅色顶栏:logo + 一级导航横排(白字落在 banner 上) */} + + + + + + + CM助手 + + + + {visibleMenuItems.map(renderLightTopItem)} + + + + + {actionCluster} + + > ) : ( <> @@ -335,6 +374,7 @@ const Header = ({ collapsed }: HeaderProps) => { > )} + {/* 三级菜单横条仅深色用;浅色三级已改到侧边栏右侧列(见 page-sidebar.tsx) */} {showTopNav && thirdSubNavItems.length ? ( diff --git a/frontend-web/src/strategy-components/lib/light-app.ts b/frontend-web/src/strategy-components/lib/light-app.ts index 481b838b..ab796bf6 100644 --- a/frontend-web/src/strategy-components/lib/light-app.ts +++ b/frontend-web/src/strategy-components/lib/light-app.ts @@ -19,11 +19,17 @@ export function mountLabel(value: string | null | undefined): string { return MOUNT_OPTIONS.find((o) => o.value === value)?.label ?? value; } -/** 登录参数可选来源;值均可在 userInfo 中取到,"other" 表示自填。 */ +/** + * 登录参数可选来源;值均可在 userInfo 中取到,"other" 表示自填。 + * accessToken / yUserId 取登录路由透传值(写入 userInfo 的同名键); + * userName / gateDepartName / typeName 取 consumer 接口 userDetailByYuserId 的返回值。 + */ export const LOGIN_SOURCE_OPTIONS: { label: string; value: string }[] = [ { label: "accessToken", value: "accessToken" }, { label: "yUserId", value: "yUserId" }, { label: "userName", value: "userName" }, + { label: "gateDepartName", value: "gateDepartName" }, + { label: "typeName", value: "typeName" }, { label: "其他(自定义值)", value: "other" }, ]; diff --git a/frontend-web/src/styles/globals.css b/frontend-web/src/styles/globals.css index 7ebd17de..3494b65c 100644 --- a/frontend-web/src/styles/globals.css +++ b/frontend-web/src/styles/globals.css @@ -932,7 +932,7 @@ body { .strategy-app-header { background-color: transparent; background-image: url("../assets/img/red-header.png"); - background-size: 100% 100%; + background-size: cover; background-repeat: no-repeat; background-position: center; } diff --git a/offline-backend-20260512/backend/CLAUDE.md b/offline-backend-20260512/backend/CLAUDE.md index 18e7823d..f6812132 100644 --- a/offline-backend-20260512/backend/CLAUDE.md +++ b/offline-backend-20260512/backend/CLAUDE.md @@ -1,759 +1,759 @@ -# CLAUDE.md -?? -This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. - -## Project Overview - -DeerFlow is a LangGraph-based AI super agent system with a full-stack architecture. The backend provides a "super agent" with sandbox execution, persistent memory, subagent delegation, and extensible tool integration - all operating in per-thread isolated environments. - -**Architecture**: -- **Gateway API** (port 8001): REST API plus embedded LangGraph-compatible agent runtime -- **Frontend** (port 3000): Next.js web interface -- **Nginx** (port 2026): Unified reverse proxy entry point -- **Provisioner** (port 8002, optional in Docker dev): Started only when sandbox is configured for provisioner/Kubernetes mode - -**Runtime**: -- `make dev`, Docker dev, and production all run the agent runtime in Gateway via `RunManager` + `run_agent()` + `StreamBridge` (`packages/harness/deerflow/runtime/`). Nginx exposes that runtime at `/api/langgraph/*` and rewrites it to Gateway's native `/api/*` routers. - -**Project Structure**: -``` -deer-flow/ -??? Makefile # Root commands (check, install, dev, stop) -??? config.yaml # Main application configuration -??? extensions_config.json # MCP servers and skills configuration -??? backend/ # Backend application (this directory) -? ??? Makefile # Backend-only commands (dev, gateway, lint) -? ??? langgraph.json # LangGraph Studio graph configuration -? ??? packages/ -? ? ??? harness/ # deerflow-harness package (import: deerflow.*) -? ? ??? pyproject.toml -? ? ??? deerflow/ -? ? ??? agents/ # LangGraph agent system -? ? ? ??? lead_agent/ # Main agent (factory + system prompt) -? ? ? ??? middlewares/ # 10 middleware components -? ? ? ??? memory/ # Memory V2: provider/manager/builtin/hindsight -? ? ? ??? thread_state.py # ThreadState schema -? ? ??? sandbox/ # Sandbox execution system -? ? ? ??? local/ # Local filesystem provider -? ? ? ??? sandbox.py # Abstract Sandbox interface -? ? ? ??? tools.py # bash, ls, read/write/str_replace -? ? ? ??? middleware.py # Sandbox lifecycle management -? ? ??? subagents/ # Subagent delegation system -? ? ? ??? builtins/ # general-purpose, bash agents -? ? ? ??? executor.py # Background execution engine -? ? ? ??? registry.py # Agent registry -? ? ??? tools/builtins/ # Built-in tools (present_files, ask_clarification, view_image) -? ? ??? mcp/ # MCP integration (tools, cache, client) -? ? ??? models/ # Model factory with thinking/vision support -? ? ??? skills/ # Skills discovery, loading, parsing -? ? ??? config/ # Configuration system (app, model, sandbox, tool, etc.) -? ? ??? community/ # Community tools (tavily, jina_ai, firecrawl, image_search, aio_sandbox) -? ? ??? reflection/ # Dynamic module loading (resolve_variable, resolve_class) -? ? ??? utils/ # Utilities (network, readability) -? ? ??? client.py # Embedded Python client (DeerFlowClient) -? ??? app/ # Application layer (import: app.*) -? ? ??? gateway/ # FastAPI Gateway API -? ? ? ??? app.py # FastAPI application -? ? ? ??? routers/ # FastAPI route modules (models, mcp, memory, skills, uploads, threads, artifacts, agents, suggestions, channels) -? ? ??? channels/ # IM platform integrations -? ??? tests/ # Test suite -? ??? docs/ # Documentation -??? frontend/ # Next.js frontend application -??? skills/ # Agent skills directory - ??? public/ # Public skills (committed) - ??? custom/ # Custom skills (gitignored) -``` - -## Important Development Guidelines - -### Documentation Update Policy -**CRITICAL: Always update README.md and CLAUDE.md after every code change** - -When making code changes, you MUST update the relevant documentation: -- Update `README.md` for user-facing changes (features, setup, usage instructions) -- Update `CLAUDE.md` for development changes (architecture, commands, workflows, internal systems) -- Keep documentation synchronized with the codebase at all times -- Ensure accuracy and timeliness of all documentation - -## Commands - -**Root directory** (for full application): -```bash -make check # Check system requirements -make install # Install all dependencies (frontend + backend) -make dev # Start all services (Gateway + Frontend + Nginx), with config.yaml preflight -make start # Start production services locally -make stop # Stop all services -``` - -**Backend directory** (for backend development only): -```bash -make install # Install backend dependencies -make dev # Run Gateway API with reload (port 8001) -make gateway # Run Gateway API only (port 8001) -make test # Run all backend tests -make lint # Lint with ruff -make format # Format code with ruff -``` - -Regression tests related to Docker/provisioner behavior: -- `tests/test_docker_sandbox_mode_detection.py` (mode detection from `config.yaml`) -- `tests/test_provisioner_kubeconfig.py` (kubeconfig file/directory handling) - -Boundary check (harness ? app import firewall): -- `tests/test_harness_boundary.py` ? ensures `packages/harness/deerflow/` never imports from `app.*` - -CI runs these regression tests for every pull request via [.github/workflows/backend-unit-tests.yml](../.github/workflows/backend-unit-tests.yml). - -## Architecture - -### Harness / App Split - -The backend is split into two layers with a strict dependency direction: - -- **Harness** (`packages/harness/deerflow/`): Publishable agent framework package (`deerflow-harness`). Import prefix: `deerflow.*`. Contains agent orchestration, tools, sandbox, models, MCP, skills, config ? everything needed to build and run agents. -- **App** (`app/`): Unpublished application code. Import prefix: `app.*`. Contains the FastAPI Gateway API and IM channel integrations (Feishu, Slack, Telegram, DingTalk). - -**Dependency rule**: App imports deerflow, but deerflow never imports app. This boundary is enforced by `tests/test_harness_boundary.py` which runs in CI. - -**Import conventions**: -```python -# Harness internal -from deerflow.agents import make_lead_agent -from deerflow.models import create_chat_model - -# App internal -from app.gateway.app import app -from app.channels.service import start_channel_service - -# App ? Harness (allowed) -from deerflow.config import get_app_config - -# Harness ? App (FORBIDDEN ? enforced by test_harness_boundary.py) -# from app.gateway.routers.uploads import ... # ? will fail CI -``` - -### Agent System - -**Lead Agent** (`packages/harness/deerflow/agents/lead_agent/agent.py`): -- Entry point: `make_lead_agent(config: RunnableConfig)` registered in `langgraph.json` -- Dynamic model selection via `create_chat_model()` with thinking/vision support -- Tools loaded via `get_available_tools()` - combines sandbox, built-in, MCP, community, and subagent tools -- System prompt generated by `apply_prompt_template()` with skills, memory, and subagent instructions - -**ThreadState** (`packages/harness/deerflow/agents/thread_state.py`): -- Extends `AgentState` with: `sandbox`, `thread_data`, `title`, `artifacts`, `todos`, `uploaded_files`, `viewed_images` -- Uses custom reducers: `merge_artifacts` (deduplicate), `merge_viewed_images` (merge/clear) - -**Runtime Configuration** (via `config.configurable`): -- `thinking_enabled` - Enable model's extended thinking -- `model_name` - Select specific LLM model -- `is_plan_mode` - Enable TodoList middleware -- `subagent_enabled` - Enable task delegation tool - -### Middleware Chain - -Lead-agent middlewares are assembled in strict append order across `packages/harness/deerflow/agents/middlewares/tool_error_handling_middleware.py` (`build_lead_runtime_middlewares`) and `packages/harness/deerflow/agents/lead_agent/agent.py` (`_build_middlewares`): - -1. **ThreadDataMiddleware** - Creates per-thread directories under the user's isolation scope (`backend/.deer-flow/users/{user_id}/threads/{thread_id}/user-data/{workspace,uploads,outputs}`); resolves `user_id` via `get_effective_user_id()` (falls back to `"default"` in no-auth mode); Web UI thread deletion now follows LangGraph thread removal with Gateway cleanup of the local thread directory -2. **UploadsMiddleware** - Tracks and injects newly uploaded files into conversation -3. **SandboxMiddleware** - Acquires sandbox, stores `sandbox_id` in state -4. **DanglingToolCallMiddleware** - Injects placeholder ToolMessages for AIMessage tool_calls that lack responses (e.g., due to user interruption), including raw provider tool-call payloads preserved only in `additional_kwargs["tool_calls"]` -5. **LLMErrorHandlingMiddleware** - Normalizes provider/model invocation failures into recoverable assistant-facing errors before later middleware/tool stages run -6. **GuardrailMiddleware** - Pre-tool-call authorization via pluggable `GuardrailProvider` protocol (optional, if `guardrails.enabled` in config). Evaluates each tool call and returns error ToolMessage on deny. Three provider options: built-in `AllowlistProvider` (zero deps), OAP policy providers (e.g. `aport-agent-guardrails`), or custom providers. See [docs/GUARDRAILS.md](docs/GUARDRAILS.md) for setup, usage, and how to implement a provider. -7. **SandboxAuditMiddleware** - Audits sandboxed shell/file operations for security logging before tool execution continues -8. **ToolErrorHandlingMiddleware** - Converts tool exceptions into error `ToolMessage`s so the run can continue instead of aborting -9. **SummarizationMiddleware** (`DeerFlowSummarizationMiddleware`) - Context reduction when approaching token limits (optional, if enabled). **Non-destructive**: overrides the stock `before_model` (which persists a `RemoveMessage(REMOVE_ALL_MESSAGES)`) to a no-op and instead compresses **transiently** in `wrap_model_call` ? the model request is replaced with `[summary, *recent]` while the full original conversation stays in state (so the frontend keeps rendering the user's real Q&A). The `summary` message is `hide_from_ui` and never persisted; summaries are cached per-thread (bounded, sticky boundary) to avoid an LLM call on every model invocation. Tests: `tests/test_summarization_middleware.py` -10. **TodoListMiddleware** - Task tracking with `write_todos` tool (optional, if plan_mode) -11. **TokenUsageMiddleware** - Records token usage metrics when token tracking is enabled (optional) -12. **TitleMiddleware** - Two-phase auto-title (never shows "untitled"): `before_model` sets an instant question-derived *provisional* title the moment the first user message arrives (no LLM, zero latency), then `after_model` asynchronously generates the final LLM title and replaces it (tracked via the `title_provisional` state flag). Normalizes structured message content before prompting the title model -13. **MemoryMiddleware** - Memory V2: `wrap_model_call` injects Hindsight recall transiently; `after_agent` auto-retains the turn to Hindsight -14. **ViewImageMiddleware** - Injects base64 image data before LLM call (conditional on vision support) -15. **DeferredToolFilterMiddleware** - Hides deferred tool schemas from the bound model until tool search is enabled (optional) -16. **SubagentLimitMiddleware** - Truncates excess `task` tool calls from model response to enforce `MAX_CONCURRENT_SUBAGENTS` limit (optional, if `subagent_enabled`) -17. **LoopDetectionMiddleware** - Detects repeated tool-call loops; hard-stop responses clear both structured `tool_calls` and raw provider tool-call metadata before forcing a final text answer -18. **ClarificationMiddleware** - Intercepts `ask_clarification` tool calls, interrupts via `Command(goto=END)` (must be last) -8a. **ToolMetricsMiddleware** - Appended immediately after `ToolErrorHandlingMiddleware` in `_build_runtime_middlewares`, so it sits *deepest* in the `wrap_tool_call` chain. Records one `tool_call_metrics` row per tool/skill/MCP invocation ? timing duration and bucketing failures into a coarse `error_category` (rate_limited / ip_blocked / auth_error / timeout / network_error / not_found / empty_result / tool_error). It catches both raised exceptions (before #8 converts them) and *self-handled* errors returned as error `ToolMessage`s (search tools that swallow HTTP 429/403). It also captures the targeted Agent Skill into `tool_call_metrics.skill_name`: the lead agent invokes a skill by `read_file`-ing its `/mnt/skills///SKILL.md` path (per the `` prompt block ? there is no dedicated "use skill" tool), so `skill_name` is recovered by scanning every tool call's string arguments for the `skills/{public,custom}/{name}/` path shape (`_SKILL_PATH_RE`), plus the explicit `name` argument of `skill_view`/`skill_manage`/`skill_archive`. `skill_view`-specific success detection handles its swallowed failures (`Skill 'x' not found.` returned as a plain string rather than raised). Regression coverage: `tests/test_tool_metrics_middleware.py`. Writes are best-effort fire-and-forget. **Per-run skill merging**: `SqlToolMetricsStore.record` upserts skill rows by `(run_id, skill_name)` instead of inserting one row per call ? a skill touched by several tool calls / retries inside one run collapses to a single row. Any success in the run wins (a skill that failed then finally succeeded is recorded as one `success`); a skill that only ever failed yields one `error` row whose `error_message` accumulates every attempt's message; durations are summed. Non-skill tool calls, and skill calls with no `run_id`, keep one row each. The read?merge?write is guarded by a per-event-loop `asyncio.Lock`. Surfaced by the admin `GET /api/admin/tool-metrics` router (which has a `kind=skill` filter and a per-skill `/summary` breakdown). -9. **PromptPrefixMiddleware** - Injects the admin-configured prompt prefix into the model request only. The prefix is tagged onto the first human message's `additional_kwargs['prompt_prefix']` by the Gateway (`app.gateway.services`) and prepended to content here at call time, so the persisted/displayed message stays prefix-free -10. **SummarizationMiddleware** (`DeerFlowSummarizationMiddleware`) - Context reduction when approaching token limits (optional, if enabled). **Non-destructive**: overrides the stock `before_model` (which persists a `RemoveMessage(REMOVE_ALL_MESSAGES)`) to a no-op and instead compresses **transiently** in `wrap_model_call` ? the model request is replaced with `[summary, *recent]` while the full original conversation stays in state (so the frontend keeps rendering the user's real Q&A). The `summary` message is `hide_from_ui` and never persisted; summaries are cached per-thread (bounded, sticky boundary) to avoid an LLM call on every model invocation. Tests: `tests/test_summarization_middleware.py` -11. **TodoListMiddleware** - Task tracking with `write_todos` tool (optional, if plan_mode) -12. **TokenUsageMiddleware** - Records token usage metrics when token tracking is enabled (optional) -13. **TitleMiddleware** - Two-phase auto-title (never shows "untitled"): `before_model` sets an instant question-derived *provisional* title the moment the first user message arrives (no LLM, zero latency), then `after_model` asynchronously generates the final LLM title and replaces it (tracked via the `title_provisional` state flag). Normalizes structured message content before prompting the title model -14. **MemoryMiddleware** - Queues conversations for async memory update (filters to user + final AI responses) -15. **ViewImageMiddleware** - Injects base64 image data before LLM call (conditional on vision support) -16. **DeferredToolFilterMiddleware** - Hides deferred tool schemas from the bound model until tool search is enabled (optional) -17. **SubagentLimitMiddleware** - Truncates excess `task` tool calls from model response to enforce `MAX_CONCURRENT_SUBAGENTS` limit (optional, if `subagent_enabled`) -18. **LoopDetectionMiddleware** - Detects repeated tool-call loops; hard-stop responses clear both structured `tool_calls` and raw provider tool-call metadata before forcing a final text answer -19. **ClarificationMiddleware** - Intercepts `ask_clarification` tool calls, interrupts via `Command(goto=END)` (must be last) - -### Configuration System - -**Main Configuration** (`config.yaml`): - -Setup: Copy `config.example.yaml` to `config.yaml` in the **project root** directory. - -**Config Versioning**: `config.example.yaml` has a `config_version` field. On startup, `AppConfig.from_file()` compares user version vs example version and emits a warning if outdated. Missing `config_version` = version 0. Run `make config-upgrade` to auto-merge missing fields. When changing the config schema, bump `config_version` in `config.example.yaml`. - -**Config Caching**: `get_app_config()` caches the parsed config, but automatically reloads it when the resolved config path changes or the file's mtime increases. This keeps Gateway and LangGraph reads aligned with `config.yaml` edits without requiring a manual process restart. - -Configuration priority: -1. Explicit `config_path` argument -2. `DEER_FLOW_CONFIG_PATH` environment variable -3. `config.yaml` in current directory (backend/) -4. `config.yaml` in parent directory (project root - **recommended location**) - -Config values starting with `$` are resolved as environment variables (e.g., `$OPENAI_API_KEY`). -`ModelConfig` also declares `use_responses_api` and `output_version` so OpenAI `/v1/responses` can be enabled explicitly while still using `langchain_openai:ChatOpenAI`. - -**Extensions Configuration** (`extensions_config.json`): - -MCP servers and skills are configured together in `extensions_config.json` in project root: - -Configuration priority: -1. Explicit `config_path` argument -2. `DEER_FLOW_EXTENSIONS_CONFIG_PATH` environment variable -3. `extensions_config.json` in current directory (backend/) -4. `extensions_config.json` in parent directory (project root - **recommended location**) - -### Gateway API (`app/gateway/`) - -FastAPI application on port 8001 with health check at `GET /health`. Set `GATEWAY_ENABLE_DOCS=false` to disable `/docs`, `/redoc`, and `/openapi.json` in production (default: enabled). - -**Routers**: - -| Router | Endpoints | -|--------|-----------| -| **Models** (`/api/models`) | `GET /` - list models; `GET /{name}` - model details | -| **MCP** (`/api/mcp`) | `GET /config` - get config; `PUT /config` - update config (saves to extensions_config.json) | -| **Skills** (`/api/skills`) | `GET /` - list skills; `GET /{name}` - details; `PUT /{name}` - update enabled; `POST /install` - install from .skill archive (accepts standard optional frontmatter like `version`, `author`, `compatibility`) | -| **Memory V2** (`/api/memory/v2`) | `GET /` - builtin data (USER.md/MEMORY.md); `POST/PUT/DELETE /entries` - add/replace/remove entry; `GET/PATCH/DELETE /config` - effective config + per-user overrides; `POST /search` - Hindsight recall/reflect proxy | -| **Agents** (`/api/agents`) | `GET /` - list built-in, owned, and published agents; `POST /` - create user-owned agent; `GET/PUT/DELETE /{id}` - read/update/delete by stable agent id; `GET /check` - validate non-empty display name (duplicates allowed) | -| **Skills** (`/api/skills`) | `GET /` - list skills (optional `search` fuzzy-name + `tag_id` filters; each item carries `tags`); `GET /{name}` - details; `PUT /{name}` - update enabled; `POST /install` - install from .skill archive (accepts standard optional frontmatter like `version`, `author`, `compatibility`) | -| **Skills** (`/api/skills`) | `GET /` - list skills (optional `search` fuzzy-name + `tag_id` filters; each item carries `tags` + owner-authored `detail`); `GET /{name}` - details; `PUT /{name}` - update enabled; `PUT /{name}/detail` - set the long-form `detail` text (custom skill: owner or admin; built-in/public: admin only ? materializes a `skills` row on demand); `POST /install` - install from .skill archive (accepts standard optional frontmatter like `version`, `author`, `compatibility`) | -| **Skills** (`/api/skills`) | `GET /` - list skills (optional `search` fuzzy-name + `tag_id` filters; each item carries `tags` + owner-authored `detail`); `GET /{name}` - details; `PUT /{name}` - update enabled; `PUT /{name}/detail` - set the long-form `detail` text (custom skill: owner or admin; built-in/public: admin only ? materializes a `skills` row on demand); `GET /{name}/content` - raw SKILL.md source, restricted to an admin or the publisher (custom-skill owner); other users get 403 and see only description + detail; `POST /install` - install from .skill archive (accepts standard optional frontmatter like `version`, `author`, `compatibility`) | -| **Skills** (`/api/skills`) | `GET /` - list skills (optional `search` fuzzy-name + repeatable `tag_ids` multi-select filter, OR-matched; each item carries `tags` + owner-authored `detail`); `GET /{name}` - details; `PUT /{name}` - update enabled; `PUT /{name}/detail` - set the long-form `detail` text (custom skill: owner or admin; built-in/public: admin only ? materializes a `skills` row on demand); `GET /{name}/content` - raw SKILL.md source, restricted to an admin or the publisher (custom-skill owner); other users get 403 and see only description + detail; `POST /install` - install from .skill archive (accepts standard optional frontmatter like `version`, `author`, `compatibility`) | -| **Memory** (`/api/memory`) | `GET /` - memory data; `POST /reload` - force reload; `GET /config` - config; `GET /status` - config + data | -| **Agents** (`/api/agents`) | `GET /` - list built-in, owned, and published agents (optional `search` + repeatable `tag_ids` multi-select filter, OR-matched; each item carries `tags` and `featured_order`); `POST /` - create user-owned agent; `GET/PUT/DELETE /{id}` - read/update/delete by stable agent id; `GET /check` - validate non-empty display name (duplicates allowed); `GET /selector` - homepage agent picker (single admin-curated list, visibility-filtered, sorted by `featured_order` asc); `PUT /{id}/featured` - **admin** sets/clears the homepage `featured_order` (must be `published=true` to feature; passing `order: null` clears unconditionally); `PUT /featured/order` - **admin** bulk-reorders the homepage list (body `{agent_ids: [...]}` ? indices become 0..N-1) | -| **Tags** (`/api/tags`) | `GET /` - list tags (optional `tag_type` filter; open to all); `POST /` - create tag (admin); `PUT /{id}` / `DELETE /{id}` - update/delete tag (admin); `GET /assignments/{target_type}/{target_id}` - tags on a resource; `PUT /assignments/{target_type}/{target_id}` - replace a resource's tags (admin). Tags are typed `agent`/`skill`/`scheduled_task` and may only tag resources of their own type. | -| **Uploads** (`/api/threads/{id}/uploads`) | `POST /` - upload files (auto-converts PDF/PPT/Excel/Word); `GET /list` - list; `DELETE /{filename}` - delete | -| **Threads** (`/api/threads/{id}`) | `DELETE /` - remove DeerFlow-managed local thread data after LangGraph thread deletion; unexpected failures are logged server-side and return a generic 500 detail | -| **Threads search** (`POST /threads/search`, LangGraph-compat) | Conversation listing. **System threads are excluded by default**: rows whose `metadata.system` is truthy (scheduler runs, roundtable workers) never crowd the user's list; a caller whose `metadata` filter explicitly references `thread_type`/`system` (e.g. studio notebooks) opts out of the exclusion. New `query` field = case-insensitive title (`display_name`) substring search, used by the chats page's server-side pagination. `POST /threads/count` takes the same request body (limit/offset ignored) and returns `{total}` under identical filtering/exclusion semantics ? powers the chats page's ??/??? display without fetching rows (`ThreadMetaStore.count`). `ThreadMetaStore.search` gained `exclude_system`/`query` params; JSON-blob filters batch-scan until the requested `limit`/`offset` window is full (so pagination over a heavily-filtered set never under-fills), with a `thread_id` tie-breaker on the `updated_at DESC` ordering for stable OFFSET paging. Tests: `tests/test_thread_meta_search.py` | -| **Artifacts** (`/api/threads/{id}/artifacts`) | `GET /{path}` - serve artifacts; active content types (`text/html`, `application/xhtml+xml`, `image/svg+xml`) are always forced as download attachments to reduce XSS risk; `?download=true` still forces download for other file types | -| **Suggestions** (`/api/threads/{id}/suggestions`) | `POST /` - generate follow-up questions; rich list/block model content is normalized before JSON parsing | -| **Thread Runs** (`/api/threads/{id}/runs`) | `POST /` - create background run; `POST /stream` - create + SSE stream; `POST /wait` - create + block; `GET /` - list runs; `GET /{rid}` - run details; `POST /{rid}/cancel` - cancel; `GET /{rid}/join` - join SSE; `GET /{rid}/messages` - paginated messages `{data, has_more}`; `GET /{rid}/events` - full event stream; `GET /../messages` - thread messages with feedback; `GET /../token-usage` - aggregate tokens | -| **Feedback** (`/api/threads/{id}/runs/{rid}/feedback`) | `PUT /` - upsert feedback; `DELETE /` - delete user feedback; `POST /` - create feedback; `GET /` - list feedback; `GET /stats` - aggregate stats; `DELETE /{fid}` - delete specific | -| **Runs** (`/api/runs`) | `POST /stream` - stateless run + SSE; `POST /wait` - stateless run + block; `GET /{rid}/messages` - paginated messages by run_id `{data, has_more}` (cursor: `after_seq`/`before_seq`); `GET /{rid}/feedback` - list feedback by run_id | -| **Thread Shares** (`/api/thread-shares`) | Per-conversation sharing with two modes (`thread_shares.mode`). **`import`**: `POST /` `{thread_id, mode}` - create (or reuse) a revocable share code; `GET /{code}/preview` + `POST /{code}/import` - copy that conversation into the caller's account (fresh `thread_id` via `app/gateway/thread_copy.py` + new `threads_meta` row + `user-data` copy; dedup on `metadata.shared_origin`). **`view`**: a public, read-only static page ? see Public Shares below. Both modes: `GET /` - list my codes; `DELETE /{code}` - revoke. | -| **Public Shares** (`/api/public/shares`) | **Unauthenticated** (the `/api/public/` prefix is whitelisted in `auth_middleware._PUBLIC_PATH_PREFIXES`). `GET /{code}` - read-only conversation snapshot (title + serialized messages + artifacts) for a `view`-mode share; messages are read straight from the latest checkpoint. `GET /{code}/files/{path}` - serves a file referenced by that conversation (images, Markdown, text, PDF, ?) so the shared page's links are openable; path resolution is confined to the owner's thread directory. Active content (`text/html`, `application/xhtml+xml`, `image/svg+xml`) is forced to download (`_public_share_disposition`); everything else is served inline. Missing/revoked/non-`view` codes all return 404. | -| **Roundtable Drafts** (`/api/roundtable-drafts`) | Per-user persistence for the roundtable-planning page (replaces the old browser `localStorage`). `GET /` - list drafts (lightweight meta); `POST /` - create; `GET/PUT/DELETE /{id}` - full read / partial update / delete. A draft = one full session (Step 1 intent + Step 2 roundtable), step snapshots stored as JSON in `PortableLongText`. Recommendation history bound to a draft: `GET /{id}/recommendations` - list newest-first; `POST /{id}/recommendations` - append one run (objective/status/rationale/picks/candidates). Backed by `deerflow.persistence.roundtable_drafts` (tables `roundtable_drafts`, `roundtable_recommend_history`); store wired in `deps.py` as `app.state.roundtable_draft_store`. | -| **Roundtable Draft Shares** (`/api/roundtable-draft-shares`) | Per-draft sharing for the roundtable-planning page, mirroring Thread Shares but the shared resource is a `roundtable_drafts` row. Two modes (`roundtable_draft_shares.mode`). **`import`**: `POST /` `{draft_id, mode}` - create (or reuse) a revocable code; `GET /{code}/preview` + `POST /{code}/import` - copy that draft (step1/2/3) into the caller's account under a deterministic per-(code,recipient) id (`imp_` ? re-import is a no-op). **`view`**: a public, read-only result page ? see Public Roundtable Shares. Both: `GET /` - list my codes; `DELETE /{code}` - revoke. Backed by `deerflow.persistence.roundtable_draft_shares` (table `roundtable_draft_shares`, migration `20260606_01`); store wired in `deps.py` as `app.state.roundtable_draft_share_store`. Tests: `tests/test_roundtable_draft_shares.py`. | -| **Public Roundtable Shares** (`/api/public/roundtable-shares`) | **Unauthenticated** (`/api/public/` whitelisted). `GET /{code}` - read-only snapshot for a `view`-mode draft share: returns the Step 3??????report HTML (`step3.html`, a self-contained document) + title/summary/`has_report`. The frontend renders it in a sandboxed `iframe` (no `allow-scripts`) at `#/roundtable-share/`. Missing/revoked/non-`view` codes all return 404. | -| **Scheduled Tasks** (`/api/scheduled-tasks`) | Per-user cron-like agent tasks. `GET/POST /` - list/create owned tasks; `PATCH/DELETE /{id}` - update/delete (owner); `POST /{id}/{pause,resume,trigger}`; `GET /{id}/runs`, `GET /runs/{rid}`, run-file preview/edit/download; `GET /runs/{rid}/messages` - agent messages for the run's underlying agent run (used to inspect a stuck/running run; authorized via `_load_run`); `GET /runs/{rid}/html` - generated HTML document for an `html_page` run; `DELETE /runs/{rid}` - owner-scoped, with an admin fallback so admins can delete any user's run; publish/subscribe under `/{id}/{publish,unpublish,subscribe}` and `GET /published`. **Admin (cross-user):** `GET /admin/all` - every task; `PATCH/DELETE /admin/{id}` - edit/delete any task; `POST /admin/{id}/trigger`; `GET /admin/{id}/runs`. Admin routes require `system_role == "admin"`. | -| **HTML Page Favorites** (`/api/html-page-favorites`) | Bookmarked generated HTML pages, owner-scoped. `GET /` - list; `POST /` `{page_id, title?, description?, tags?}` - bookmark a `scheduled_html_pages` row (extracts a reusable style/layout summary via GLM5 at favorite time); a page can only be favorited once per user (`DuplicateFavoriteError` ? HTTP 409). `GET/DELETE /{id}` - read/delete (the create-task reference picker deletes favorites here). A favorite can be selected as a style reference when creating a new `html_page` task. | -| **Public Scheduled Tasks** (`/api/public/scheduled-tasks`) | **Unauthenticated** mirror for embedding. Adds `GET /runs/{rid}/html` and `GET /{task_id}/latest-html` for the `html_page` preview; the run result keeps `result.html` (only file paths are redacted). | -| **Light Apps** (`/api/light-apps`) | Light-app registry (????? / ????), **global/shared** (no user scoping; `created_by`/`updated_by` audit-only). Two kinds: `window_open` (launched in a new tab from the ???? card grid) and `iframe` (embedded under a parent sidebar menu via `mount_location` and dynamically injected into the sidebar). Both carry `route_params` ? a list of `{key, paramType: login\|theme, source, customValue, themeValue}` descriptors the **frontend** resolves at launch time (login params read from `localStorage.userInfo`; theme params use a literal value) and appends to `url` as query string. `GET /` - list (filters `app_type`/`mount_location`/`enabled_only`/`search`); `GET /{app_id}` - read one (both open to any authenticated user); `POST /`, `PUT /{app_id}`, `DELETE /{app_id}` - **admin only**; iframe apps must carry a `mount_location` (400 otherwise); the unique `name` collides as HTTP 409. Backed by `deerflow.persistence.light_apps` (table `light_apps`, auto-created by `create_all`); store wired in `deps.py` as `app.state.light_app_store`. Tests: `tests/test_light_apps.py`. | -| **Knowledge** (`/api/knowledge`) | Product knowledge base, **global/shared** (no user isolation; writes record `created_by`/`updated_by` for audit). `POST /ingest/thread/{thread_id}` - sediment one thread into a note (reads messages from the checkpointer, `threads:read` owner-checked); `POST /ingest/threads/batch` - batch-sediment the caller's recent threads (`skip_existing` via `.manifest.json` dedup). **Optional model selection**: both ingest endpoints accept `model_name` (overrides `knowledge.extract_model_name` for that capture; threaded through `KnowledgeService.capture_thread` ? `extract_thread_llm_multi`). **Model-error surfacing**: when the chosen model produces nothing usable (timeout / API error / non-JSON) capture still **falls back to rule-based extraction and lands a draft** (`llm_fallback:true`); the single-thread (non-background) response and the batch response (`llm_fallback` count) carry this so the UI tells the user "???????". The "??????" chat button stays background/fire-and-forget (passes `model_name`, no per-save error toast); the batch-import dialog is synchronous and shows the fallback count. `POST /ingest/file` - **import an uploaded file** (multipart: `file` + optional `title`/`tags`/`status`/`model_name`) into wiki note(s): the file is converted to Markdown (md/txt read directly, pdf/word/ppt/excel via `convert_file_to_markdown`), distilled by the same wiki-capture LLM pipeline as thread capture (`source_type="file"`), and falls back to a verbatim **draft** note when the model returns nothing usable (`llm_fallback:true`); `POST /ingest/search` - sediment search/tool results as a reference note; `POST /notes` - manual note; `GET /notes` (keyword/tag/source_type/status + pagination); `GET/PUT/DELETE /notes/{id}` (DELETE is soft = archive, `?hard=true` removes the vault file); `POST /search` - `mode=keyword\|vector\|hybrid` (vector?keyword fallback when no embedding endpoint); `GET /notes/{id}/versions` - edit history; `GET /duplicates` + `POST /merge` - dedup; `POST /reindex` - rebuild vector index; `GET /graph` - product graph data (notes + entities, graph-blacklist filtered); `GET/POST /graph/blacklist` + `DELETE /graph/blacklist/{id}` - graph node blacklist (labels = entity names / note titles, case-insensitive match; blacklisted nodes and their edges are dropped from `/graph` and the wiki-export artifacts; table `knowledge_graph_blacklist`, auto-created); `POST /export/graph` + `GET /export/files/{graph.json\|graph.html\|graph.graphml\|cypher.txt}` - obsidian-wiki `wiki-export` artifacts (self-contained `graph.html` for iframe); `POST /resolve` `{targets:[?]}` + `GET /notes/{id}/wikilinks` - resolve `[[wikilink]]` targets to a note id / vault page / missing (powers clickable wikilinks); `GET /vault-page?path=` - read a raw vault meta/hub Markdown page (no DB note). Backed by the `deerflow.knowledge` module (see below). | - -Proxied through nginx: `/api/langgraph/*` ? LangGraph, all other `/api/*` ? Gateway. - -### AI Writing Built-in Functional Agents (????????) - -The AI-writing graph (`packages/harness/deerflow/agents/ai_writing/`) has four stage "brains" ? ?????? / ????? / ?? / ?? ? that can each run in one of two modes, switched per-stage by `config.yaml ? ai_writing.use_builtin_agents.{researcher,outliner,writer,editor}` (default **false**, mtime hot-reloaded, new sessions pick up changes immediately): - -- **Legacy (flag off)**: hard-coded prompts in `prompts/*.py`, direct LLM call (unchanged behavior). -- **Builtin-agent (flag on)**: the node loads the corresponding functional agent from `.deer-flow/agents/ai-writing-/` ? `SOUL.md` becomes the system prompt and `config.yaml`'s `model` overrides the stage model (priority: `agent.model > node explicit fallback (keyword_model/rank_model) > state.model_name`). Admins edit SOUL/model from the agent management page. Runner: `nodes/_agent_runner.py` (`run_writing_agent` / `stream_writing_agent` / `parse_marker_json`). Output contracts (marker + ```json``` blocks, intent.py-style): researcher `[KEYWORDS_READY]` / `[MATERIALS_RANKED]`, outliner `[OUTLINE_READY]`, editor (per-dimension) `[REVIEW_READY]`; writer emits raw section Markdown (keeps per-section streaming) with the legacy `[[SECTION_BLOCKED]]` sentinel in strict mode. **Orchestration stays in the nodes** (search fan-out, per-section loop, 3-dimension review loop, all pause points) ? the agent only does single structured generations, and a missing/empty SOUL silently falls back to the legacy path (`WritingAgentUnavailable`). - -Seeding mirrors the roundtable pattern: verified `config.yaml` + `SOUL.md` ship in `app/gateway/routers/_ai_writing_seed_assets//`; `_ai_writing_seed.py::ensure_ai_writing_functional_agents()` copies them to `.deer-flow/agents/` when missing (never overwrites local edits), called from the startup lifespan (before `_sync_legacy_agents`, so the agents appear in `/api/agents` on first boot) and self-heals from `GET /api/ai-writing/sessions` + `GET /api/ai-writing/article-types`. Tests: `tests/test_ai_writing_seed.py`, `tests/test_ai_writing_agent_runner.py`. - -**Skill-driven retrieval (检索类型).** `material_source` has exactly two values: `general` (通用检索, default) and `notebook` (我的空间); legacy `knowledge_base`/`skill` are normalized to `general` by the researcher node. General retrieval is orchestrated from the **skills configured on the `ai-writing-researcher` agent** (`config.yaml → skills`, editable from the agent management page): the built-in `knowledge-base-search` skill (知识库检索) runs the native keyword fan-out web/intranet search (the old default path), while any other configured skill searches with a `description`-targeted query; skills run in configured order with early-stop at `max_materials`, and an empty/unresolvable skills config falls back to the built-in knowledge search so retrieval always works. The skill itself is seeded from `_ai_writing_seed_assets/skills/knowledge-base-search/SKILL.md` into `skills/public/` (never overwrites), and a pre-existing researcher `config.yaml` lacking the `skills` key is patched in place with the default (explicit `skills: []` is respected). **PAUSE-1 input append flow**: when the user types natural language into the material-confirm input (`re_search`/legacy `enable_skill_search` + `user_query`), the node matches the most relevant configured skill via `SkillPicker(candidates=…)` (skipped when only one skill is configured), searches with the user's words, and **appends** the new materials to the existing package (URL-dedup, original order preserved, no LLM re-rank/truncation, 100-item hard cap). Tests: `tests/test_ai_writing_material_source.py`. - -### Admin Leaderboard Snapshots (`/api/admin/users/leaderboard*`) - -The admin ????? is served from **precomputed per-day snapshots** in `admin_leaderboard_daily_stats` (keyed by `(stat_date, settings_hash)`), not by aggregating raw rows on every request. A background scheduler inside the Gateway (`_leaderboard_snapshot_scheduler_loop` in `app/gateway/app.py`, singleton file-locked) ticks every `ADMIN_LEADERBOARD_BACKGROUND_INTERVAL_SECONDS` (default 300s): it prewarms recent days, then builds any `queued`/`error`/stale-`running` day. `GET /leaderboard` merges the per-day snapshots for the requested range. **Today** is rebuilt whenever its snapshot is older than `ADMIN_LEADERBOARD_TODAY_TTL_SECONDS` (default 43200s = half a day, so the leaderboard refreshes ~twice daily); a past day, once `ready`, is treated as fresh forever. - -Two on-demand/automatic refresh paths layer on top: -- **Manual "????"** ? `POST /leaderboard/refresh-today` force-rebuilds *today's* snapshot **synchronously** (bounded by the same semaphore/timeout the scheduler uses), then returns the merged range. Lets an admin pull the latest real-time records into the stats without waiting for the TTL. Frontend: a "????" button on `AdminUserLeaderboardPage` ? `useRefreshTodayLeaderboard` (`core/admin/hooks.ts`), which primes + invalidates the `["admin-activity","leaderboard"]` query. -- **Daily finalize of the previous day** ? once per day, on/after `ADMIN_LEADERBOARD_FINALIZE_HOUR` (local Beijing, default `1` = ?? 1 ?), the scheduler tick force-recomputes *yesterday* a single time so runs/tool-calls that only landed near midnight are fully captured. The decision is the pure `deerflow.persistence.admin_stats.finalize.should_finalize_previous_day` (status/generated_at/now/hour) ? the rebuild stamps `generated_at` to today, which self-debounces it for the rest of the day (no extra state file). Tests: `tests/test_leaderboard_finalize.py`. - -### Scheduled Task Runtime - -`ScheduledTaskService` (`deerflow/runtime/scheduler/service.py`) polls due tasks every `poll_interval_seconds` and executes each in its own throwaway thread, serialized by a shared `_agent_run_lock`. - -**Stuck-run recovery**: each execution attempt is bounded by `_RUN_ATTEMPT_TIMEOUT_SECONDS` (30 min) via `asyncio.wait_for`. On timeout the run is retried up to `_MAX_RUN_RETRIES` (3) times, then marked `failed`. A watchdog (`_reconcile_stuck_runs`, run every poll) recovers runs left in `running` with no in-process owner ? e.g. orphaned by a crash or restart ? re-driving them through the same retry budget so a task can never stay perpetually "executing". The `scheduled_task_runs.retry_count` column tracks consumed retries. - -**Unique task names**: task names are unique per user (`uq_scheduled_tasks_user_name`). `create_task`/`update_task`/`admin_update_task` proactively check via `_assert_name_available()` and raise `DuplicateTaskNameError` on a collision; the router translates that into HTTP `409` instead of letting the raw DB `IntegrityError` surface as a `500`. - -**HTML page tasks (`task_kind == "html_page"`)**: a second task kind that renders a full HTML document each run instead of a Markdown report. The kind + its config (`html_config`: `page_prompt`, `negative_prompt`, `reference_favorite_id`, `model_name`) live inside `execution_context` ? no schema change to `scheduled_tasks`. Data gathering is delegated to the agent's own search skills (driven by `page_prompt`), so there is no explicit search-query/data-source field; the render model is user-selectable (`model_name`, default `zai-org/GLM-5-FP8`). `_run_task_attempt()` dispatches by kind; `_run_html_page_attempt()` runs the pipeline: gather real data via the lead agent (its search tools) ? optionally load a reference favorite's reusable style/layout ? render with the chosen model (thinking off) ? fault-tolerant JSON parse ? sanitize (BeautifulSoup, strips `
/report`): the Step 3 经典 HTML 看板 (a * self-contained HTML document) rendered full-screen inside a sandboxed - * `iframe` (no `allow-scripts`), so the agent-generated HTML can never execute - * code in the app origin. + * `iframe` (no `allow-scripts`). + * - dashboard (`#/roundtable-share//dashboard`): the Step 3 大屏多页 (模式二) + * — rendered on the fly from the stored structured data via `renderDashboardHtml` + * into the same sandboxed `iframe`. */ export default function PublicRoundtableSharePage({ - report = false, + view = "landing", }: { - report?: boolean; + view?: RoundtableShareView; }) { const { shareCode } = useParams<{ shareCode: string }>(); // 嵌套方可用 ?bg=/?text= 或 postMessage 覆盖背景/文字色。 @@ -108,6 +118,17 @@ export default function PublicRoundtableSharePage({ refetchOnWindowFocus: false, }); + // 大屏多页:用与应用内一致的 React 大屏(DashboardApp)渲染,而非自包含 HTML 模板。 + // 把存储的结构化数据(report-json)解析并 adapt 成 DashboardApp 的入参;与 + // Step3DashboardMode 的「应用渲染」、DashboardStandalonePage 的「新窗口打开」同源。 + const dashboardData = useMemo(() => { + const json = data?.dashboard_json; + if (!json) return undefined; + const parsed = parseReportJson(json); + if (!parsed) return undefined; + return adaptReportData(parsed, data?.generated_at ?? ""); + }, [data?.dashboard_json, data?.generated_at]); + if (isLoading) { return ( @@ -128,8 +149,24 @@ export default function PublicRoundtableSharePage({ ); } - // ── 报告页:整页沙箱 iframe 展示第 3 步结果绘制 ────────────────────────────── - if (report) { + // ── 大屏多页:整屏渲染应用内 React 大屏(DashboardApp),与应用渲染/新窗口打开一致 ── + if (view === "dashboard") { + return ( + + {dashboardData ? ( + + ) : ( + + 暂无大屏多页 + 这次会商还没有生成第三步「大屏多页」的方案报告。 + + )} + + ); + } + + // ── 结果绘制(经典 HTML 看板):整页沙箱 iframe 展示自包含报告 ───────────────── + if (view === "report") { return ( @@ -161,8 +198,10 @@ export default function PublicRoundtableSharePage({ ); } - // ── 落地页:总控最终结论 + 各席位最终交付 + 查看结果绘制(新标签打开报告页)───── - const reportUrl = `${window.location.origin}${window.location.pathname}#/roundtable-share/${shareCode}/report`; + // ── 落地页:结果绘制/大屏多页入口 + 方案总结报告 + 总控最终结论 + 各席位最终交付 ── + const shareBase = `${window.location.origin}${window.location.pathname}#/roundtable-share/${shareCode}`; + const reportUrl = `${shareBase}/report`; + const dashboardUrl = `${shareBase}/dashboard`; return ( @@ -182,23 +221,39 @@ export default function PublicRoundtableSharePage({ - {/* 1. 结果绘制 —— 新标签打开报告页 */} + {/* 1. 结果绘制 / 大屏多页 —— 新标签打开整页报告 */} 结果绘制 - {data.has_report ? ( - - - 查看结果绘制 - - + {data.has_report || data.has_dashboard ? ( + + {data.has_report && ( + + + 查看结果绘制 + + + )} + {data.has_dashboard && ( + + + 查看大屏多页 + + + )} + ) : ( 这次会商还没有生成第三步「结果绘制」的方案报告。 @@ -206,7 +261,22 @@ export default function PublicRoundtableSharePage({ )} - {/* 2. 总控最终结论 */} + {/* 2. 方案总结报告(Markdown) */} + {data.has_summary_report && ( + + + + 方案总结报告 + + + + {data.summary_report_md} + + + + )} + + {/* 3. 总控最终结论 */} @@ -223,7 +293,7 @@ export default function PublicRoundtableSharePage({ )} - {/* 3. 各席位最终交付 */} + {/* 4. 各席位最终交付 */} diff --git a/frontend-web/src/pages/light-app-management.css b/frontend-web/src/pages/light-app-management.css new file mode 100644 index 00000000..bc3eafc0 --- /dev/null +++ b/frontend-web/src/pages/light-app-management.css @@ -0,0 +1,570 @@ +@import "../../docs/ui规范-深蓝/tokens.css"; +@import "./light-app-pages.css"; + +/** + * 应用管理页 —— 深蓝色主题适配(《深蓝色 UI 规范》) + * 浅色样式仍由组件内 Tailwind 承担;.dark 下本文件覆盖为 ZS-004 / ZS-002 令牌。 + */ +.dark .light-app-management-page { + color: var(--color-text); +} + +/* ── 页头 / 文案 ─────────────────────────────────────────────────────── */ +.dark .light-app-management-page .lam-title { + color: var(--color-text-strong); +} + +.dark .light-app-management-page .lam-desc { + color: var(--color-text-secondary); +} + +.dark .light-app-management-page .lam-desc code { + background: rgba(22, 119, 255, 0.12); + color: var(--color-link); +} + +.dark .light-app-management-page .lam-primary-btn { + background: var(--color-primary); + box-shadow: none; +} + +.dark .light-app-management-page .lam-primary-btn:hover { + background: var(--color-primary-hover); +} + +.dark .light-app-management-page .lam-primary-btn:active { + background: var(--color-primary-active); +} + +/* ── 工具条 / 搜索 / 视图切换 ─────────────────────────────────────────── */ +.dark .light-app-management-page .lam-toolbar { + background: var(--color-bg-card); + border: 1px solid var(--color-border); + box-shadow: var(--shadow-1); +} + +.dark .light-app-management-page .lam-search-input { + background: var(--color-bg-page); + border-color: var(--color-border); + color: var(--color-text); +} + +.dark .light-app-management-page .lam-search-input::placeholder { + color: var(--color-text-disabled); +} + +.dark .light-app-management-page .lam-search-input:hover { + background: var(--color-bg-card); + border-color: var(--color-link); +} + +.dark .light-app-management-page .lam-search-input:focus { + background: var(--color-bg-card); + border-color: var(--color-primary); + box-shadow: 0 0 0 2px rgba(22, 119, 255, 0.16); + outline: none; +} + +.dark .light-app-management-page .lam-view-toggle { + background: var(--color-bg-page); + border-color: var(--color-border); +} + +.dark .light-app-management-page .lam-view-toggle-btn.is-active { + background: var(--color-bg-card); + color: var(--color-link); + box-shadow: var(--shadow-1); +} + +.dark .light-app-management-page .lam-view-toggle-btn:not(.is-active) { + color: var(--color-text-secondary); +} + +.dark .light-app-management-page .lam-view-toggle-btn:not(.is-active):hover { + color: var(--color-text); +} + +.dark .light-app-management-page .lam-count { + color: var(--color-text-secondary); +} + +.dark .light-app-management-page .lam-count strong { + color: var(--color-link); +} + +/* ── 表格(§28)──────────────────────────────────────────────────────── */ +.dark .light-app-management-page .lam-table-wrap { + background: var(--color-bg-page); + border: 1px solid var(--color-border); + box-shadow: var(--shadow-1); +} + +.dark .light-app-management-page .lam-table thead tr { + background: var(--color-bg-card); + border-color: var(--color-border); + color: var(--color-text); +} + +.dark .light-app-management-page .lam-table tbody { + border-color: var(--color-border); +} + +.dark .light-app-management-page .lam-table tbody tr { + border-color: var(--color-border); +} + +.dark .light-app-management-page .lam-table tbody tr:hover { + background: rgba(22, 119, 255, 0.08); +} + +.dark .light-app-management-page .lam-row-name { + color: var(--color-text); +} + +.dark .light-app-management-page .lam-table tbody tr:hover .lam-row-name { + color: var(--color-link); +} + +.dark .light-app-management-page .lam-tag-window { + border-color: rgba(245, 34, 45, 0.35); + background: var(--func-error-bg); + color: #ff7875; +} + +.dark .light-app-management-page .lam-tag-iframe { + border-color: rgba(22, 119, 255, 0.35); + background: var(--func-info-bg); + color: var(--color-link); +} + +.dark .light-app-management-page .lam-url { + color: var(--color-text-secondary); +} + +.dark .light-app-management-page .lam-table tbody tr:hover .lam-url { + color: var(--color-text); +} + +.dark .light-app-management-page .lam-url a { + color: inherit; +} + +.dark .light-app-management-page .lam-url a:hover { + color: var(--color-link); +} + +.dark .light-app-management-page .lam-mount-tag { + background: var(--func-info-bg); + color: var(--color-link); +} + +.dark .light-app-management-page .lam-mount-dot { + background: var(--color-primary); +} + +.dark .light-app-management-page .lam-param-btn { + border-color: var(--color-border); + background: var(--color-bg-page); + color: var(--color-text-secondary); +} + +.dark .light-app-management-page .lam-param-btn:hover { + background: rgba(22, 119, 255, 0.08); + color: var(--color-text); +} + +.dark .light-app-management-page .lam-param-popover { + border-color: var(--color-border); + background: var(--color-bg-card); + box-shadow: var(--shadow-3); +} + +.dark .light-app-management-page .lam-param-popover-head { + border-color: var(--color-border); + color: var(--color-text-secondary); +} + +.dark .light-app-management-page .lam-param-popover-close { + color: var(--color-link); +} + +.dark .light-app-management-page .lam-param-item { + border-color: var(--color-border); + background: var(--color-bg-page); +} + +.dark .light-app-management-page .lam-param-key { + color: #ff7875; +} + +.dark .light-app-management-page .lam-param-val { + color: var(--color-text-secondary); +} + +.dark .light-app-management-page .lam-status-on { + color: var(--func-success); +} + +.dark .light-app-management-page .lam-status-on-dot { + background: var(--func-success); +} + +.dark .light-app-management-page .lam-status-on-ping { + background: var(--func-success); +} + +.dark .light-app-management-page .lam-status-off { + color: var(--color-text-disabled); +} + +.dark .light-app-management-page .lam-status-off-dot { + background: var(--color-text-disabled); +} + +.dark .light-app-management-page .lam-icon-btn { + color: var(--color-text-secondary); +} + +.dark .light-app-management-page .lam-icon-btn:hover { + background: rgba(22, 119, 255, 0.08); + color: var(--color-link); +} + +.dark .light-app-management-page .lam-icon-btn-danger:hover { + background: var(--func-error-bg); + color: var(--func-error); +} + +.dark .light-app-management-page .lam-empty-icon { + background: var(--color-bg-page); + border: 1px solid var(--color-border); + color: var(--color-text-disabled); +} + +.dark .light-app-management-page .lam-empty-title { + color: var(--color-text-secondary); +} + +.dark .light-app-management-page .lam-empty-sub { + color: var(--color-text-disabled); +} + +/* ── 卡片视图(§22 / §43)────────────────────────────────────────────── */ +.dark .light-app-management-page .lam-card { + background: var(--color-bg-card); + border: 1px solid var(--color-border); + box-shadow: var(--shadow-1); +} + +.dark .light-app-management-page .lam-card:hover { + box-shadow: var(--shadow-2); +} + +.dark .light-app-management-page .lam-card-title { + color: var(--color-text-strong); +} + +.dark .light-app-management-page .lam-card:hover .lam-card-title { + color: var(--color-link); +} + +.dark .light-app-management-page .lam-card-desc { + color: var(--color-text-secondary); +} + +.dark .light-app-management-page .lam-card-url-box { + background: var(--color-bg-page); +} + +.dark .light-app-management-page .lam-card-param { + border-color: var(--color-border); + background: var(--color-bg-page); + color: var(--color-text-secondary); +} + +.dark .light-app-management-page .lam-card-mount { + background: var(--func-info-bg); + color: var(--color-link); +} + +.dark .light-app-management-page .lam-card-footer { + border-color: var(--color-border); + background: rgba(12, 24, 54, 0.5); +} + +/* ── 底部提示(§33 警告)────────────────────────────────────────────── */ +.dark .light-app-management-page .lam-tip-box { + background: var(--func-warning-bg); + border: 1px solid rgba(250, 173, 20, 0.25); + box-shadow: none; +} + +.dark .light-app-management-page .lam-tip-head { + border-color: rgba(250, 173, 20, 0.2); + color: var(--func-warning); +} + +.dark .light-app-management-page .lam-tip-list { + color: var(--color-text-secondary); +} + +.dark .light-app-management-page .lam-tip-list strong { + color: var(--color-text); +} + +.dark .light-app-management-page .lam-tip-bullet { + color: var(--func-warning); +} + +/* ── 弹窗(§21 Modal)────────────────────────────────────────────────── */ +.dark .light-app-management-page .lam-dialog-overlay { + background: rgba(6, 14, 36, 0.6); +} + +.dark .light-app-management-page .lam-dialog { + border-color: var(--color-border); + background: linear-gradient(180deg, var(--color-bg-card), var(--color-bg-page)); + box-shadow: var(--shadow-3); + color: var(--color-text); +} + +.dark .light-app-management-page .lam-dialog-header { + border-color: var(--color-border); + border-bottom: none; + padding-bottom: 0; +} + +.dark .light-app-management-page .lam-dialog-header::after { + content: ""; + display: block; + height: 1px; + margin-top: 16px; + margin-bottom: 0; + background: var(--gradient-title-line); +} + +.dark .light-app-management-page .lam-dialog-icon { + background: var(--func-info-bg); + color: var(--color-link); +} + +.dark .light-app-management-page .lam-dialog-title { + color: var(--color-text-strong); +} + +.dark .light-app-management-page .lam-dialog-close { + color: var(--color-text-secondary); +} + +.dark .light-app-management-page .lam-dialog-close:hover { + background: rgba(22, 119, 255, 0.08); + color: var(--color-link); +} + +.dark .light-app-management-page .lam-form-label { + color: var(--color-text); +} + +.dark .light-app-management-page .lam-form-label .lam-required { + color: var(--func-error); +} + +.dark .light-app-management-page .lam-form-label svg { + color: var(--color-text-secondary); +} + +.dark .light-app-management-page .lam-form-hint { + color: var(--color-text-secondary); +} + +.dark .light-app-management-page .lam-dialog .lam-input, +.dark .light-app-management-page .lam-dialog input[type="text"] { + background: var(--color-bg-page); + border-color: var(--color-border); + color: var(--color-text); +} + +.dark .light-app-management-page .lam-dialog .lam-input::placeholder, +.dark .light-app-management-page .lam-dialog input[type="text"]::placeholder { + color: var(--color-text-disabled); +} + +.dark .light-app-management-page .lam-dialog .lam-input:focus, +.dark .light-app-management-page .lam-dialog input[type="text"]:focus { + border-color: var(--color-primary); + box-shadow: 0 0 0 2px rgba(22, 119, 255, 0.16); + outline: none; +} + +.dark .light-app-management-page .lam-param-panel { + border-color: var(--color-border); + background: var(--color-bg-page); +} + +.dark .light-app-management-page .lam-param-row { + border-color: var(--color-border); + background: var(--color-bg-card); +} + +.dark .light-app-management-page .lam-param-theme-box { + background: var(--color-bg-page); +} + +.dark .light-app-management-page .lam-param-empty { + border-color: var(--color-border); + background: var(--color-bg-page); + color: var(--color-text-secondary); +} + +.dark .light-app-management-page .lam-add-param-btn { + border-color: var(--color-border-strong); + background: var(--color-bg-card); + color: var(--color-text); +} + +.dark .light-app-management-page .lam-add-param-btn:hover { + border-color: var(--color-primary); + color: var(--color-link); +} + +.dark .light-app-management-page .lam-dialog-footer { + border-color: var(--color-border); +} + +.dark .light-app-management-page .lam-dialog-cancel { + border-color: var(--color-border-strong); + background: transparent; + color: var(--color-text); +} + +.dark .light-app-management-page .lam-dialog-cancel:hover { + border-color: var(--color-link); + color: var(--color-link); +} + +.dark .light-app-management-page .lam-dialog-save { + background: var(--color-primary); + box-shadow: none; +} + +.dark .light-app-management-page .lam-dialog-save:hover { + background: var(--color-primary-hover); +} + +.dark .light-app-management-page .lam-dialog-save:active { + background: var(--color-primary-active); +} + +.dark .light-app-management-page .lam-switch-on { + background: var(--color-primary); +} + +.dark .light-app-management-page .lam-switch-off { + background: var(--color-text-disabled); +} + +/* ── TDesign Select(§15)──────────────────────────────────────────── */ +.dark .light-app-management-page .t-select .t-input, +.dark .light-app-management-page .t-select .t-input__wrap { + background: var(--color-bg-page); + border-color: var(--color-border); + border-radius: var(--radius-input); +} + +.dark .light-app-management-page .t-select .t-input__inner { + color: var(--color-text); +} + +.dark .light-app-management-page .t-select .t-input__inner::placeholder { + color: var(--color-text-disabled); +} + +.dark .light-app-management-page .t-select:hover .t-input, +.dark .light-app-management-page .t-select:hover .t-input__wrap { + border-color: var(--color-link); +} + +.dark .light-app-management-page .t-select.t-is-focused .t-input, +.dark .light-app-management-page .t-select.t-is-focused .t-input__wrap { + border-color: var(--color-primary); + box-shadow: 0 0 0 2px rgba(22, 119, 255, 0.16); +} + +.dark .light-app-management-page .t-select .t-fake-arrow { + color: var(--color-text-secondary); +} + +.dark .light-app-management-page .t-select__dropdown { + background: var(--color-bg-card); + border-color: var(--color-border); + box-shadow: var(--shadow-2); +} + +.dark .light-app-management-page .t-select-option { + color: var(--color-text); +} + +.dark .light-app-management-page .t-select-option:hover { + background: rgba(22, 119, 255, 0.08); +} + +.dark .light-app-management-page .t-select-option.t-is-selected { + background: rgba(22, 119, 255, 0.16); + color: var(--color-link); +} + +/* shadcn Input / Textarea in dialog */ +.dark .light-app-management-page .lam-dialog [data-slot="input"], +.dark .light-app-management-page .lam-dialog textarea { + background: var(--color-bg-page); + border-color: var(--color-border); + color: var(--color-text); +} + +.dark .light-app-management-page .lam-dialog [data-slot="input"]:focus-visible, +.dark .light-app-management-page .lam-dialog textarea:focus-visible { + border-color: var(--color-primary); + box-shadow: 0 0 0 2px rgba(22, 119, 255, 0.16); +} + +.dark .light-app-management-page .lam-param-theme-box span { + color: var(--color-text-secondary); +} + +/* TDesign 下拉挂载到 body 时的深蓝样式 */ +.dark .lam-select-popup.t-popup { + background: var(--color-bg-card); + border-color: var(--color-border); + box-shadow: var(--shadow-2); +} + +.dark .lam-select-popup .t-select-option { + color: var(--color-text); +} + +.dark .lam-select-popup .t-select-option:hover { + background: rgba(22, 119, 255, 0.08); +} + +.dark .lam-select-popup .t-select-option.t-is-selected { + background: rgba(22, 119, 255, 0.16); + color: var(--color-link); +} + +.dark .light-app-management-page .lam-search-input + button, +.dark .light-app-management-page .lam-toolbar .absolute.text-neutral-400 { + color: var(--color-text-secondary); +} + +.dark .light-app-management-page .lam-table .text-neutral-300 { + color: var(--color-text-disabled); +} + +.dark .light-app-management-page .lam-card-url-box button { + color: var(--color-text-secondary); +} + +.dark .light-app-management-page .lam-card-url-box button:hover { + background: rgba(22, 119, 255, 0.08); + color: var(--color-link); +} diff --git a/frontend-web/src/pages/light-app-pages.css b/frontend-web/src/pages/light-app-pages.css new file mode 100644 index 00000000..459be997 --- /dev/null +++ b/frontend-web/src/pages/light-app-pages.css @@ -0,0 +1,12 @@ +@import "../../docs/ui规范-深蓝/tokens.css"; + +/** 轻应用中心列表 / 管理页 —— 深色模式共用页面底图(与 workspace 深蓝主题一致) */ +.dark .light-app-management-page, +.dark .light-app-list-page { + background-color: var(--color-bg-page); + background-image: url("../assets/img/layout-bg-theme-dark-blue.svg"); + background-size: cover; + background-position: center top; + background-repeat: no-repeat; + background-attachment: fixed; +} diff --git a/frontend-web/src/roundtable-planning/components/HighFidelityReport.tsx b/frontend-web/src/roundtable-planning/components/HighFidelityReport.tsx index 533f8ba9..f12a0bb5 100644 --- a/frontend-web/src/roundtable-planning/components/HighFidelityReport.tsx +++ b/frontend-web/src/roundtable-planning/components/HighFidelityReport.tsx @@ -1009,6 +1009,7 @@ export const HighFidelityReport: React.FC = ({ filepath={fileSandboxVirtualUrl} threadId={fileSandboxTarget.threadId} onClose={handleCloseFileSandbox} + allowVirtualMarkdownEdit /> )} diff --git a/frontend-web/src/roundtable-planning/components/Step2SandboxLayout.tsx b/frontend-web/src/roundtable-planning/components/Step2SandboxLayout.tsx index 1d69dea8..59585fa9 100644 --- a/frontend-web/src/roundtable-planning/components/Step2SandboxLayout.tsx +++ b/frontend-web/src/roundtable-planning/components/Step2SandboxLayout.tsx @@ -115,6 +115,7 @@ export function Step2SandboxLayout({ filepath={selectedArtifact} threadId={previewThreadId} onClose={artifactsFill && onArtifactsClose ? handleArtifactsClose : undefined} + allowVirtualMarkdownEdit /> ) : ( diff --git a/frontend-web/src/strategy-components/Header.tsx b/frontend-web/src/strategy-components/Header.tsx index 7e4f144f..b27f18a5 100644 --- a/frontend-web/src/strategy-components/Header.tsx +++ b/frontend-web/src/strategy-components/Header.tsx @@ -139,6 +139,10 @@ const Header = ({ collapsed }: HeaderProps) => { return email ? email.split("@")[0] : "用户"; }, [auth?.email]); const showTopNav = isDark && !collapsed && !isWujieEnv(); + // 浅色(红/蓝)顶栏:一级导航正常横排进顶栏,不做深色那种围绕基座的特殊处理。 + // 与 collapsed 解耦(折叠只影响侧边栏,一级导航始终可见);wujie 嵌入下隐藏, + // 此时一级导航回落到侧边栏(见 page-sidebar.tsx)。 + const showLightTopNav = !isDark && !isWujieEnv(); const hasActiveChild = (item: MenuItem): boolean => { if (item.path && pathname === item.path) { @@ -194,6 +198,20 @@ const Header = ({ collapsed }: HeaderProps) => { ); }; + const renderLightTopItem = (item: MenuItem) => { + const isSelected = item.path ? pathname === item.path : hasActiveChild(item); + return ( + handleNavItemClick(item)} + > + {item.label} + + ); + }; + // 右侧操作区(换肤/通知/用户)—— 深浅两套布局共用 const actionCluster = ( <> @@ -309,6 +327,27 @@ const Header = ({ collapsed }: HeaderProps) => { > + ) : showLightTopNav ? ( + <> + {/* 浅色顶栏:logo + 一级导航横排(白字落在 banner 上) */} + + + + + + + CM助手 + + + + {visibleMenuItems.map(renderLightTopItem)} + + + + + {actionCluster} + + > ) : ( <> @@ -335,6 +374,7 @@ const Header = ({ collapsed }: HeaderProps) => { > )} + {/* 三级菜单横条仅深色用;浅色三级已改到侧边栏右侧列(见 page-sidebar.tsx) */} {showTopNav && thirdSubNavItems.length ? ( diff --git a/frontend-web/src/strategy-components/lib/light-app.ts b/frontend-web/src/strategy-components/lib/light-app.ts index 481b838b..ab796bf6 100644 --- a/frontend-web/src/strategy-components/lib/light-app.ts +++ b/frontend-web/src/strategy-components/lib/light-app.ts @@ -19,11 +19,17 @@ export function mountLabel(value: string | null | undefined): string { return MOUNT_OPTIONS.find((o) => o.value === value)?.label ?? value; } -/** 登录参数可选来源;值均可在 userInfo 中取到,"other" 表示自填。 */ +/** + * 登录参数可选来源;值均可在 userInfo 中取到,"other" 表示自填。 + * accessToken / yUserId 取登录路由透传值(写入 userInfo 的同名键); + * userName / gateDepartName / typeName 取 consumer 接口 userDetailByYuserId 的返回值。 + */ export const LOGIN_SOURCE_OPTIONS: { label: string; value: string }[] = [ { label: "accessToken", value: "accessToken" }, { label: "yUserId", value: "yUserId" }, { label: "userName", value: "userName" }, + { label: "gateDepartName", value: "gateDepartName" }, + { label: "typeName", value: "typeName" }, { label: "其他(自定义值)", value: "other" }, ]; diff --git a/frontend-web/src/styles/globals.css b/frontend-web/src/styles/globals.css index 7ebd17de..3494b65c 100644 --- a/frontend-web/src/styles/globals.css +++ b/frontend-web/src/styles/globals.css @@ -932,7 +932,7 @@ body { .strategy-app-header { background-color: transparent; background-image: url("../assets/img/red-header.png"); - background-size: 100% 100%; + background-size: cover; background-repeat: no-repeat; background-position: center; } diff --git a/offline-backend-20260512/backend/CLAUDE.md b/offline-backend-20260512/backend/CLAUDE.md index 18e7823d..f6812132 100644 --- a/offline-backend-20260512/backend/CLAUDE.md +++ b/offline-backend-20260512/backend/CLAUDE.md @@ -1,759 +1,759 @@ -# CLAUDE.md -?? -This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. - -## Project Overview - -DeerFlow is a LangGraph-based AI super agent system with a full-stack architecture. The backend provides a "super agent" with sandbox execution, persistent memory, subagent delegation, and extensible tool integration - all operating in per-thread isolated environments. - -**Architecture**: -- **Gateway API** (port 8001): REST API plus embedded LangGraph-compatible agent runtime -- **Frontend** (port 3000): Next.js web interface -- **Nginx** (port 2026): Unified reverse proxy entry point -- **Provisioner** (port 8002, optional in Docker dev): Started only when sandbox is configured for provisioner/Kubernetes mode - -**Runtime**: -- `make dev`, Docker dev, and production all run the agent runtime in Gateway via `RunManager` + `run_agent()` + `StreamBridge` (`packages/harness/deerflow/runtime/`). Nginx exposes that runtime at `/api/langgraph/*` and rewrites it to Gateway's native `/api/*` routers. - -**Project Structure**: -``` -deer-flow/ -??? Makefile # Root commands (check, install, dev, stop) -??? config.yaml # Main application configuration -??? extensions_config.json # MCP servers and skills configuration -??? backend/ # Backend application (this directory) -? ??? Makefile # Backend-only commands (dev, gateway, lint) -? ??? langgraph.json # LangGraph Studio graph configuration -? ??? packages/ -? ? ??? harness/ # deerflow-harness package (import: deerflow.*) -? ? ??? pyproject.toml -? ? ??? deerflow/ -? ? ??? agents/ # LangGraph agent system -? ? ? ??? lead_agent/ # Main agent (factory + system prompt) -? ? ? ??? middlewares/ # 10 middleware components -? ? ? ??? memory/ # Memory V2: provider/manager/builtin/hindsight -? ? ? ??? thread_state.py # ThreadState schema -? ? ??? sandbox/ # Sandbox execution system -? ? ? ??? local/ # Local filesystem provider -? ? ? ??? sandbox.py # Abstract Sandbox interface -? ? ? ??? tools.py # bash, ls, read/write/str_replace -? ? ? ??? middleware.py # Sandbox lifecycle management -? ? ??? subagents/ # Subagent delegation system -? ? ? ??? builtins/ # general-purpose, bash agents -? ? ? ??? executor.py # Background execution engine -? ? ? ??? registry.py # Agent registry -? ? ??? tools/builtins/ # Built-in tools (present_files, ask_clarification, view_image) -? ? ??? mcp/ # MCP integration (tools, cache, client) -? ? ??? models/ # Model factory with thinking/vision support -? ? ??? skills/ # Skills discovery, loading, parsing -? ? ??? config/ # Configuration system (app, model, sandbox, tool, etc.) -? ? ??? community/ # Community tools (tavily, jina_ai, firecrawl, image_search, aio_sandbox) -? ? ??? reflection/ # Dynamic module loading (resolve_variable, resolve_class) -? ? ??? utils/ # Utilities (network, readability) -? ? ??? client.py # Embedded Python client (DeerFlowClient) -? ??? app/ # Application layer (import: app.*) -? ? ??? gateway/ # FastAPI Gateway API -? ? ? ??? app.py # FastAPI application -? ? ? ??? routers/ # FastAPI route modules (models, mcp, memory, skills, uploads, threads, artifacts, agents, suggestions, channels) -? ? ??? channels/ # IM platform integrations -? ??? tests/ # Test suite -? ??? docs/ # Documentation -??? frontend/ # Next.js frontend application -??? skills/ # Agent skills directory - ??? public/ # Public skills (committed) - ??? custom/ # Custom skills (gitignored) -``` - -## Important Development Guidelines - -### Documentation Update Policy -**CRITICAL: Always update README.md and CLAUDE.md after every code change** - -When making code changes, you MUST update the relevant documentation: -- Update `README.md` for user-facing changes (features, setup, usage instructions) -- Update `CLAUDE.md` for development changes (architecture, commands, workflows, internal systems) -- Keep documentation synchronized with the codebase at all times -- Ensure accuracy and timeliness of all documentation - -## Commands - -**Root directory** (for full application): -```bash -make check # Check system requirements -make install # Install all dependencies (frontend + backend) -make dev # Start all services (Gateway + Frontend + Nginx), with config.yaml preflight -make start # Start production services locally -make stop # Stop all services -``` - -**Backend directory** (for backend development only): -```bash -make install # Install backend dependencies -make dev # Run Gateway API with reload (port 8001) -make gateway # Run Gateway API only (port 8001) -make test # Run all backend tests -make lint # Lint with ruff -make format # Format code with ruff -``` - -Regression tests related to Docker/provisioner behavior: -- `tests/test_docker_sandbox_mode_detection.py` (mode detection from `config.yaml`) -- `tests/test_provisioner_kubeconfig.py` (kubeconfig file/directory handling) - -Boundary check (harness ? app import firewall): -- `tests/test_harness_boundary.py` ? ensures `packages/harness/deerflow/` never imports from `app.*` - -CI runs these regression tests for every pull request via [.github/workflows/backend-unit-tests.yml](../.github/workflows/backend-unit-tests.yml). - -## Architecture - -### Harness / App Split - -The backend is split into two layers with a strict dependency direction: - -- **Harness** (`packages/harness/deerflow/`): Publishable agent framework package (`deerflow-harness`). Import prefix: `deerflow.*`. Contains agent orchestration, tools, sandbox, models, MCP, skills, config ? everything needed to build and run agents. -- **App** (`app/`): Unpublished application code. Import prefix: `app.*`. Contains the FastAPI Gateway API and IM channel integrations (Feishu, Slack, Telegram, DingTalk). - -**Dependency rule**: App imports deerflow, but deerflow never imports app. This boundary is enforced by `tests/test_harness_boundary.py` which runs in CI. - -**Import conventions**: -```python -# Harness internal -from deerflow.agents import make_lead_agent -from deerflow.models import create_chat_model - -# App internal -from app.gateway.app import app -from app.channels.service import start_channel_service - -# App ? Harness (allowed) -from deerflow.config import get_app_config - -# Harness ? App (FORBIDDEN ? enforced by test_harness_boundary.py) -# from app.gateway.routers.uploads import ... # ? will fail CI -``` - -### Agent System - -**Lead Agent** (`packages/harness/deerflow/agents/lead_agent/agent.py`): -- Entry point: `make_lead_agent(config: RunnableConfig)` registered in `langgraph.json` -- Dynamic model selection via `create_chat_model()` with thinking/vision support -- Tools loaded via `get_available_tools()` - combines sandbox, built-in, MCP, community, and subagent tools -- System prompt generated by `apply_prompt_template()` with skills, memory, and subagent instructions - -**ThreadState** (`packages/harness/deerflow/agents/thread_state.py`): -- Extends `AgentState` with: `sandbox`, `thread_data`, `title`, `artifacts`, `todos`, `uploaded_files`, `viewed_images` -- Uses custom reducers: `merge_artifacts` (deduplicate), `merge_viewed_images` (merge/clear) - -**Runtime Configuration** (via `config.configurable`): -- `thinking_enabled` - Enable model's extended thinking -- `model_name` - Select specific LLM model -- `is_plan_mode` - Enable TodoList middleware -- `subagent_enabled` - Enable task delegation tool - -### Middleware Chain - -Lead-agent middlewares are assembled in strict append order across `packages/harness/deerflow/agents/middlewares/tool_error_handling_middleware.py` (`build_lead_runtime_middlewares`) and `packages/harness/deerflow/agents/lead_agent/agent.py` (`_build_middlewares`): - -1. **ThreadDataMiddleware** - Creates per-thread directories under the user's isolation scope (`backend/.deer-flow/users/{user_id}/threads/{thread_id}/user-data/{workspace,uploads,outputs}`); resolves `user_id` via `get_effective_user_id()` (falls back to `"default"` in no-auth mode); Web UI thread deletion now follows LangGraph thread removal with Gateway cleanup of the local thread directory -2. **UploadsMiddleware** - Tracks and injects newly uploaded files into conversation -3. **SandboxMiddleware** - Acquires sandbox, stores `sandbox_id` in state -4. **DanglingToolCallMiddleware** - Injects placeholder ToolMessages for AIMessage tool_calls that lack responses (e.g., due to user interruption), including raw provider tool-call payloads preserved only in `additional_kwargs["tool_calls"]` -5. **LLMErrorHandlingMiddleware** - Normalizes provider/model invocation failures into recoverable assistant-facing errors before later middleware/tool stages run -6. **GuardrailMiddleware** - Pre-tool-call authorization via pluggable `GuardrailProvider` protocol (optional, if `guardrails.enabled` in config). Evaluates each tool call and returns error ToolMessage on deny. Three provider options: built-in `AllowlistProvider` (zero deps), OAP policy providers (e.g. `aport-agent-guardrails`), or custom providers. See [docs/GUARDRAILS.md](docs/GUARDRAILS.md) for setup, usage, and how to implement a provider. -7. **SandboxAuditMiddleware** - Audits sandboxed shell/file operations for security logging before tool execution continues -8. **ToolErrorHandlingMiddleware** - Converts tool exceptions into error `ToolMessage`s so the run can continue instead of aborting -9. **SummarizationMiddleware** (`DeerFlowSummarizationMiddleware`) - Context reduction when approaching token limits (optional, if enabled). **Non-destructive**: overrides the stock `before_model` (which persists a `RemoveMessage(REMOVE_ALL_MESSAGES)`) to a no-op and instead compresses **transiently** in `wrap_model_call` ? the model request is replaced with `[summary, *recent]` while the full original conversation stays in state (so the frontend keeps rendering the user's real Q&A). The `summary` message is `hide_from_ui` and never persisted; summaries are cached per-thread (bounded, sticky boundary) to avoid an LLM call on every model invocation. Tests: `tests/test_summarization_middleware.py` -10. **TodoListMiddleware** - Task tracking with `write_todos` tool (optional, if plan_mode) -11. **TokenUsageMiddleware** - Records token usage metrics when token tracking is enabled (optional) -12. **TitleMiddleware** - Two-phase auto-title (never shows "untitled"): `before_model` sets an instant question-derived *provisional* title the moment the first user message arrives (no LLM, zero latency), then `after_model` asynchronously generates the final LLM title and replaces it (tracked via the `title_provisional` state flag). Normalizes structured message content before prompting the title model -13. **MemoryMiddleware** - Memory V2: `wrap_model_call` injects Hindsight recall transiently; `after_agent` auto-retains the turn to Hindsight -14. **ViewImageMiddleware** - Injects base64 image data before LLM call (conditional on vision support) -15. **DeferredToolFilterMiddleware** - Hides deferred tool schemas from the bound model until tool search is enabled (optional) -16. **SubagentLimitMiddleware** - Truncates excess `task` tool calls from model response to enforce `MAX_CONCURRENT_SUBAGENTS` limit (optional, if `subagent_enabled`) -17. **LoopDetectionMiddleware** - Detects repeated tool-call loops; hard-stop responses clear both structured `tool_calls` and raw provider tool-call metadata before forcing a final text answer -18. **ClarificationMiddleware** - Intercepts `ask_clarification` tool calls, interrupts via `Command(goto=END)` (must be last) -8a. **ToolMetricsMiddleware** - Appended immediately after `ToolErrorHandlingMiddleware` in `_build_runtime_middlewares`, so it sits *deepest* in the `wrap_tool_call` chain. Records one `tool_call_metrics` row per tool/skill/MCP invocation ? timing duration and bucketing failures into a coarse `error_category` (rate_limited / ip_blocked / auth_error / timeout / network_error / not_found / empty_result / tool_error). It catches both raised exceptions (before #8 converts them) and *self-handled* errors returned as error `ToolMessage`s (search tools that swallow HTTP 429/403). It also captures the targeted Agent Skill into `tool_call_metrics.skill_name`: the lead agent invokes a skill by `read_file`-ing its `/mnt/skills///SKILL.md` path (per the `` prompt block ? there is no dedicated "use skill" tool), so `skill_name` is recovered by scanning every tool call's string arguments for the `skills/{public,custom}/{name}/` path shape (`_SKILL_PATH_RE`), plus the explicit `name` argument of `skill_view`/`skill_manage`/`skill_archive`. `skill_view`-specific success detection handles its swallowed failures (`Skill 'x' not found.` returned as a plain string rather than raised). Regression coverage: `tests/test_tool_metrics_middleware.py`. Writes are best-effort fire-and-forget. **Per-run skill merging**: `SqlToolMetricsStore.record` upserts skill rows by `(run_id, skill_name)` instead of inserting one row per call ? a skill touched by several tool calls / retries inside one run collapses to a single row. Any success in the run wins (a skill that failed then finally succeeded is recorded as one `success`); a skill that only ever failed yields one `error` row whose `error_message` accumulates every attempt's message; durations are summed. Non-skill tool calls, and skill calls with no `run_id`, keep one row each. The read?merge?write is guarded by a per-event-loop `asyncio.Lock`. Surfaced by the admin `GET /api/admin/tool-metrics` router (which has a `kind=skill` filter and a per-skill `/summary` breakdown). -9. **PromptPrefixMiddleware** - Injects the admin-configured prompt prefix into the model request only. The prefix is tagged onto the first human message's `additional_kwargs['prompt_prefix']` by the Gateway (`app.gateway.services`) and prepended to content here at call time, so the persisted/displayed message stays prefix-free -10. **SummarizationMiddleware** (`DeerFlowSummarizationMiddleware`) - Context reduction when approaching token limits (optional, if enabled). **Non-destructive**: overrides the stock `before_model` (which persists a `RemoveMessage(REMOVE_ALL_MESSAGES)`) to a no-op and instead compresses **transiently** in `wrap_model_call` ? the model request is replaced with `[summary, *recent]` while the full original conversation stays in state (so the frontend keeps rendering the user's real Q&A). The `summary` message is `hide_from_ui` and never persisted; summaries are cached per-thread (bounded, sticky boundary) to avoid an LLM call on every model invocation. Tests: `tests/test_summarization_middleware.py` -11. **TodoListMiddleware** - Task tracking with `write_todos` tool (optional, if plan_mode) -12. **TokenUsageMiddleware** - Records token usage metrics when token tracking is enabled (optional) -13. **TitleMiddleware** - Two-phase auto-title (never shows "untitled"): `before_model` sets an instant question-derived *provisional* title the moment the first user message arrives (no LLM, zero latency), then `after_model` asynchronously generates the final LLM title and replaces it (tracked via the `title_provisional` state flag). Normalizes structured message content before prompting the title model -14. **MemoryMiddleware** - Queues conversations for async memory update (filters to user + final AI responses) -15. **ViewImageMiddleware** - Injects base64 image data before LLM call (conditional on vision support) -16. **DeferredToolFilterMiddleware** - Hides deferred tool schemas from the bound model until tool search is enabled (optional) -17. **SubagentLimitMiddleware** - Truncates excess `task` tool calls from model response to enforce `MAX_CONCURRENT_SUBAGENTS` limit (optional, if `subagent_enabled`) -18. **LoopDetectionMiddleware** - Detects repeated tool-call loops; hard-stop responses clear both structured `tool_calls` and raw provider tool-call metadata before forcing a final text answer -19. **ClarificationMiddleware** - Intercepts `ask_clarification` tool calls, interrupts via `Command(goto=END)` (must be last) - -### Configuration System - -**Main Configuration** (`config.yaml`): - -Setup: Copy `config.example.yaml` to `config.yaml` in the **project root** directory. - -**Config Versioning**: `config.example.yaml` has a `config_version` field. On startup, `AppConfig.from_file()` compares user version vs example version and emits a warning if outdated. Missing `config_version` = version 0. Run `make config-upgrade` to auto-merge missing fields. When changing the config schema, bump `config_version` in `config.example.yaml`. - -**Config Caching**: `get_app_config()` caches the parsed config, but automatically reloads it when the resolved config path changes or the file's mtime increases. This keeps Gateway and LangGraph reads aligned with `config.yaml` edits without requiring a manual process restart. - -Configuration priority: -1. Explicit `config_path` argument -2. `DEER_FLOW_CONFIG_PATH` environment variable -3. `config.yaml` in current directory (backend/) -4. `config.yaml` in parent directory (project root - **recommended location**) - -Config values starting with `$` are resolved as environment variables (e.g., `$OPENAI_API_KEY`). -`ModelConfig` also declares `use_responses_api` and `output_version` so OpenAI `/v1/responses` can be enabled explicitly while still using `langchain_openai:ChatOpenAI`. - -**Extensions Configuration** (`extensions_config.json`): - -MCP servers and skills are configured together in `extensions_config.json` in project root: - -Configuration priority: -1. Explicit `config_path` argument -2. `DEER_FLOW_EXTENSIONS_CONFIG_PATH` environment variable -3. `extensions_config.json` in current directory (backend/) -4. `extensions_config.json` in parent directory (project root - **recommended location**) - -### Gateway API (`app/gateway/`) - -FastAPI application on port 8001 with health check at `GET /health`. Set `GATEWAY_ENABLE_DOCS=false` to disable `/docs`, `/redoc`, and `/openapi.json` in production (default: enabled). - -**Routers**: - -| Router | Endpoints | -|--------|-----------| -| **Models** (`/api/models`) | `GET /` - list models; `GET /{name}` - model details | -| **MCP** (`/api/mcp`) | `GET /config` - get config; `PUT /config` - update config (saves to extensions_config.json) | -| **Skills** (`/api/skills`) | `GET /` - list skills; `GET /{name}` - details; `PUT /{name}` - update enabled; `POST /install` - install from .skill archive (accepts standard optional frontmatter like `version`, `author`, `compatibility`) | -| **Memory V2** (`/api/memory/v2`) | `GET /` - builtin data (USER.md/MEMORY.md); `POST/PUT/DELETE /entries` - add/replace/remove entry; `GET/PATCH/DELETE /config` - effective config + per-user overrides; `POST /search` - Hindsight recall/reflect proxy | -| **Agents** (`/api/agents`) | `GET /` - list built-in, owned, and published agents; `POST /` - create user-owned agent; `GET/PUT/DELETE /{id}` - read/update/delete by stable agent id; `GET /check` - validate non-empty display name (duplicates allowed) | -| **Skills** (`/api/skills`) | `GET /` - list skills (optional `search` fuzzy-name + `tag_id` filters; each item carries `tags`); `GET /{name}` - details; `PUT /{name}` - update enabled; `POST /install` - install from .skill archive (accepts standard optional frontmatter like `version`, `author`, `compatibility`) | -| **Skills** (`/api/skills`) | `GET /` - list skills (optional `search` fuzzy-name + `tag_id` filters; each item carries `tags` + owner-authored `detail`); `GET /{name}` - details; `PUT /{name}` - update enabled; `PUT /{name}/detail` - set the long-form `detail` text (custom skill: owner or admin; built-in/public: admin only ? materializes a `skills` row on demand); `POST /install` - install from .skill archive (accepts standard optional frontmatter like `version`, `author`, `compatibility`) | -| **Skills** (`/api/skills`) | `GET /` - list skills (optional `search` fuzzy-name + `tag_id` filters; each item carries `tags` + owner-authored `detail`); `GET /{name}` - details; `PUT /{name}` - update enabled; `PUT /{name}/detail` - set the long-form `detail` text (custom skill: owner or admin; built-in/public: admin only ? materializes a `skills` row on demand); `GET /{name}/content` - raw SKILL.md source, restricted to an admin or the publisher (custom-skill owner); other users get 403 and see only description + detail; `POST /install` - install from .skill archive (accepts standard optional frontmatter like `version`, `author`, `compatibility`) | -| **Skills** (`/api/skills`) | `GET /` - list skills (optional `search` fuzzy-name + repeatable `tag_ids` multi-select filter, OR-matched; each item carries `tags` + owner-authored `detail`); `GET /{name}` - details; `PUT /{name}` - update enabled; `PUT /{name}/detail` - set the long-form `detail` text (custom skill: owner or admin; built-in/public: admin only ? materializes a `skills` row on demand); `GET /{name}/content` - raw SKILL.md source, restricted to an admin or the publisher (custom-skill owner); other users get 403 and see only description + detail; `POST /install` - install from .skill archive (accepts standard optional frontmatter like `version`, `author`, `compatibility`) | -| **Memory** (`/api/memory`) | `GET /` - memory data; `POST /reload` - force reload; `GET /config` - config; `GET /status` - config + data | -| **Agents** (`/api/agents`) | `GET /` - list built-in, owned, and published agents (optional `search` + repeatable `tag_ids` multi-select filter, OR-matched; each item carries `tags` and `featured_order`); `POST /` - create user-owned agent; `GET/PUT/DELETE /{id}` - read/update/delete by stable agent id; `GET /check` - validate non-empty display name (duplicates allowed); `GET /selector` - homepage agent picker (single admin-curated list, visibility-filtered, sorted by `featured_order` asc); `PUT /{id}/featured` - **admin** sets/clears the homepage `featured_order` (must be `published=true` to feature; passing `order: null` clears unconditionally); `PUT /featured/order` - **admin** bulk-reorders the homepage list (body `{agent_ids: [...]}` ? indices become 0..N-1) | -| **Tags** (`/api/tags`) | `GET /` - list tags (optional `tag_type` filter; open to all); `POST /` - create tag (admin); `PUT /{id}` / `DELETE /{id}` - update/delete tag (admin); `GET /assignments/{target_type}/{target_id}` - tags on a resource; `PUT /assignments/{target_type}/{target_id}` - replace a resource's tags (admin). Tags are typed `agent`/`skill`/`scheduled_task` and may only tag resources of their own type. | -| **Uploads** (`/api/threads/{id}/uploads`) | `POST /` - upload files (auto-converts PDF/PPT/Excel/Word); `GET /list` - list; `DELETE /{filename}` - delete | -| **Threads** (`/api/threads/{id}`) | `DELETE /` - remove DeerFlow-managed local thread data after LangGraph thread deletion; unexpected failures are logged server-side and return a generic 500 detail | -| **Threads search** (`POST /threads/search`, LangGraph-compat) | Conversation listing. **System threads are excluded by default**: rows whose `metadata.system` is truthy (scheduler runs, roundtable workers) never crowd the user's list; a caller whose `metadata` filter explicitly references `thread_type`/`system` (e.g. studio notebooks) opts out of the exclusion. New `query` field = case-insensitive title (`display_name`) substring search, used by the chats page's server-side pagination. `POST /threads/count` takes the same request body (limit/offset ignored) and returns `{total}` under identical filtering/exclusion semantics ? powers the chats page's ??/??? display without fetching rows (`ThreadMetaStore.count`). `ThreadMetaStore.search` gained `exclude_system`/`query` params; JSON-blob filters batch-scan until the requested `limit`/`offset` window is full (so pagination over a heavily-filtered set never under-fills), with a `thread_id` tie-breaker on the `updated_at DESC` ordering for stable OFFSET paging. Tests: `tests/test_thread_meta_search.py` | -| **Artifacts** (`/api/threads/{id}/artifacts`) | `GET /{path}` - serve artifacts; active content types (`text/html`, `application/xhtml+xml`, `image/svg+xml`) are always forced as download attachments to reduce XSS risk; `?download=true` still forces download for other file types | -| **Suggestions** (`/api/threads/{id}/suggestions`) | `POST /` - generate follow-up questions; rich list/block model content is normalized before JSON parsing | -| **Thread Runs** (`/api/threads/{id}/runs`) | `POST /` - create background run; `POST /stream` - create + SSE stream; `POST /wait` - create + block; `GET /` - list runs; `GET /{rid}` - run details; `POST /{rid}/cancel` - cancel; `GET /{rid}/join` - join SSE; `GET /{rid}/messages` - paginated messages `{data, has_more}`; `GET /{rid}/events` - full event stream; `GET /../messages` - thread messages with feedback; `GET /../token-usage` - aggregate tokens | -| **Feedback** (`/api/threads/{id}/runs/{rid}/feedback`) | `PUT /` - upsert feedback; `DELETE /` - delete user feedback; `POST /` - create feedback; `GET /` - list feedback; `GET /stats` - aggregate stats; `DELETE /{fid}` - delete specific | -| **Runs** (`/api/runs`) | `POST /stream` - stateless run + SSE; `POST /wait` - stateless run + block; `GET /{rid}/messages` - paginated messages by run_id `{data, has_more}` (cursor: `after_seq`/`before_seq`); `GET /{rid}/feedback` - list feedback by run_id | -| **Thread Shares** (`/api/thread-shares`) | Per-conversation sharing with two modes (`thread_shares.mode`). **`import`**: `POST /` `{thread_id, mode}` - create (or reuse) a revocable share code; `GET /{code}/preview` + `POST /{code}/import` - copy that conversation into the caller's account (fresh `thread_id` via `app/gateway/thread_copy.py` + new `threads_meta` row + `user-data` copy; dedup on `metadata.shared_origin`). **`view`**: a public, read-only static page ? see Public Shares below. Both modes: `GET /` - list my codes; `DELETE /{code}` - revoke. | -| **Public Shares** (`/api/public/shares`) | **Unauthenticated** (the `/api/public/` prefix is whitelisted in `auth_middleware._PUBLIC_PATH_PREFIXES`). `GET /{code}` - read-only conversation snapshot (title + serialized messages + artifacts) for a `view`-mode share; messages are read straight from the latest checkpoint. `GET /{code}/files/{path}` - serves a file referenced by that conversation (images, Markdown, text, PDF, ?) so the shared page's links are openable; path resolution is confined to the owner's thread directory. Active content (`text/html`, `application/xhtml+xml`, `image/svg+xml`) is forced to download (`_public_share_disposition`); everything else is served inline. Missing/revoked/non-`view` codes all return 404. | -| **Roundtable Drafts** (`/api/roundtable-drafts`) | Per-user persistence for the roundtable-planning page (replaces the old browser `localStorage`). `GET /` - list drafts (lightweight meta); `POST /` - create; `GET/PUT/DELETE /{id}` - full read / partial update / delete. A draft = one full session (Step 1 intent + Step 2 roundtable), step snapshots stored as JSON in `PortableLongText`. Recommendation history bound to a draft: `GET /{id}/recommendations` - list newest-first; `POST /{id}/recommendations` - append one run (objective/status/rationale/picks/candidates). Backed by `deerflow.persistence.roundtable_drafts` (tables `roundtable_drafts`, `roundtable_recommend_history`); store wired in `deps.py` as `app.state.roundtable_draft_store`. | -| **Roundtable Draft Shares** (`/api/roundtable-draft-shares`) | Per-draft sharing for the roundtable-planning page, mirroring Thread Shares but the shared resource is a `roundtable_drafts` row. Two modes (`roundtable_draft_shares.mode`). **`import`**: `POST /` `{draft_id, mode}` - create (or reuse) a revocable code; `GET /{code}/preview` + `POST /{code}/import` - copy that draft (step1/2/3) into the caller's account under a deterministic per-(code,recipient) id (`imp_` ? re-import is a no-op). **`view`**: a public, read-only result page ? see Public Roundtable Shares. Both: `GET /` - list my codes; `DELETE /{code}` - revoke. Backed by `deerflow.persistence.roundtable_draft_shares` (table `roundtable_draft_shares`, migration `20260606_01`); store wired in `deps.py` as `app.state.roundtable_draft_share_store`. Tests: `tests/test_roundtable_draft_shares.py`. | -| **Public Roundtable Shares** (`/api/public/roundtable-shares`) | **Unauthenticated** (`/api/public/` whitelisted). `GET /{code}` - read-only snapshot for a `view`-mode draft share: returns the Step 3??????report HTML (`step3.html`, a self-contained document) + title/summary/`has_report`. The frontend renders it in a sandboxed `iframe` (no `allow-scripts`) at `#/roundtable-share/`. Missing/revoked/non-`view` codes all return 404. | -| **Scheduled Tasks** (`/api/scheduled-tasks`) | Per-user cron-like agent tasks. `GET/POST /` - list/create owned tasks; `PATCH/DELETE /{id}` - update/delete (owner); `POST /{id}/{pause,resume,trigger}`; `GET /{id}/runs`, `GET /runs/{rid}`, run-file preview/edit/download; `GET /runs/{rid}/messages` - agent messages for the run's underlying agent run (used to inspect a stuck/running run; authorized via `_load_run`); `GET /runs/{rid}/html` - generated HTML document for an `html_page` run; `DELETE /runs/{rid}` - owner-scoped, with an admin fallback so admins can delete any user's run; publish/subscribe under `/{id}/{publish,unpublish,subscribe}` and `GET /published`. **Admin (cross-user):** `GET /admin/all` - every task; `PATCH/DELETE /admin/{id}` - edit/delete any task; `POST /admin/{id}/trigger`; `GET /admin/{id}/runs`. Admin routes require `system_role == "admin"`. | -| **HTML Page Favorites** (`/api/html-page-favorites`) | Bookmarked generated HTML pages, owner-scoped. `GET /` - list; `POST /` `{page_id, title?, description?, tags?}` - bookmark a `scheduled_html_pages` row (extracts a reusable style/layout summary via GLM5 at favorite time); a page can only be favorited once per user (`DuplicateFavoriteError` ? HTTP 409). `GET/DELETE /{id}` - read/delete (the create-task reference picker deletes favorites here). A favorite can be selected as a style reference when creating a new `html_page` task. | -| **Public Scheduled Tasks** (`/api/public/scheduled-tasks`) | **Unauthenticated** mirror for embedding. Adds `GET /runs/{rid}/html` and `GET /{task_id}/latest-html` for the `html_page` preview; the run result keeps `result.html` (only file paths are redacted). | -| **Light Apps** (`/api/light-apps`) | Light-app registry (????? / ????), **global/shared** (no user scoping; `created_by`/`updated_by` audit-only). Two kinds: `window_open` (launched in a new tab from the ???? card grid) and `iframe` (embedded under a parent sidebar menu via `mount_location` and dynamically injected into the sidebar). Both carry `route_params` ? a list of `{key, paramType: login\|theme, source, customValue, themeValue}` descriptors the **frontend** resolves at launch time (login params read from `localStorage.userInfo`; theme params use a literal value) and appends to `url` as query string. `GET /` - list (filters `app_type`/`mount_location`/`enabled_only`/`search`); `GET /{app_id}` - read one (both open to any authenticated user); `POST /`, `PUT /{app_id}`, `DELETE /{app_id}` - **admin only**; iframe apps must carry a `mount_location` (400 otherwise); the unique `name` collides as HTTP 409. Backed by `deerflow.persistence.light_apps` (table `light_apps`, auto-created by `create_all`); store wired in `deps.py` as `app.state.light_app_store`. Tests: `tests/test_light_apps.py`. | -| **Knowledge** (`/api/knowledge`) | Product knowledge base, **global/shared** (no user isolation; writes record `created_by`/`updated_by` for audit). `POST /ingest/thread/{thread_id}` - sediment one thread into a note (reads messages from the checkpointer, `threads:read` owner-checked); `POST /ingest/threads/batch` - batch-sediment the caller's recent threads (`skip_existing` via `.manifest.json` dedup). **Optional model selection**: both ingest endpoints accept `model_name` (overrides `knowledge.extract_model_name` for that capture; threaded through `KnowledgeService.capture_thread` ? `extract_thread_llm_multi`). **Model-error surfacing**: when the chosen model produces nothing usable (timeout / API error / non-JSON) capture still **falls back to rule-based extraction and lands a draft** (`llm_fallback:true`); the single-thread (non-background) response and the batch response (`llm_fallback` count) carry this so the UI tells the user "???????". The "??????" chat button stays background/fire-and-forget (passes `model_name`, no per-save error toast); the batch-import dialog is synchronous and shows the fallback count. `POST /ingest/file` - **import an uploaded file** (multipart: `file` + optional `title`/`tags`/`status`/`model_name`) into wiki note(s): the file is converted to Markdown (md/txt read directly, pdf/word/ppt/excel via `convert_file_to_markdown`), distilled by the same wiki-capture LLM pipeline as thread capture (`source_type="file"`), and falls back to a verbatim **draft** note when the model returns nothing usable (`llm_fallback:true`); `POST /ingest/search` - sediment search/tool results as a reference note; `POST /notes` - manual note; `GET /notes` (keyword/tag/source_type/status + pagination); `GET/PUT/DELETE /notes/{id}` (DELETE is soft = archive, `?hard=true` removes the vault file); `POST /search` - `mode=keyword\|vector\|hybrid` (vector?keyword fallback when no embedding endpoint); `GET /notes/{id}/versions` - edit history; `GET /duplicates` + `POST /merge` - dedup; `POST /reindex` - rebuild vector index; `GET /graph` - product graph data (notes + entities, graph-blacklist filtered); `GET/POST /graph/blacklist` + `DELETE /graph/blacklist/{id}` - graph node blacklist (labels = entity names / note titles, case-insensitive match; blacklisted nodes and their edges are dropped from `/graph` and the wiki-export artifacts; table `knowledge_graph_blacklist`, auto-created); `POST /export/graph` + `GET /export/files/{graph.json\|graph.html\|graph.graphml\|cypher.txt}` - obsidian-wiki `wiki-export` artifacts (self-contained `graph.html` for iframe); `POST /resolve` `{targets:[?]}` + `GET /notes/{id}/wikilinks` - resolve `[[wikilink]]` targets to a note id / vault page / missing (powers clickable wikilinks); `GET /vault-page?path=` - read a raw vault meta/hub Markdown page (no DB note). Backed by the `deerflow.knowledge` module (see below). | - -Proxied through nginx: `/api/langgraph/*` ? LangGraph, all other `/api/*` ? Gateway. - -### AI Writing Built-in Functional Agents (????????) - -The AI-writing graph (`packages/harness/deerflow/agents/ai_writing/`) has four stage "brains" ? ?????? / ????? / ?? / ?? ? that can each run in one of two modes, switched per-stage by `config.yaml ? ai_writing.use_builtin_agents.{researcher,outliner,writer,editor}` (default **false**, mtime hot-reloaded, new sessions pick up changes immediately): - -- **Legacy (flag off)**: hard-coded prompts in `prompts/*.py`, direct LLM call (unchanged behavior). -- **Builtin-agent (flag on)**: the node loads the corresponding functional agent from `.deer-flow/agents/ai-writing-/` ? `SOUL.md` becomes the system prompt and `config.yaml`'s `model` overrides the stage model (priority: `agent.model > node explicit fallback (keyword_model/rank_model) > state.model_name`). Admins edit SOUL/model from the agent management page. Runner: `nodes/_agent_runner.py` (`run_writing_agent` / `stream_writing_agent` / `parse_marker_json`). Output contracts (marker + ```json``` blocks, intent.py-style): researcher `[KEYWORDS_READY]` / `[MATERIALS_RANKED]`, outliner `[OUTLINE_READY]`, editor (per-dimension) `[REVIEW_READY]`; writer emits raw section Markdown (keeps per-section streaming) with the legacy `[[SECTION_BLOCKED]]` sentinel in strict mode. **Orchestration stays in the nodes** (search fan-out, per-section loop, 3-dimension review loop, all pause points) ? the agent only does single structured generations, and a missing/empty SOUL silently falls back to the legacy path (`WritingAgentUnavailable`). - -Seeding mirrors the roundtable pattern: verified `config.yaml` + `SOUL.md` ship in `app/gateway/routers/_ai_writing_seed_assets//`; `_ai_writing_seed.py::ensure_ai_writing_functional_agents()` copies them to `.deer-flow/agents/` when missing (never overwrites local edits), called from the startup lifespan (before `_sync_legacy_agents`, so the agents appear in `/api/agents` on first boot) and self-heals from `GET /api/ai-writing/sessions` + `GET /api/ai-writing/article-types`. Tests: `tests/test_ai_writing_seed.py`, `tests/test_ai_writing_agent_runner.py`. - -**Skill-driven retrieval (检索类型).** `material_source` has exactly two values: `general` (通用检索, default) and `notebook` (我的空间); legacy `knowledge_base`/`skill` are normalized to `general` by the researcher node. General retrieval is orchestrated from the **skills configured on the `ai-writing-researcher` agent** (`config.yaml → skills`, editable from the agent management page): the built-in `knowledge-base-search` skill (知识库检索) runs the native keyword fan-out web/intranet search (the old default path), while any other configured skill searches with a `description`-targeted query; skills run in configured order with early-stop at `max_materials`, and an empty/unresolvable skills config falls back to the built-in knowledge search so retrieval always works. The skill itself is seeded from `_ai_writing_seed_assets/skills/knowledge-base-search/SKILL.md` into `skills/public/` (never overwrites), and a pre-existing researcher `config.yaml` lacking the `skills` key is patched in place with the default (explicit `skills: []` is respected). **PAUSE-1 input append flow**: when the user types natural language into the material-confirm input (`re_search`/legacy `enable_skill_search` + `user_query`), the node matches the most relevant configured skill via `SkillPicker(candidates=…)` (skipped when only one skill is configured), searches with the user's words, and **appends** the new materials to the existing package (URL-dedup, original order preserved, no LLM re-rank/truncation, 100-item hard cap). Tests: `tests/test_ai_writing_material_source.py`. - -### Admin Leaderboard Snapshots (`/api/admin/users/leaderboard*`) - -The admin ????? is served from **precomputed per-day snapshots** in `admin_leaderboard_daily_stats` (keyed by `(stat_date, settings_hash)`), not by aggregating raw rows on every request. A background scheduler inside the Gateway (`_leaderboard_snapshot_scheduler_loop` in `app/gateway/app.py`, singleton file-locked) ticks every `ADMIN_LEADERBOARD_BACKGROUND_INTERVAL_SECONDS` (default 300s): it prewarms recent days, then builds any `queued`/`error`/stale-`running` day. `GET /leaderboard` merges the per-day snapshots for the requested range. **Today** is rebuilt whenever its snapshot is older than `ADMIN_LEADERBOARD_TODAY_TTL_SECONDS` (default 43200s = half a day, so the leaderboard refreshes ~twice daily); a past day, once `ready`, is treated as fresh forever. - -Two on-demand/automatic refresh paths layer on top: -- **Manual "????"** ? `POST /leaderboard/refresh-today` force-rebuilds *today's* snapshot **synchronously** (bounded by the same semaphore/timeout the scheduler uses), then returns the merged range. Lets an admin pull the latest real-time records into the stats without waiting for the TTL. Frontend: a "????" button on `AdminUserLeaderboardPage` ? `useRefreshTodayLeaderboard` (`core/admin/hooks.ts`), which primes + invalidates the `["admin-activity","leaderboard"]` query. -- **Daily finalize of the previous day** ? once per day, on/after `ADMIN_LEADERBOARD_FINALIZE_HOUR` (local Beijing, default `1` = ?? 1 ?), the scheduler tick force-recomputes *yesterday* a single time so runs/tool-calls that only landed near midnight are fully captured. The decision is the pure `deerflow.persistence.admin_stats.finalize.should_finalize_previous_day` (status/generated_at/now/hour) ? the rebuild stamps `generated_at` to today, which self-debounces it for the rest of the day (no extra state file). Tests: `tests/test_leaderboard_finalize.py`. - -### Scheduled Task Runtime - -`ScheduledTaskService` (`deerflow/runtime/scheduler/service.py`) polls due tasks every `poll_interval_seconds` and executes each in its own throwaway thread, serialized by a shared `_agent_run_lock`. - -**Stuck-run recovery**: each execution attempt is bounded by `_RUN_ATTEMPT_TIMEOUT_SECONDS` (30 min) via `asyncio.wait_for`. On timeout the run is retried up to `_MAX_RUN_RETRIES` (3) times, then marked `failed`. A watchdog (`_reconcile_stuck_runs`, run every poll) recovers runs left in `running` with no in-process owner ? e.g. orphaned by a crash or restart ? re-driving them through the same retry budget so a task can never stay perpetually "executing". The `scheduled_task_runs.retry_count` column tracks consumed retries. - -**Unique task names**: task names are unique per user (`uq_scheduled_tasks_user_name`). `create_task`/`update_task`/`admin_update_task` proactively check via `_assert_name_available()` and raise `DuplicateTaskNameError` on a collision; the router translates that into HTTP `409` instead of letting the raw DB `IntegrityError` surface as a `500`. - -**HTML page tasks (`task_kind == "html_page"`)**: a second task kind that renders a full HTML document each run instead of a Markdown report. The kind + its config (`html_config`: `page_prompt`, `negative_prompt`, `reference_favorite_id`, `model_name`) live inside `execution_context` ? no schema change to `scheduled_tasks`. Data gathering is delegated to the agent's own search skills (driven by `page_prompt`), so there is no explicit search-query/data-source field; the render model is user-selectable (`model_name`, default `zai-org/GLM-5-FP8`). `_run_task_attempt()` dispatches by kind; `_run_html_page_attempt()` runs the pipeline: gather real data via the lead agent (its search tools) ? optionally load a reference favorite's reusable style/layout ? render with the chosen model (thinking off) ? fault-tolerant JSON parse ? sanitize (BeautifulSoup, strips `
/dashboard`): the Step 3 大屏多页 (模式二) + * — rendered on the fly from the stored structured data via `renderDashboardHtml` + * into the same sandboxed `iframe`. */ export default function PublicRoundtableSharePage({ - report = false, + view = "landing", }: { - report?: boolean; + view?: RoundtableShareView; }) { const { shareCode } = useParams<{ shareCode: string }>(); // 嵌套方可用 ?bg=/?text= 或 postMessage 覆盖背景/文字色。 @@ -108,6 +118,17 @@ export default function PublicRoundtableSharePage({ refetchOnWindowFocus: false, }); + // 大屏多页:用与应用内一致的 React 大屏(DashboardApp)渲染,而非自包含 HTML 模板。 + // 把存储的结构化数据(report-json)解析并 adapt 成 DashboardApp 的入参;与 + // Step3DashboardMode 的「应用渲染」、DashboardStandalonePage 的「新窗口打开」同源。 + const dashboardData = useMemo(() => { + const json = data?.dashboard_json; + if (!json) return undefined; + const parsed = parseReportJson(json); + if (!parsed) return undefined; + return adaptReportData(parsed, data?.generated_at ?? ""); + }, [data?.dashboard_json, data?.generated_at]); + if (isLoading) { return ( @@ -128,8 +149,24 @@ export default function PublicRoundtableSharePage({ ); } - // ── 报告页:整页沙箱 iframe 展示第 3 步结果绘制 ────────────────────────────── - if (report) { + // ── 大屏多页:整屏渲染应用内 React 大屏(DashboardApp),与应用渲染/新窗口打开一致 ── + if (view === "dashboard") { + return ( + + {dashboardData ? ( + + ) : ( + + 暂无大屏多页 + 这次会商还没有生成第三步「大屏多页」的方案报告。 + + )} + + ); + } + + // ── 结果绘制(经典 HTML 看板):整页沙箱 iframe 展示自包含报告 ───────────────── + if (view === "report") { return ( @@ -161,8 +198,10 @@ export default function PublicRoundtableSharePage({ ); } - // ── 落地页:总控最终结论 + 各席位最终交付 + 查看结果绘制(新标签打开报告页)───── - const reportUrl = `${window.location.origin}${window.location.pathname}#/roundtable-share/${shareCode}/report`; + // ── 落地页:结果绘制/大屏多页入口 + 方案总结报告 + 总控最终结论 + 各席位最终交付 ── + const shareBase = `${window.location.origin}${window.location.pathname}#/roundtable-share/${shareCode}`; + const reportUrl = `${shareBase}/report`; + const dashboardUrl = `${shareBase}/dashboard`; return ( @@ -182,23 +221,39 @@ export default function PublicRoundtableSharePage({ - {/* 1. 结果绘制 —— 新标签打开报告页 */} + {/* 1. 结果绘制 / 大屏多页 —— 新标签打开整页报告 */} 结果绘制 - {data.has_report ? ( - - - 查看结果绘制 - - + {data.has_report || data.has_dashboard ? ( + + {data.has_report && ( + + + 查看结果绘制 + + + )} + {data.has_dashboard && ( + + + 查看大屏多页 + + + )} + ) : ( 这次会商还没有生成第三步「结果绘制」的方案报告。 @@ -206,7 +261,22 @@ export default function PublicRoundtableSharePage({ )} - {/* 2. 总控最终结论 */} + {/* 2. 方案总结报告(Markdown) */} + {data.has_summary_report && ( + + + + 方案总结报告 + + + + {data.summary_report_md} + + + + )} + + {/* 3. 总控最终结论 */} @@ -223,7 +293,7 @@ export default function PublicRoundtableSharePage({ )} - {/* 3. 各席位最终交付 */} + {/* 4. 各席位最终交付 */} diff --git a/frontend-web/src/pages/light-app-management.css b/frontend-web/src/pages/light-app-management.css new file mode 100644 index 00000000..bc3eafc0 --- /dev/null +++ b/frontend-web/src/pages/light-app-management.css @@ -0,0 +1,570 @@ +@import "../../docs/ui规范-深蓝/tokens.css"; +@import "./light-app-pages.css"; + +/** + * 应用管理页 —— 深蓝色主题适配(《深蓝色 UI 规范》) + * 浅色样式仍由组件内 Tailwind 承担;.dark 下本文件覆盖为 ZS-004 / ZS-002 令牌。 + */ +.dark .light-app-management-page { + color: var(--color-text); +} + +/* ── 页头 / 文案 ─────────────────────────────────────────────────────── */ +.dark .light-app-management-page .lam-title { + color: var(--color-text-strong); +} + +.dark .light-app-management-page .lam-desc { + color: var(--color-text-secondary); +} + +.dark .light-app-management-page .lam-desc code { + background: rgba(22, 119, 255, 0.12); + color: var(--color-link); +} + +.dark .light-app-management-page .lam-primary-btn { + background: var(--color-primary); + box-shadow: none; +} + +.dark .light-app-management-page .lam-primary-btn:hover { + background: var(--color-primary-hover); +} + +.dark .light-app-management-page .lam-primary-btn:active { + background: var(--color-primary-active); +} + +/* ── 工具条 / 搜索 / 视图切换 ─────────────────────────────────────────── */ +.dark .light-app-management-page .lam-toolbar { + background: var(--color-bg-card); + border: 1px solid var(--color-border); + box-shadow: var(--shadow-1); +} + +.dark .light-app-management-page .lam-search-input { + background: var(--color-bg-page); + border-color: var(--color-border); + color: var(--color-text); +} + +.dark .light-app-management-page .lam-search-input::placeholder { + color: var(--color-text-disabled); +} + +.dark .light-app-management-page .lam-search-input:hover { + background: var(--color-bg-card); + border-color: var(--color-link); +} + +.dark .light-app-management-page .lam-search-input:focus { + background: var(--color-bg-card); + border-color: var(--color-primary); + box-shadow: 0 0 0 2px rgba(22, 119, 255, 0.16); + outline: none; +} + +.dark .light-app-management-page .lam-view-toggle { + background: var(--color-bg-page); + border-color: var(--color-border); +} + +.dark .light-app-management-page .lam-view-toggle-btn.is-active { + background: var(--color-bg-card); + color: var(--color-link); + box-shadow: var(--shadow-1); +} + +.dark .light-app-management-page .lam-view-toggle-btn:not(.is-active) { + color: var(--color-text-secondary); +} + +.dark .light-app-management-page .lam-view-toggle-btn:not(.is-active):hover { + color: var(--color-text); +} + +.dark .light-app-management-page .lam-count { + color: var(--color-text-secondary); +} + +.dark .light-app-management-page .lam-count strong { + color: var(--color-link); +} + +/* ── 表格(§28)──────────────────────────────────────────────────────── */ +.dark .light-app-management-page .lam-table-wrap { + background: var(--color-bg-page); + border: 1px solid var(--color-border); + box-shadow: var(--shadow-1); +} + +.dark .light-app-management-page .lam-table thead tr { + background: var(--color-bg-card); + border-color: var(--color-border); + color: var(--color-text); +} + +.dark .light-app-management-page .lam-table tbody { + border-color: var(--color-border); +} + +.dark .light-app-management-page .lam-table tbody tr { + border-color: var(--color-border); +} + +.dark .light-app-management-page .lam-table tbody tr:hover { + background: rgba(22, 119, 255, 0.08); +} + +.dark .light-app-management-page .lam-row-name { + color: var(--color-text); +} + +.dark .light-app-management-page .lam-table tbody tr:hover .lam-row-name { + color: var(--color-link); +} + +.dark .light-app-management-page .lam-tag-window { + border-color: rgba(245, 34, 45, 0.35); + background: var(--func-error-bg); + color: #ff7875; +} + +.dark .light-app-management-page .lam-tag-iframe { + border-color: rgba(22, 119, 255, 0.35); + background: var(--func-info-bg); + color: var(--color-link); +} + +.dark .light-app-management-page .lam-url { + color: var(--color-text-secondary); +} + +.dark .light-app-management-page .lam-table tbody tr:hover .lam-url { + color: var(--color-text); +} + +.dark .light-app-management-page .lam-url a { + color: inherit; +} + +.dark .light-app-management-page .lam-url a:hover { + color: var(--color-link); +} + +.dark .light-app-management-page .lam-mount-tag { + background: var(--func-info-bg); + color: var(--color-link); +} + +.dark .light-app-management-page .lam-mount-dot { + background: var(--color-primary); +} + +.dark .light-app-management-page .lam-param-btn { + border-color: var(--color-border); + background: var(--color-bg-page); + color: var(--color-text-secondary); +} + +.dark .light-app-management-page .lam-param-btn:hover { + background: rgba(22, 119, 255, 0.08); + color: var(--color-text); +} + +.dark .light-app-management-page .lam-param-popover { + border-color: var(--color-border); + background: var(--color-bg-card); + box-shadow: var(--shadow-3); +} + +.dark .light-app-management-page .lam-param-popover-head { + border-color: var(--color-border); + color: var(--color-text-secondary); +} + +.dark .light-app-management-page .lam-param-popover-close { + color: var(--color-link); +} + +.dark .light-app-management-page .lam-param-item { + border-color: var(--color-border); + background: var(--color-bg-page); +} + +.dark .light-app-management-page .lam-param-key { + color: #ff7875; +} + +.dark .light-app-management-page .lam-param-val { + color: var(--color-text-secondary); +} + +.dark .light-app-management-page .lam-status-on { + color: var(--func-success); +} + +.dark .light-app-management-page .lam-status-on-dot { + background: var(--func-success); +} + +.dark .light-app-management-page .lam-status-on-ping { + background: var(--func-success); +} + +.dark .light-app-management-page .lam-status-off { + color: var(--color-text-disabled); +} + +.dark .light-app-management-page .lam-status-off-dot { + background: var(--color-text-disabled); +} + +.dark .light-app-management-page .lam-icon-btn { + color: var(--color-text-secondary); +} + +.dark .light-app-management-page .lam-icon-btn:hover { + background: rgba(22, 119, 255, 0.08); + color: var(--color-link); +} + +.dark .light-app-management-page .lam-icon-btn-danger:hover { + background: var(--func-error-bg); + color: var(--func-error); +} + +.dark .light-app-management-page .lam-empty-icon { + background: var(--color-bg-page); + border: 1px solid var(--color-border); + color: var(--color-text-disabled); +} + +.dark .light-app-management-page .lam-empty-title { + color: var(--color-text-secondary); +} + +.dark .light-app-management-page .lam-empty-sub { + color: var(--color-text-disabled); +} + +/* ── 卡片视图(§22 / §43)────────────────────────────────────────────── */ +.dark .light-app-management-page .lam-card { + background: var(--color-bg-card); + border: 1px solid var(--color-border); + box-shadow: var(--shadow-1); +} + +.dark .light-app-management-page .lam-card:hover { + box-shadow: var(--shadow-2); +} + +.dark .light-app-management-page .lam-card-title { + color: var(--color-text-strong); +} + +.dark .light-app-management-page .lam-card:hover .lam-card-title { + color: var(--color-link); +} + +.dark .light-app-management-page .lam-card-desc { + color: var(--color-text-secondary); +} + +.dark .light-app-management-page .lam-card-url-box { + background: var(--color-bg-page); +} + +.dark .light-app-management-page .lam-card-param { + border-color: var(--color-border); + background: var(--color-bg-page); + color: var(--color-text-secondary); +} + +.dark .light-app-management-page .lam-card-mount { + background: var(--func-info-bg); + color: var(--color-link); +} + +.dark .light-app-management-page .lam-card-footer { + border-color: var(--color-border); + background: rgba(12, 24, 54, 0.5); +} + +/* ── 底部提示(§33 警告)────────────────────────────────────────────── */ +.dark .light-app-management-page .lam-tip-box { + background: var(--func-warning-bg); + border: 1px solid rgba(250, 173, 20, 0.25); + box-shadow: none; +} + +.dark .light-app-management-page .lam-tip-head { + border-color: rgba(250, 173, 20, 0.2); + color: var(--func-warning); +} + +.dark .light-app-management-page .lam-tip-list { + color: var(--color-text-secondary); +} + +.dark .light-app-management-page .lam-tip-list strong { + color: var(--color-text); +} + +.dark .light-app-management-page .lam-tip-bullet { + color: var(--func-warning); +} + +/* ── 弹窗(§21 Modal)────────────────────────────────────────────────── */ +.dark .light-app-management-page .lam-dialog-overlay { + background: rgba(6, 14, 36, 0.6); +} + +.dark .light-app-management-page .lam-dialog { + border-color: var(--color-border); + background: linear-gradient(180deg, var(--color-bg-card), var(--color-bg-page)); + box-shadow: var(--shadow-3); + color: var(--color-text); +} + +.dark .light-app-management-page .lam-dialog-header { + border-color: var(--color-border); + border-bottom: none; + padding-bottom: 0; +} + +.dark .light-app-management-page .lam-dialog-header::after { + content: ""; + display: block; + height: 1px; + margin-top: 16px; + margin-bottom: 0; + background: var(--gradient-title-line); +} + +.dark .light-app-management-page .lam-dialog-icon { + background: var(--func-info-bg); + color: var(--color-link); +} + +.dark .light-app-management-page .lam-dialog-title { + color: var(--color-text-strong); +} + +.dark .light-app-management-page .lam-dialog-close { + color: var(--color-text-secondary); +} + +.dark .light-app-management-page .lam-dialog-close:hover { + background: rgba(22, 119, 255, 0.08); + color: var(--color-link); +} + +.dark .light-app-management-page .lam-form-label { + color: var(--color-text); +} + +.dark .light-app-management-page .lam-form-label .lam-required { + color: var(--func-error); +} + +.dark .light-app-management-page .lam-form-label svg { + color: var(--color-text-secondary); +} + +.dark .light-app-management-page .lam-form-hint { + color: var(--color-text-secondary); +} + +.dark .light-app-management-page .lam-dialog .lam-input, +.dark .light-app-management-page .lam-dialog input[type="text"] { + background: var(--color-bg-page); + border-color: var(--color-border); + color: var(--color-text); +} + +.dark .light-app-management-page .lam-dialog .lam-input::placeholder, +.dark .light-app-management-page .lam-dialog input[type="text"]::placeholder { + color: var(--color-text-disabled); +} + +.dark .light-app-management-page .lam-dialog .lam-input:focus, +.dark .light-app-management-page .lam-dialog input[type="text"]:focus { + border-color: var(--color-primary); + box-shadow: 0 0 0 2px rgba(22, 119, 255, 0.16); + outline: none; +} + +.dark .light-app-management-page .lam-param-panel { + border-color: var(--color-border); + background: var(--color-bg-page); +} + +.dark .light-app-management-page .lam-param-row { + border-color: var(--color-border); + background: var(--color-bg-card); +} + +.dark .light-app-management-page .lam-param-theme-box { + background: var(--color-bg-page); +} + +.dark .light-app-management-page .lam-param-empty { + border-color: var(--color-border); + background: var(--color-bg-page); + color: var(--color-text-secondary); +} + +.dark .light-app-management-page .lam-add-param-btn { + border-color: var(--color-border-strong); + background: var(--color-bg-card); + color: var(--color-text); +} + +.dark .light-app-management-page .lam-add-param-btn:hover { + border-color: var(--color-primary); + color: var(--color-link); +} + +.dark .light-app-management-page .lam-dialog-footer { + border-color: var(--color-border); +} + +.dark .light-app-management-page .lam-dialog-cancel { + border-color: var(--color-border-strong); + background: transparent; + color: var(--color-text); +} + +.dark .light-app-management-page .lam-dialog-cancel:hover { + border-color: var(--color-link); + color: var(--color-link); +} + +.dark .light-app-management-page .lam-dialog-save { + background: var(--color-primary); + box-shadow: none; +} + +.dark .light-app-management-page .lam-dialog-save:hover { + background: var(--color-primary-hover); +} + +.dark .light-app-management-page .lam-dialog-save:active { + background: var(--color-primary-active); +} + +.dark .light-app-management-page .lam-switch-on { + background: var(--color-primary); +} + +.dark .light-app-management-page .lam-switch-off { + background: var(--color-text-disabled); +} + +/* ── TDesign Select(§15)──────────────────────────────────────────── */ +.dark .light-app-management-page .t-select .t-input, +.dark .light-app-management-page .t-select .t-input__wrap { + background: var(--color-bg-page); + border-color: var(--color-border); + border-radius: var(--radius-input); +} + +.dark .light-app-management-page .t-select .t-input__inner { + color: var(--color-text); +} + +.dark .light-app-management-page .t-select .t-input__inner::placeholder { + color: var(--color-text-disabled); +} + +.dark .light-app-management-page .t-select:hover .t-input, +.dark .light-app-management-page .t-select:hover .t-input__wrap { + border-color: var(--color-link); +} + +.dark .light-app-management-page .t-select.t-is-focused .t-input, +.dark .light-app-management-page .t-select.t-is-focused .t-input__wrap { + border-color: var(--color-primary); + box-shadow: 0 0 0 2px rgba(22, 119, 255, 0.16); +} + +.dark .light-app-management-page .t-select .t-fake-arrow { + color: var(--color-text-secondary); +} + +.dark .light-app-management-page .t-select__dropdown { + background: var(--color-bg-card); + border-color: var(--color-border); + box-shadow: var(--shadow-2); +} + +.dark .light-app-management-page .t-select-option { + color: var(--color-text); +} + +.dark .light-app-management-page .t-select-option:hover { + background: rgba(22, 119, 255, 0.08); +} + +.dark .light-app-management-page .t-select-option.t-is-selected { + background: rgba(22, 119, 255, 0.16); + color: var(--color-link); +} + +/* shadcn Input / Textarea in dialog */ +.dark .light-app-management-page .lam-dialog [data-slot="input"], +.dark .light-app-management-page .lam-dialog textarea { + background: var(--color-bg-page); + border-color: var(--color-border); + color: var(--color-text); +} + +.dark .light-app-management-page .lam-dialog [data-slot="input"]:focus-visible, +.dark .light-app-management-page .lam-dialog textarea:focus-visible { + border-color: var(--color-primary); + box-shadow: 0 0 0 2px rgba(22, 119, 255, 0.16); +} + +.dark .light-app-management-page .lam-param-theme-box span { + color: var(--color-text-secondary); +} + +/* TDesign 下拉挂载到 body 时的深蓝样式 */ +.dark .lam-select-popup.t-popup { + background: var(--color-bg-card); + border-color: var(--color-border); + box-shadow: var(--shadow-2); +} + +.dark .lam-select-popup .t-select-option { + color: var(--color-text); +} + +.dark .lam-select-popup .t-select-option:hover { + background: rgba(22, 119, 255, 0.08); +} + +.dark .lam-select-popup .t-select-option.t-is-selected { + background: rgba(22, 119, 255, 0.16); + color: var(--color-link); +} + +.dark .light-app-management-page .lam-search-input + button, +.dark .light-app-management-page .lam-toolbar .absolute.text-neutral-400 { + color: var(--color-text-secondary); +} + +.dark .light-app-management-page .lam-table .text-neutral-300 { + color: var(--color-text-disabled); +} + +.dark .light-app-management-page .lam-card-url-box button { + color: var(--color-text-secondary); +} + +.dark .light-app-management-page .lam-card-url-box button:hover { + background: rgba(22, 119, 255, 0.08); + color: var(--color-link); +} diff --git a/frontend-web/src/pages/light-app-pages.css b/frontend-web/src/pages/light-app-pages.css new file mode 100644 index 00000000..459be997 --- /dev/null +++ b/frontend-web/src/pages/light-app-pages.css @@ -0,0 +1,12 @@ +@import "../../docs/ui规范-深蓝/tokens.css"; + +/** 轻应用中心列表 / 管理页 —— 深色模式共用页面底图(与 workspace 深蓝主题一致) */ +.dark .light-app-management-page, +.dark .light-app-list-page { + background-color: var(--color-bg-page); + background-image: url("../assets/img/layout-bg-theme-dark-blue.svg"); + background-size: cover; + background-position: center top; + background-repeat: no-repeat; + background-attachment: fixed; +} diff --git a/frontend-web/src/roundtable-planning/components/HighFidelityReport.tsx b/frontend-web/src/roundtable-planning/components/HighFidelityReport.tsx index 533f8ba9..f12a0bb5 100644 --- a/frontend-web/src/roundtable-planning/components/HighFidelityReport.tsx +++ b/frontend-web/src/roundtable-planning/components/HighFidelityReport.tsx @@ -1009,6 +1009,7 @@ export const HighFidelityReport: React.FC = ({ filepath={fileSandboxVirtualUrl} threadId={fileSandboxTarget.threadId} onClose={handleCloseFileSandbox} + allowVirtualMarkdownEdit /> )} diff --git a/frontend-web/src/roundtable-planning/components/Step2SandboxLayout.tsx b/frontend-web/src/roundtable-planning/components/Step2SandboxLayout.tsx index 1d69dea8..59585fa9 100644 --- a/frontend-web/src/roundtable-planning/components/Step2SandboxLayout.tsx +++ b/frontend-web/src/roundtable-planning/components/Step2SandboxLayout.tsx @@ -115,6 +115,7 @@ export function Step2SandboxLayout({ filepath={selectedArtifact} threadId={previewThreadId} onClose={artifactsFill && onArtifactsClose ? handleArtifactsClose : undefined} + allowVirtualMarkdownEdit /> ) : ( diff --git a/frontend-web/src/strategy-components/Header.tsx b/frontend-web/src/strategy-components/Header.tsx index 7e4f144f..b27f18a5 100644 --- a/frontend-web/src/strategy-components/Header.tsx +++ b/frontend-web/src/strategy-components/Header.tsx @@ -139,6 +139,10 @@ const Header = ({ collapsed }: HeaderProps) => { return email ? email.split("@")[0] : "用户"; }, [auth?.email]); const showTopNav = isDark && !collapsed && !isWujieEnv(); + // 浅色(红/蓝)顶栏:一级导航正常横排进顶栏,不做深色那种围绕基座的特殊处理。 + // 与 collapsed 解耦(折叠只影响侧边栏,一级导航始终可见);wujie 嵌入下隐藏, + // 此时一级导航回落到侧边栏(见 page-sidebar.tsx)。 + const showLightTopNav = !isDark && !isWujieEnv(); const hasActiveChild = (item: MenuItem): boolean => { if (item.path && pathname === item.path) { @@ -194,6 +198,20 @@ const Header = ({ collapsed }: HeaderProps) => { ); }; + const renderLightTopItem = (item: MenuItem) => { + const isSelected = item.path ? pathname === item.path : hasActiveChild(item); + return ( + handleNavItemClick(item)} + > + {item.label} + + ); + }; + // 右侧操作区(换肤/通知/用户)—— 深浅两套布局共用 const actionCluster = ( <> @@ -309,6 +327,27 @@ const Header = ({ collapsed }: HeaderProps) => { > + ) : showLightTopNav ? ( + <> + {/* 浅色顶栏:logo + 一级导航横排(白字落在 banner 上) */} + + + + + + + CM助手 + + + + {visibleMenuItems.map(renderLightTopItem)} + + + + + {actionCluster} + + > ) : ( <> @@ -335,6 +374,7 @@ const Header = ({ collapsed }: HeaderProps) => { > )} + {/* 三级菜单横条仅深色用;浅色三级已改到侧边栏右侧列(见 page-sidebar.tsx) */} {showTopNav && thirdSubNavItems.length ? ( diff --git a/frontend-web/src/strategy-components/lib/light-app.ts b/frontend-web/src/strategy-components/lib/light-app.ts index 481b838b..ab796bf6 100644 --- a/frontend-web/src/strategy-components/lib/light-app.ts +++ b/frontend-web/src/strategy-components/lib/light-app.ts @@ -19,11 +19,17 @@ export function mountLabel(value: string | null | undefined): string { return MOUNT_OPTIONS.find((o) => o.value === value)?.label ?? value; } -/** 登录参数可选来源;值均可在 userInfo 中取到,"other" 表示自填。 */ +/** + * 登录参数可选来源;值均可在 userInfo 中取到,"other" 表示自填。 + * accessToken / yUserId 取登录路由透传值(写入 userInfo 的同名键); + * userName / gateDepartName / typeName 取 consumer 接口 userDetailByYuserId 的返回值。 + */ export const LOGIN_SOURCE_OPTIONS: { label: string; value: string }[] = [ { label: "accessToken", value: "accessToken" }, { label: "yUserId", value: "yUserId" }, { label: "userName", value: "userName" }, + { label: "gateDepartName", value: "gateDepartName" }, + { label: "typeName", value: "typeName" }, { label: "其他(自定义值)", value: "other" }, ]; diff --git a/frontend-web/src/styles/globals.css b/frontend-web/src/styles/globals.css index 7ebd17de..3494b65c 100644 --- a/frontend-web/src/styles/globals.css +++ b/frontend-web/src/styles/globals.css @@ -932,7 +932,7 @@ body { .strategy-app-header { background-color: transparent; background-image: url("../assets/img/red-header.png"); - background-size: 100% 100%; + background-size: cover; background-repeat: no-repeat; background-position: center; } diff --git a/offline-backend-20260512/backend/CLAUDE.md b/offline-backend-20260512/backend/CLAUDE.md index 18e7823d..f6812132 100644 --- a/offline-backend-20260512/backend/CLAUDE.md +++ b/offline-backend-20260512/backend/CLAUDE.md @@ -1,759 +1,759 @@ -# CLAUDE.md -?? -This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. - -## Project Overview - -DeerFlow is a LangGraph-based AI super agent system with a full-stack architecture. The backend provides a "super agent" with sandbox execution, persistent memory, subagent delegation, and extensible tool integration - all operating in per-thread isolated environments. - -**Architecture**: -- **Gateway API** (port 8001): REST API plus embedded LangGraph-compatible agent runtime -- **Frontend** (port 3000): Next.js web interface -- **Nginx** (port 2026): Unified reverse proxy entry point -- **Provisioner** (port 8002, optional in Docker dev): Started only when sandbox is configured for provisioner/Kubernetes mode - -**Runtime**: -- `make dev`, Docker dev, and production all run the agent runtime in Gateway via `RunManager` + `run_agent()` + `StreamBridge` (`packages/harness/deerflow/runtime/`). Nginx exposes that runtime at `/api/langgraph/*` and rewrites it to Gateway's native `/api/*` routers. - -**Project Structure**: -``` -deer-flow/ -??? Makefile # Root commands (check, install, dev, stop) -??? config.yaml # Main application configuration -??? extensions_config.json # MCP servers and skills configuration -??? backend/ # Backend application (this directory) -? ??? Makefile # Backend-only commands (dev, gateway, lint) -? ??? langgraph.json # LangGraph Studio graph configuration -? ??? packages/ -? ? ??? harness/ # deerflow-harness package (import: deerflow.*) -? ? ??? pyproject.toml -? ? ??? deerflow/ -? ? ??? agents/ # LangGraph agent system -? ? ? ??? lead_agent/ # Main agent (factory + system prompt) -? ? ? ??? middlewares/ # 10 middleware components -? ? ? ??? memory/ # Memory V2: provider/manager/builtin/hindsight -? ? ? ??? thread_state.py # ThreadState schema -? ? ??? sandbox/ # Sandbox execution system -? ? ? ??? local/ # Local filesystem provider -? ? ? ??? sandbox.py # Abstract Sandbox interface -? ? ? ??? tools.py # bash, ls, read/write/str_replace -? ? ? ??? middleware.py # Sandbox lifecycle management -? ? ??? subagents/ # Subagent delegation system -? ? ? ??? builtins/ # general-purpose, bash agents -? ? ? ??? executor.py # Background execution engine -? ? ? ??? registry.py # Agent registry -? ? ??? tools/builtins/ # Built-in tools (present_files, ask_clarification, view_image) -? ? ??? mcp/ # MCP integration (tools, cache, client) -? ? ??? models/ # Model factory with thinking/vision support -? ? ??? skills/ # Skills discovery, loading, parsing -? ? ??? config/ # Configuration system (app, model, sandbox, tool, etc.) -? ? ??? community/ # Community tools (tavily, jina_ai, firecrawl, image_search, aio_sandbox) -? ? ??? reflection/ # Dynamic module loading (resolve_variable, resolve_class) -? ? ??? utils/ # Utilities (network, readability) -? ? ??? client.py # Embedded Python client (DeerFlowClient) -? ??? app/ # Application layer (import: app.*) -? ? ??? gateway/ # FastAPI Gateway API -? ? ? ??? app.py # FastAPI application -? ? ? ??? routers/ # FastAPI route modules (models, mcp, memory, skills, uploads, threads, artifacts, agents, suggestions, channels) -? ? ??? channels/ # IM platform integrations -? ??? tests/ # Test suite -? ??? docs/ # Documentation -??? frontend/ # Next.js frontend application -??? skills/ # Agent skills directory - ??? public/ # Public skills (committed) - ??? custom/ # Custom skills (gitignored) -``` - -## Important Development Guidelines - -### Documentation Update Policy -**CRITICAL: Always update README.md and CLAUDE.md after every code change** - -When making code changes, you MUST update the relevant documentation: -- Update `README.md` for user-facing changes (features, setup, usage instructions) -- Update `CLAUDE.md` for development changes (architecture, commands, workflows, internal systems) -- Keep documentation synchronized with the codebase at all times -- Ensure accuracy and timeliness of all documentation - -## Commands - -**Root directory** (for full application): -```bash -make check # Check system requirements -make install # Install all dependencies (frontend + backend) -make dev # Start all services (Gateway + Frontend + Nginx), with config.yaml preflight -make start # Start production services locally -make stop # Stop all services -``` - -**Backend directory** (for backend development only): -```bash -make install # Install backend dependencies -make dev # Run Gateway API with reload (port 8001) -make gateway # Run Gateway API only (port 8001) -make test # Run all backend tests -make lint # Lint with ruff -make format # Format code with ruff -``` - -Regression tests related to Docker/provisioner behavior: -- `tests/test_docker_sandbox_mode_detection.py` (mode detection from `config.yaml`) -- `tests/test_provisioner_kubeconfig.py` (kubeconfig file/directory handling) - -Boundary check (harness ? app import firewall): -- `tests/test_harness_boundary.py` ? ensures `packages/harness/deerflow/` never imports from `app.*` - -CI runs these regression tests for every pull request via [.github/workflows/backend-unit-tests.yml](../.github/workflows/backend-unit-tests.yml). - -## Architecture - -### Harness / App Split - -The backend is split into two layers with a strict dependency direction: - -- **Harness** (`packages/harness/deerflow/`): Publishable agent framework package (`deerflow-harness`). Import prefix: `deerflow.*`. Contains agent orchestration, tools, sandbox, models, MCP, skills, config ? everything needed to build and run agents. -- **App** (`app/`): Unpublished application code. Import prefix: `app.*`. Contains the FastAPI Gateway API and IM channel integrations (Feishu, Slack, Telegram, DingTalk). - -**Dependency rule**: App imports deerflow, but deerflow never imports app. This boundary is enforced by `tests/test_harness_boundary.py` which runs in CI. - -**Import conventions**: -```python -# Harness internal -from deerflow.agents import make_lead_agent -from deerflow.models import create_chat_model - -# App internal -from app.gateway.app import app -from app.channels.service import start_channel_service - -# App ? Harness (allowed) -from deerflow.config import get_app_config - -# Harness ? App (FORBIDDEN ? enforced by test_harness_boundary.py) -# from app.gateway.routers.uploads import ... # ? will fail CI -``` - -### Agent System - -**Lead Agent** (`packages/harness/deerflow/agents/lead_agent/agent.py`): -- Entry point: `make_lead_agent(config: RunnableConfig)` registered in `langgraph.json` -- Dynamic model selection via `create_chat_model()` with thinking/vision support -- Tools loaded via `get_available_tools()` - combines sandbox, built-in, MCP, community, and subagent tools -- System prompt generated by `apply_prompt_template()` with skills, memory, and subagent instructions - -**ThreadState** (`packages/harness/deerflow/agents/thread_state.py`): -- Extends `AgentState` with: `sandbox`, `thread_data`, `title`, `artifacts`, `todos`, `uploaded_files`, `viewed_images` -- Uses custom reducers: `merge_artifacts` (deduplicate), `merge_viewed_images` (merge/clear) - -**Runtime Configuration** (via `config.configurable`): -- `thinking_enabled` - Enable model's extended thinking -- `model_name` - Select specific LLM model -- `is_plan_mode` - Enable TodoList middleware -- `subagent_enabled` - Enable task delegation tool - -### Middleware Chain - -Lead-agent middlewares are assembled in strict append order across `packages/harness/deerflow/agents/middlewares/tool_error_handling_middleware.py` (`build_lead_runtime_middlewares`) and `packages/harness/deerflow/agents/lead_agent/agent.py` (`_build_middlewares`): - -1. **ThreadDataMiddleware** - Creates per-thread directories under the user's isolation scope (`backend/.deer-flow/users/{user_id}/threads/{thread_id}/user-data/{workspace,uploads,outputs}`); resolves `user_id` via `get_effective_user_id()` (falls back to `"default"` in no-auth mode); Web UI thread deletion now follows LangGraph thread removal with Gateway cleanup of the local thread directory -2. **UploadsMiddleware** - Tracks and injects newly uploaded files into conversation -3. **SandboxMiddleware** - Acquires sandbox, stores `sandbox_id` in state -4. **DanglingToolCallMiddleware** - Injects placeholder ToolMessages for AIMessage tool_calls that lack responses (e.g., due to user interruption), including raw provider tool-call payloads preserved only in `additional_kwargs["tool_calls"]` -5. **LLMErrorHandlingMiddleware** - Normalizes provider/model invocation failures into recoverable assistant-facing errors before later middleware/tool stages run -6. **GuardrailMiddleware** - Pre-tool-call authorization via pluggable `GuardrailProvider` protocol (optional, if `guardrails.enabled` in config). Evaluates each tool call and returns error ToolMessage on deny. Three provider options: built-in `AllowlistProvider` (zero deps), OAP policy providers (e.g. `aport-agent-guardrails`), or custom providers. See [docs/GUARDRAILS.md](docs/GUARDRAILS.md) for setup, usage, and how to implement a provider. -7. **SandboxAuditMiddleware** - Audits sandboxed shell/file operations for security logging before tool execution continues -8. **ToolErrorHandlingMiddleware** - Converts tool exceptions into error `ToolMessage`s so the run can continue instead of aborting -9. **SummarizationMiddleware** (`DeerFlowSummarizationMiddleware`) - Context reduction when approaching token limits (optional, if enabled). **Non-destructive**: overrides the stock `before_model` (which persists a `RemoveMessage(REMOVE_ALL_MESSAGES)`) to a no-op and instead compresses **transiently** in `wrap_model_call` ? the model request is replaced with `[summary, *recent]` while the full original conversation stays in state (so the frontend keeps rendering the user's real Q&A). The `summary` message is `hide_from_ui` and never persisted; summaries are cached per-thread (bounded, sticky boundary) to avoid an LLM call on every model invocation. Tests: `tests/test_summarization_middleware.py` -10. **TodoListMiddleware** - Task tracking with `write_todos` tool (optional, if plan_mode) -11. **TokenUsageMiddleware** - Records token usage metrics when token tracking is enabled (optional) -12. **TitleMiddleware** - Two-phase auto-title (never shows "untitled"): `before_model` sets an instant question-derived *provisional* title the moment the first user message arrives (no LLM, zero latency), then `after_model` asynchronously generates the final LLM title and replaces it (tracked via the `title_provisional` state flag). Normalizes structured message content before prompting the title model -13. **MemoryMiddleware** - Memory V2: `wrap_model_call` injects Hindsight recall transiently; `after_agent` auto-retains the turn to Hindsight -14. **ViewImageMiddleware** - Injects base64 image data before LLM call (conditional on vision support) -15. **DeferredToolFilterMiddleware** - Hides deferred tool schemas from the bound model until tool search is enabled (optional) -16. **SubagentLimitMiddleware** - Truncates excess `task` tool calls from model response to enforce `MAX_CONCURRENT_SUBAGENTS` limit (optional, if `subagent_enabled`) -17. **LoopDetectionMiddleware** - Detects repeated tool-call loops; hard-stop responses clear both structured `tool_calls` and raw provider tool-call metadata before forcing a final text answer -18. **ClarificationMiddleware** - Intercepts `ask_clarification` tool calls, interrupts via `Command(goto=END)` (must be last) -8a. **ToolMetricsMiddleware** - Appended immediately after `ToolErrorHandlingMiddleware` in `_build_runtime_middlewares`, so it sits *deepest* in the `wrap_tool_call` chain. Records one `tool_call_metrics` row per tool/skill/MCP invocation ? timing duration and bucketing failures into a coarse `error_category` (rate_limited / ip_blocked / auth_error / timeout / network_error / not_found / empty_result / tool_error). It catches both raised exceptions (before #8 converts them) and *self-handled* errors returned as error `ToolMessage`s (search tools that swallow HTTP 429/403). It also captures the targeted Agent Skill into `tool_call_metrics.skill_name`: the lead agent invokes a skill by `read_file`-ing its `/mnt/skills///SKILL.md` path (per the `` prompt block ? there is no dedicated "use skill" tool), so `skill_name` is recovered by scanning every tool call's string arguments for the `skills/{public,custom}/{name}/` path shape (`_SKILL_PATH_RE`), plus the explicit `name` argument of `skill_view`/`skill_manage`/`skill_archive`. `skill_view`-specific success detection handles its swallowed failures (`Skill 'x' not found.` returned as a plain string rather than raised). Regression coverage: `tests/test_tool_metrics_middleware.py`. Writes are best-effort fire-and-forget. **Per-run skill merging**: `SqlToolMetricsStore.record` upserts skill rows by `(run_id, skill_name)` instead of inserting one row per call ? a skill touched by several tool calls / retries inside one run collapses to a single row. Any success in the run wins (a skill that failed then finally succeeded is recorded as one `success`); a skill that only ever failed yields one `error` row whose `error_message` accumulates every attempt's message; durations are summed. Non-skill tool calls, and skill calls with no `run_id`, keep one row each. The read?merge?write is guarded by a per-event-loop `asyncio.Lock`. Surfaced by the admin `GET /api/admin/tool-metrics` router (which has a `kind=skill` filter and a per-skill `/summary` breakdown). -9. **PromptPrefixMiddleware** - Injects the admin-configured prompt prefix into the model request only. The prefix is tagged onto the first human message's `additional_kwargs['prompt_prefix']` by the Gateway (`app.gateway.services`) and prepended to content here at call time, so the persisted/displayed message stays prefix-free -10. **SummarizationMiddleware** (`DeerFlowSummarizationMiddleware`) - Context reduction when approaching token limits (optional, if enabled). **Non-destructive**: overrides the stock `before_model` (which persists a `RemoveMessage(REMOVE_ALL_MESSAGES)`) to a no-op and instead compresses **transiently** in `wrap_model_call` ? the model request is replaced with `[summary, *recent]` while the full original conversation stays in state (so the frontend keeps rendering the user's real Q&A). The `summary` message is `hide_from_ui` and never persisted; summaries are cached per-thread (bounded, sticky boundary) to avoid an LLM call on every model invocation. Tests: `tests/test_summarization_middleware.py` -11. **TodoListMiddleware** - Task tracking with `write_todos` tool (optional, if plan_mode) -12. **TokenUsageMiddleware** - Records token usage metrics when token tracking is enabled (optional) -13. **TitleMiddleware** - Two-phase auto-title (never shows "untitled"): `before_model` sets an instant question-derived *provisional* title the moment the first user message arrives (no LLM, zero latency), then `after_model` asynchronously generates the final LLM title and replaces it (tracked via the `title_provisional` state flag). Normalizes structured message content before prompting the title model -14. **MemoryMiddleware** - Queues conversations for async memory update (filters to user + final AI responses) -15. **ViewImageMiddleware** - Injects base64 image data before LLM call (conditional on vision support) -16. **DeferredToolFilterMiddleware** - Hides deferred tool schemas from the bound model until tool search is enabled (optional) -17. **SubagentLimitMiddleware** - Truncates excess `task` tool calls from model response to enforce `MAX_CONCURRENT_SUBAGENTS` limit (optional, if `subagent_enabled`) -18. **LoopDetectionMiddleware** - Detects repeated tool-call loops; hard-stop responses clear both structured `tool_calls` and raw provider tool-call metadata before forcing a final text answer -19. **ClarificationMiddleware** - Intercepts `ask_clarification` tool calls, interrupts via `Command(goto=END)` (must be last) - -### Configuration System - -**Main Configuration** (`config.yaml`): - -Setup: Copy `config.example.yaml` to `config.yaml` in the **project root** directory. - -**Config Versioning**: `config.example.yaml` has a `config_version` field. On startup, `AppConfig.from_file()` compares user version vs example version and emits a warning if outdated. Missing `config_version` = version 0. Run `make config-upgrade` to auto-merge missing fields. When changing the config schema, bump `config_version` in `config.example.yaml`. - -**Config Caching**: `get_app_config()` caches the parsed config, but automatically reloads it when the resolved config path changes or the file's mtime increases. This keeps Gateway and LangGraph reads aligned with `config.yaml` edits without requiring a manual process restart. - -Configuration priority: -1. Explicit `config_path` argument -2. `DEER_FLOW_CONFIG_PATH` environment variable -3. `config.yaml` in current directory (backend/) -4. `config.yaml` in parent directory (project root - **recommended location**) - -Config values starting with `$` are resolved as environment variables (e.g., `$OPENAI_API_KEY`). -`ModelConfig` also declares `use_responses_api` and `output_version` so OpenAI `/v1/responses` can be enabled explicitly while still using `langchain_openai:ChatOpenAI`. - -**Extensions Configuration** (`extensions_config.json`): - -MCP servers and skills are configured together in `extensions_config.json` in project root: - -Configuration priority: -1. Explicit `config_path` argument -2. `DEER_FLOW_EXTENSIONS_CONFIG_PATH` environment variable -3. `extensions_config.json` in current directory (backend/) -4. `extensions_config.json` in parent directory (project root - **recommended location**) - -### Gateway API (`app/gateway/`) - -FastAPI application on port 8001 with health check at `GET /health`. Set `GATEWAY_ENABLE_DOCS=false` to disable `/docs`, `/redoc`, and `/openapi.json` in production (default: enabled). - -**Routers**: - -| Router | Endpoints | -|--------|-----------| -| **Models** (`/api/models`) | `GET /` - list models; `GET /{name}` - model details | -| **MCP** (`/api/mcp`) | `GET /config` - get config; `PUT /config` - update config (saves to extensions_config.json) | -| **Skills** (`/api/skills`) | `GET /` - list skills; `GET /{name}` - details; `PUT /{name}` - update enabled; `POST /install` - install from .skill archive (accepts standard optional frontmatter like `version`, `author`, `compatibility`) | -| **Memory V2** (`/api/memory/v2`) | `GET /` - builtin data (USER.md/MEMORY.md); `POST/PUT/DELETE /entries` - add/replace/remove entry; `GET/PATCH/DELETE /config` - effective config + per-user overrides; `POST /search` - Hindsight recall/reflect proxy | -| **Agents** (`/api/agents`) | `GET /` - list built-in, owned, and published agents; `POST /` - create user-owned agent; `GET/PUT/DELETE /{id}` - read/update/delete by stable agent id; `GET /check` - validate non-empty display name (duplicates allowed) | -| **Skills** (`/api/skills`) | `GET /` - list skills (optional `search` fuzzy-name + `tag_id` filters; each item carries `tags`); `GET /{name}` - details; `PUT /{name}` - update enabled; `POST /install` - install from .skill archive (accepts standard optional frontmatter like `version`, `author`, `compatibility`) | -| **Skills** (`/api/skills`) | `GET /` - list skills (optional `search` fuzzy-name + `tag_id` filters; each item carries `tags` + owner-authored `detail`); `GET /{name}` - details; `PUT /{name}` - update enabled; `PUT /{name}/detail` - set the long-form `detail` text (custom skill: owner or admin; built-in/public: admin only ? materializes a `skills` row on demand); `POST /install` - install from .skill archive (accepts standard optional frontmatter like `version`, `author`, `compatibility`) | -| **Skills** (`/api/skills`) | `GET /` - list skills (optional `search` fuzzy-name + `tag_id` filters; each item carries `tags` + owner-authored `detail`); `GET /{name}` - details; `PUT /{name}` - update enabled; `PUT /{name}/detail` - set the long-form `detail` text (custom skill: owner or admin; built-in/public: admin only ? materializes a `skills` row on demand); `GET /{name}/content` - raw SKILL.md source, restricted to an admin or the publisher (custom-skill owner); other users get 403 and see only description + detail; `POST /install` - install from .skill archive (accepts standard optional frontmatter like `version`, `author`, `compatibility`) | -| **Skills** (`/api/skills`) | `GET /` - list skills (optional `search` fuzzy-name + repeatable `tag_ids` multi-select filter, OR-matched; each item carries `tags` + owner-authored `detail`); `GET /{name}` - details; `PUT /{name}` - update enabled; `PUT /{name}/detail` - set the long-form `detail` text (custom skill: owner or admin; built-in/public: admin only ? materializes a `skills` row on demand); `GET /{name}/content` - raw SKILL.md source, restricted to an admin or the publisher (custom-skill owner); other users get 403 and see only description + detail; `POST /install` - install from .skill archive (accepts standard optional frontmatter like `version`, `author`, `compatibility`) | -| **Memory** (`/api/memory`) | `GET /` - memory data; `POST /reload` - force reload; `GET /config` - config; `GET /status` - config + data | -| **Agents** (`/api/agents`) | `GET /` - list built-in, owned, and published agents (optional `search` + repeatable `tag_ids` multi-select filter, OR-matched; each item carries `tags` and `featured_order`); `POST /` - create user-owned agent; `GET/PUT/DELETE /{id}` - read/update/delete by stable agent id; `GET /check` - validate non-empty display name (duplicates allowed); `GET /selector` - homepage agent picker (single admin-curated list, visibility-filtered, sorted by `featured_order` asc); `PUT /{id}/featured` - **admin** sets/clears the homepage `featured_order` (must be `published=true` to feature; passing `order: null` clears unconditionally); `PUT /featured/order` - **admin** bulk-reorders the homepage list (body `{agent_ids: [...]}` ? indices become 0..N-1) | -| **Tags** (`/api/tags`) | `GET /` - list tags (optional `tag_type` filter; open to all); `POST /` - create tag (admin); `PUT /{id}` / `DELETE /{id}` - update/delete tag (admin); `GET /assignments/{target_type}/{target_id}` - tags on a resource; `PUT /assignments/{target_type}/{target_id}` - replace a resource's tags (admin). Tags are typed `agent`/`skill`/`scheduled_task` and may only tag resources of their own type. | -| **Uploads** (`/api/threads/{id}/uploads`) | `POST /` - upload files (auto-converts PDF/PPT/Excel/Word); `GET /list` - list; `DELETE /{filename}` - delete | -| **Threads** (`/api/threads/{id}`) | `DELETE /` - remove DeerFlow-managed local thread data after LangGraph thread deletion; unexpected failures are logged server-side and return a generic 500 detail | -| **Threads search** (`POST /threads/search`, LangGraph-compat) | Conversation listing. **System threads are excluded by default**: rows whose `metadata.system` is truthy (scheduler runs, roundtable workers) never crowd the user's list; a caller whose `metadata` filter explicitly references `thread_type`/`system` (e.g. studio notebooks) opts out of the exclusion. New `query` field = case-insensitive title (`display_name`) substring search, used by the chats page's server-side pagination. `POST /threads/count` takes the same request body (limit/offset ignored) and returns `{total}` under identical filtering/exclusion semantics ? powers the chats page's ??/??? display without fetching rows (`ThreadMetaStore.count`). `ThreadMetaStore.search` gained `exclude_system`/`query` params; JSON-blob filters batch-scan until the requested `limit`/`offset` window is full (so pagination over a heavily-filtered set never under-fills), with a `thread_id` tie-breaker on the `updated_at DESC` ordering for stable OFFSET paging. Tests: `tests/test_thread_meta_search.py` | -| **Artifacts** (`/api/threads/{id}/artifacts`) | `GET /{path}` - serve artifacts; active content types (`text/html`, `application/xhtml+xml`, `image/svg+xml`) are always forced as download attachments to reduce XSS risk; `?download=true` still forces download for other file types | -| **Suggestions** (`/api/threads/{id}/suggestions`) | `POST /` - generate follow-up questions; rich list/block model content is normalized before JSON parsing | -| **Thread Runs** (`/api/threads/{id}/runs`) | `POST /` - create background run; `POST /stream` - create + SSE stream; `POST /wait` - create + block; `GET /` - list runs; `GET /{rid}` - run details; `POST /{rid}/cancel` - cancel; `GET /{rid}/join` - join SSE; `GET /{rid}/messages` - paginated messages `{data, has_more}`; `GET /{rid}/events` - full event stream; `GET /../messages` - thread messages with feedback; `GET /../token-usage` - aggregate tokens | -| **Feedback** (`/api/threads/{id}/runs/{rid}/feedback`) | `PUT /` - upsert feedback; `DELETE /` - delete user feedback; `POST /` - create feedback; `GET /` - list feedback; `GET /stats` - aggregate stats; `DELETE /{fid}` - delete specific | -| **Runs** (`/api/runs`) | `POST /stream` - stateless run + SSE; `POST /wait` - stateless run + block; `GET /{rid}/messages` - paginated messages by run_id `{data, has_more}` (cursor: `after_seq`/`before_seq`); `GET /{rid}/feedback` - list feedback by run_id | -| **Thread Shares** (`/api/thread-shares`) | Per-conversation sharing with two modes (`thread_shares.mode`). **`import`**: `POST /` `{thread_id, mode}` - create (or reuse) a revocable share code; `GET /{code}/preview` + `POST /{code}/import` - copy that conversation into the caller's account (fresh `thread_id` via `app/gateway/thread_copy.py` + new `threads_meta` row + `user-data` copy; dedup on `metadata.shared_origin`). **`view`**: a public, read-only static page ? see Public Shares below. Both modes: `GET /` - list my codes; `DELETE /{code}` - revoke. | -| **Public Shares** (`/api/public/shares`) | **Unauthenticated** (the `/api/public/` prefix is whitelisted in `auth_middleware._PUBLIC_PATH_PREFIXES`). `GET /{code}` - read-only conversation snapshot (title + serialized messages + artifacts) for a `view`-mode share; messages are read straight from the latest checkpoint. `GET /{code}/files/{path}` - serves a file referenced by that conversation (images, Markdown, text, PDF, ?) so the shared page's links are openable; path resolution is confined to the owner's thread directory. Active content (`text/html`, `application/xhtml+xml`, `image/svg+xml`) is forced to download (`_public_share_disposition`); everything else is served inline. Missing/revoked/non-`view` codes all return 404. | -| **Roundtable Drafts** (`/api/roundtable-drafts`) | Per-user persistence for the roundtable-planning page (replaces the old browser `localStorage`). `GET /` - list drafts (lightweight meta); `POST /` - create; `GET/PUT/DELETE /{id}` - full read / partial update / delete. A draft = one full session (Step 1 intent + Step 2 roundtable), step snapshots stored as JSON in `PortableLongText`. Recommendation history bound to a draft: `GET /{id}/recommendations` - list newest-first; `POST /{id}/recommendations` - append one run (objective/status/rationale/picks/candidates). Backed by `deerflow.persistence.roundtable_drafts` (tables `roundtable_drafts`, `roundtable_recommend_history`); store wired in `deps.py` as `app.state.roundtable_draft_store`. | -| **Roundtable Draft Shares** (`/api/roundtable-draft-shares`) | Per-draft sharing for the roundtable-planning page, mirroring Thread Shares but the shared resource is a `roundtable_drafts` row. Two modes (`roundtable_draft_shares.mode`). **`import`**: `POST /` `{draft_id, mode}` - create (or reuse) a revocable code; `GET /{code}/preview` + `POST /{code}/import` - copy that draft (step1/2/3) into the caller's account under a deterministic per-(code,recipient) id (`imp_` ? re-import is a no-op). **`view`**: a public, read-only result page ? see Public Roundtable Shares. Both: `GET /` - list my codes; `DELETE /{code}` - revoke. Backed by `deerflow.persistence.roundtable_draft_shares` (table `roundtable_draft_shares`, migration `20260606_01`); store wired in `deps.py` as `app.state.roundtable_draft_share_store`. Tests: `tests/test_roundtable_draft_shares.py`. | -| **Public Roundtable Shares** (`/api/public/roundtable-shares`) | **Unauthenticated** (`/api/public/` whitelisted). `GET /{code}` - read-only snapshot for a `view`-mode draft share: returns the Step 3??????report HTML (`step3.html`, a self-contained document) + title/summary/`has_report`. The frontend renders it in a sandboxed `iframe` (no `allow-scripts`) at `#/roundtable-share/`. Missing/revoked/non-`view` codes all return 404. | -| **Scheduled Tasks** (`/api/scheduled-tasks`) | Per-user cron-like agent tasks. `GET/POST /` - list/create owned tasks; `PATCH/DELETE /{id}` - update/delete (owner); `POST /{id}/{pause,resume,trigger}`; `GET /{id}/runs`, `GET /runs/{rid}`, run-file preview/edit/download; `GET /runs/{rid}/messages` - agent messages for the run's underlying agent run (used to inspect a stuck/running run; authorized via `_load_run`); `GET /runs/{rid}/html` - generated HTML document for an `html_page` run; `DELETE /runs/{rid}` - owner-scoped, with an admin fallback so admins can delete any user's run; publish/subscribe under `/{id}/{publish,unpublish,subscribe}` and `GET /published`. **Admin (cross-user):** `GET /admin/all` - every task; `PATCH/DELETE /admin/{id}` - edit/delete any task; `POST /admin/{id}/trigger`; `GET /admin/{id}/runs`. Admin routes require `system_role == "admin"`. | -| **HTML Page Favorites** (`/api/html-page-favorites`) | Bookmarked generated HTML pages, owner-scoped. `GET /` - list; `POST /` `{page_id, title?, description?, tags?}` - bookmark a `scheduled_html_pages` row (extracts a reusable style/layout summary via GLM5 at favorite time); a page can only be favorited once per user (`DuplicateFavoriteError` ? HTTP 409). `GET/DELETE /{id}` - read/delete (the create-task reference picker deletes favorites here). A favorite can be selected as a style reference when creating a new `html_page` task. | -| **Public Scheduled Tasks** (`/api/public/scheduled-tasks`) | **Unauthenticated** mirror for embedding. Adds `GET /runs/{rid}/html` and `GET /{task_id}/latest-html` for the `html_page` preview; the run result keeps `result.html` (only file paths are redacted). | -| **Light Apps** (`/api/light-apps`) | Light-app registry (????? / ????), **global/shared** (no user scoping; `created_by`/`updated_by` audit-only). Two kinds: `window_open` (launched in a new tab from the ???? card grid) and `iframe` (embedded under a parent sidebar menu via `mount_location` and dynamically injected into the sidebar). Both carry `route_params` ? a list of `{key, paramType: login\|theme, source, customValue, themeValue}` descriptors the **frontend** resolves at launch time (login params read from `localStorage.userInfo`; theme params use a literal value) and appends to `url` as query string. `GET /` - list (filters `app_type`/`mount_location`/`enabled_only`/`search`); `GET /{app_id}` - read one (both open to any authenticated user); `POST /`, `PUT /{app_id}`, `DELETE /{app_id}` - **admin only**; iframe apps must carry a `mount_location` (400 otherwise); the unique `name` collides as HTTP 409. Backed by `deerflow.persistence.light_apps` (table `light_apps`, auto-created by `create_all`); store wired in `deps.py` as `app.state.light_app_store`. Tests: `tests/test_light_apps.py`. | -| **Knowledge** (`/api/knowledge`) | Product knowledge base, **global/shared** (no user isolation; writes record `created_by`/`updated_by` for audit). `POST /ingest/thread/{thread_id}` - sediment one thread into a note (reads messages from the checkpointer, `threads:read` owner-checked); `POST /ingest/threads/batch` - batch-sediment the caller's recent threads (`skip_existing` via `.manifest.json` dedup). **Optional model selection**: both ingest endpoints accept `model_name` (overrides `knowledge.extract_model_name` for that capture; threaded through `KnowledgeService.capture_thread` ? `extract_thread_llm_multi`). **Model-error surfacing**: when the chosen model produces nothing usable (timeout / API error / non-JSON) capture still **falls back to rule-based extraction and lands a draft** (`llm_fallback:true`); the single-thread (non-background) response and the batch response (`llm_fallback` count) carry this so the UI tells the user "???????". The "??????" chat button stays background/fire-and-forget (passes `model_name`, no per-save error toast); the batch-import dialog is synchronous and shows the fallback count. `POST /ingest/file` - **import an uploaded file** (multipart: `file` + optional `title`/`tags`/`status`/`model_name`) into wiki note(s): the file is converted to Markdown (md/txt read directly, pdf/word/ppt/excel via `convert_file_to_markdown`), distilled by the same wiki-capture LLM pipeline as thread capture (`source_type="file"`), and falls back to a verbatim **draft** note when the model returns nothing usable (`llm_fallback:true`); `POST /ingest/search` - sediment search/tool results as a reference note; `POST /notes` - manual note; `GET /notes` (keyword/tag/source_type/status + pagination); `GET/PUT/DELETE /notes/{id}` (DELETE is soft = archive, `?hard=true` removes the vault file); `POST /search` - `mode=keyword\|vector\|hybrid` (vector?keyword fallback when no embedding endpoint); `GET /notes/{id}/versions` - edit history; `GET /duplicates` + `POST /merge` - dedup; `POST /reindex` - rebuild vector index; `GET /graph` - product graph data (notes + entities, graph-blacklist filtered); `GET/POST /graph/blacklist` + `DELETE /graph/blacklist/{id}` - graph node blacklist (labels = entity names / note titles, case-insensitive match; blacklisted nodes and their edges are dropped from `/graph` and the wiki-export artifacts; table `knowledge_graph_blacklist`, auto-created); `POST /export/graph` + `GET /export/files/{graph.json\|graph.html\|graph.graphml\|cypher.txt}` - obsidian-wiki `wiki-export` artifacts (self-contained `graph.html` for iframe); `POST /resolve` `{targets:[?]}` + `GET /notes/{id}/wikilinks` - resolve `[[wikilink]]` targets to a note id / vault page / missing (powers clickable wikilinks); `GET /vault-page?path=` - read a raw vault meta/hub Markdown page (no DB note). Backed by the `deerflow.knowledge` module (see below). | - -Proxied through nginx: `/api/langgraph/*` ? LangGraph, all other `/api/*` ? Gateway. - -### AI Writing Built-in Functional Agents (????????) - -The AI-writing graph (`packages/harness/deerflow/agents/ai_writing/`) has four stage "brains" ? ?????? / ????? / ?? / ?? ? that can each run in one of two modes, switched per-stage by `config.yaml ? ai_writing.use_builtin_agents.{researcher,outliner,writer,editor}` (default **false**, mtime hot-reloaded, new sessions pick up changes immediately): - -- **Legacy (flag off)**: hard-coded prompts in `prompts/*.py`, direct LLM call (unchanged behavior). -- **Builtin-agent (flag on)**: the node loads the corresponding functional agent from `.deer-flow/agents/ai-writing-/` ? `SOUL.md` becomes the system prompt and `config.yaml`'s `model` overrides the stage model (priority: `agent.model > node explicit fallback (keyword_model/rank_model) > state.model_name`). Admins edit SOUL/model from the agent management page. Runner: `nodes/_agent_runner.py` (`run_writing_agent` / `stream_writing_agent` / `parse_marker_json`). Output contracts (marker + ```json``` blocks, intent.py-style): researcher `[KEYWORDS_READY]` / `[MATERIALS_RANKED]`, outliner `[OUTLINE_READY]`, editor (per-dimension) `[REVIEW_READY]`; writer emits raw section Markdown (keeps per-section streaming) with the legacy `[[SECTION_BLOCKED]]` sentinel in strict mode. **Orchestration stays in the nodes** (search fan-out, per-section loop, 3-dimension review loop, all pause points) ? the agent only does single structured generations, and a missing/empty SOUL silently falls back to the legacy path (`WritingAgentUnavailable`). - -Seeding mirrors the roundtable pattern: verified `config.yaml` + `SOUL.md` ship in `app/gateway/routers/_ai_writing_seed_assets//`; `_ai_writing_seed.py::ensure_ai_writing_functional_agents()` copies them to `.deer-flow/agents/` when missing (never overwrites local edits), called from the startup lifespan (before `_sync_legacy_agents`, so the agents appear in `/api/agents` on first boot) and self-heals from `GET /api/ai-writing/sessions` + `GET /api/ai-writing/article-types`. Tests: `tests/test_ai_writing_seed.py`, `tests/test_ai_writing_agent_runner.py`. - -**Skill-driven retrieval (检索类型).** `material_source` has exactly two values: `general` (通用检索, default) and `notebook` (我的空间); legacy `knowledge_base`/`skill` are normalized to `general` by the researcher node. General retrieval is orchestrated from the **skills configured on the `ai-writing-researcher` agent** (`config.yaml → skills`, editable from the agent management page): the built-in `knowledge-base-search` skill (知识库检索) runs the native keyword fan-out web/intranet search (the old default path), while any other configured skill searches with a `description`-targeted query; skills run in configured order with early-stop at `max_materials`, and an empty/unresolvable skills config falls back to the built-in knowledge search so retrieval always works. The skill itself is seeded from `_ai_writing_seed_assets/skills/knowledge-base-search/SKILL.md` into `skills/public/` (never overwrites), and a pre-existing researcher `config.yaml` lacking the `skills` key is patched in place with the default (explicit `skills: []` is respected). **PAUSE-1 input append flow**: when the user types natural language into the material-confirm input (`re_search`/legacy `enable_skill_search` + `user_query`), the node matches the most relevant configured skill via `SkillPicker(candidates=…)` (skipped when only one skill is configured), searches with the user's words, and **appends** the new materials to the existing package (URL-dedup, original order preserved, no LLM re-rank/truncation, 100-item hard cap). Tests: `tests/test_ai_writing_material_source.py`. - -### Admin Leaderboard Snapshots (`/api/admin/users/leaderboard*`) - -The admin ????? is served from **precomputed per-day snapshots** in `admin_leaderboard_daily_stats` (keyed by `(stat_date, settings_hash)`), not by aggregating raw rows on every request. A background scheduler inside the Gateway (`_leaderboard_snapshot_scheduler_loop` in `app/gateway/app.py`, singleton file-locked) ticks every `ADMIN_LEADERBOARD_BACKGROUND_INTERVAL_SECONDS` (default 300s): it prewarms recent days, then builds any `queued`/`error`/stale-`running` day. `GET /leaderboard` merges the per-day snapshots for the requested range. **Today** is rebuilt whenever its snapshot is older than `ADMIN_LEADERBOARD_TODAY_TTL_SECONDS` (default 43200s = half a day, so the leaderboard refreshes ~twice daily); a past day, once `ready`, is treated as fresh forever. - -Two on-demand/automatic refresh paths layer on top: -- **Manual "????"** ? `POST /leaderboard/refresh-today` force-rebuilds *today's* snapshot **synchronously** (bounded by the same semaphore/timeout the scheduler uses), then returns the merged range. Lets an admin pull the latest real-time records into the stats without waiting for the TTL. Frontend: a "????" button on `AdminUserLeaderboardPage` ? `useRefreshTodayLeaderboard` (`core/admin/hooks.ts`), which primes + invalidates the `["admin-activity","leaderboard"]` query. -- **Daily finalize of the previous day** ? once per day, on/after `ADMIN_LEADERBOARD_FINALIZE_HOUR` (local Beijing, default `1` = ?? 1 ?), the scheduler tick force-recomputes *yesterday* a single time so runs/tool-calls that only landed near midnight are fully captured. The decision is the pure `deerflow.persistence.admin_stats.finalize.should_finalize_previous_day` (status/generated_at/now/hour) ? the rebuild stamps `generated_at` to today, which self-debounces it for the rest of the day (no extra state file). Tests: `tests/test_leaderboard_finalize.py`. - -### Scheduled Task Runtime - -`ScheduledTaskService` (`deerflow/runtime/scheduler/service.py`) polls due tasks every `poll_interval_seconds` and executes each in its own throwaway thread, serialized by a shared `_agent_run_lock`. - -**Stuck-run recovery**: each execution attempt is bounded by `_RUN_ATTEMPT_TIMEOUT_SECONDS` (30 min) via `asyncio.wait_for`. On timeout the run is retried up to `_MAX_RUN_RETRIES` (3) times, then marked `failed`. A watchdog (`_reconcile_stuck_runs`, run every poll) recovers runs left in `running` with no in-process owner ? e.g. orphaned by a crash or restart ? re-driving them through the same retry budget so a task can never stay perpetually "executing". The `scheduled_task_runs.retry_count` column tracks consumed retries. - -**Unique task names**: task names are unique per user (`uq_scheduled_tasks_user_name`). `create_task`/`update_task`/`admin_update_task` proactively check via `_assert_name_available()` and raise `DuplicateTaskNameError` on a collision; the router translates that into HTTP `409` instead of letting the raw DB `IntegrityError` surface as a `500`. - -**HTML page tasks (`task_kind == "html_page"`)**: a second task kind that renders a full HTML document each run instead of a Markdown report. The kind + its config (`html_config`: `page_prompt`, `negative_prompt`, `reference_favorite_id`, `model_name`) live inside `execution_context` ? no schema change to `scheduled_tasks`. Data gathering is delegated to the agent's own search skills (driven by `page_prompt`), so there is no explicit search-query/data-source field; the render model is user-selectable (`model_name`, default `zai-org/GLM-5-FP8`). `_run_task_attempt()` dispatches by kind; `_run_html_page_attempt()` runs the pipeline: gather real data via the lead agent (its search tools) ? optionally load a reference favorite's reusable style/layout ? render with the chosen model (thinking off) ? fault-tolerant JSON parse ? sanitize (BeautifulSoup, strips `
暂无大屏多页
这次会商还没有生成第三步「大屏多页」的方案报告。
这次会商还没有生成第三步「结果绘制」的方案报告。 @@ -206,7 +261,22 @@ export default function PublicRoundtableSharePage({ )}
`. Missing/revoked/non-`view` codes all return 404. | -| **Scheduled Tasks** (`/api/scheduled-tasks`) | Per-user cron-like agent tasks. `GET/POST /` - list/create owned tasks; `PATCH/DELETE /{id}` - update/delete (owner); `POST /{id}/{pause,resume,trigger}`; `GET /{id}/runs`, `GET /runs/{rid}`, run-file preview/edit/download; `GET /runs/{rid}/messages` - agent messages for the run's underlying agent run (used to inspect a stuck/running run; authorized via `_load_run`); `GET /runs/{rid}/html` - generated HTML document for an `html_page` run; `DELETE /runs/{rid}` - owner-scoped, with an admin fallback so admins can delete any user's run; publish/subscribe under `/{id}/{publish,unpublish,subscribe}` and `GET /published`. **Admin (cross-user):** `GET /admin/all` - every task; `PATCH/DELETE /admin/{id}` - edit/delete any task; `POST /admin/{id}/trigger`; `GET /admin/{id}/runs`. Admin routes require `system_role == "admin"`. | -| **HTML Page Favorites** (`/api/html-page-favorites`) | Bookmarked generated HTML pages, owner-scoped. `GET /` - list; `POST /` `{page_id, title?, description?, tags?}` - bookmark a `scheduled_html_pages` row (extracts a reusable style/layout summary via GLM5 at favorite time); a page can only be favorited once per user (`DuplicateFavoriteError` ? HTTP 409). `GET/DELETE /{id}` - read/delete (the create-task reference picker deletes favorites here). A favorite can be selected as a style reference when creating a new `html_page` task. | -| **Public Scheduled Tasks** (`/api/public/scheduled-tasks`) | **Unauthenticated** mirror for embedding. Adds `GET /runs/{rid}/html` and `GET /{task_id}/latest-html` for the `html_page` preview; the run result keeps `result.html` (only file paths are redacted). | -| **Light Apps** (`/api/light-apps`) | Light-app registry (????? / ????), **global/shared** (no user scoping; `created_by`/`updated_by` audit-only). Two kinds: `window_open` (launched in a new tab from the ???? card grid) and `iframe` (embedded under a parent sidebar menu via `mount_location` and dynamically injected into the sidebar). Both carry `route_params` ? a list of `{key, paramType: login\|theme, source, customValue, themeValue}` descriptors the **frontend** resolves at launch time (login params read from `localStorage.userInfo`; theme params use a literal value) and appends to `url` as query string. `GET /` - list (filters `app_type`/`mount_location`/`enabled_only`/`search`); `GET /{app_id}` - read one (both open to any authenticated user); `POST /`, `PUT /{app_id}`, `DELETE /{app_id}` - **admin only**; iframe apps must carry a `mount_location` (400 otherwise); the unique `name` collides as HTTP 409. Backed by `deerflow.persistence.light_apps` (table `light_apps`, auto-created by `create_all`); store wired in `deps.py` as `app.state.light_app_store`. Tests: `tests/test_light_apps.py`. | -| **Knowledge** (`/api/knowledge`) | Product knowledge base, **global/shared** (no user isolation; writes record `created_by`/`updated_by` for audit). `POST /ingest/thread/{thread_id}` - sediment one thread into a note (reads messages from the checkpointer, `threads:read` owner-checked); `POST /ingest/threads/batch` - batch-sediment the caller's recent threads (`skip_existing` via `.manifest.json` dedup). **Optional model selection**: both ingest endpoints accept `model_name` (overrides `knowledge.extract_model_name` for that capture; threaded through `KnowledgeService.capture_thread` ? `extract_thread_llm_multi`). **Model-error surfacing**: when the chosen model produces nothing usable (timeout / API error / non-JSON) capture still **falls back to rule-based extraction and lands a draft** (`llm_fallback:true`); the single-thread (non-background) response and the batch response (`llm_fallback` count) carry this so the UI tells the user "???????". The "??????" chat button stays background/fire-and-forget (passes `model_name`, no per-save error toast); the batch-import dialog is synchronous and shows the fallback count. `POST /ingest/file` - **import an uploaded file** (multipart: `file` + optional `title`/`tags`/`status`/`model_name`) into wiki note(s): the file is converted to Markdown (md/txt read directly, pdf/word/ppt/excel via `convert_file_to_markdown`), distilled by the same wiki-capture LLM pipeline as thread capture (`source_type="file"`), and falls back to a verbatim **draft** note when the model returns nothing usable (`llm_fallback:true`); `POST /ingest/search` - sediment search/tool results as a reference note; `POST /notes` - manual note; `GET /notes` (keyword/tag/source_type/status + pagination); `GET/PUT/DELETE /notes/{id}` (DELETE is soft = archive, `?hard=true` removes the vault file); `POST /search` - `mode=keyword\|vector\|hybrid` (vector?keyword fallback when no embedding endpoint); `GET /notes/{id}/versions` - edit history; `GET /duplicates` + `POST /merge` - dedup; `POST /reindex` - rebuild vector index; `GET /graph` - product graph data (notes + entities, graph-blacklist filtered); `GET/POST /graph/blacklist` + `DELETE /graph/blacklist/{id}` - graph node blacklist (labels = entity names / note titles, case-insensitive match; blacklisted nodes and their edges are dropped from `/graph` and the wiki-export artifacts; table `knowledge_graph_blacklist`, auto-created); `POST /export/graph` + `GET /export/files/{graph.json\|graph.html\|graph.graphml\|cypher.txt}` - obsidian-wiki `wiki-export` artifacts (self-contained `graph.html` for iframe); `POST /resolve` `{targets:[?]}` + `GET /notes/{id}/wikilinks` - resolve `[[wikilink]]` targets to a note id / vault page / missing (powers clickable wikilinks); `GET /vault-page?path=` - read a raw vault meta/hub Markdown page (no DB note). Backed by the `deerflow.knowledge` module (see below). | - -Proxied through nginx: `/api/langgraph/*` ? LangGraph, all other `/api/*` ? Gateway. - -### AI Writing Built-in Functional Agents (????????) - -The AI-writing graph (`packages/harness/deerflow/agents/ai_writing/`) has four stage "brains" ? ?????? / ????? / ?? / ?? ? that can each run in one of two modes, switched per-stage by `config.yaml ? ai_writing.use_builtin_agents.{researcher,outliner,writer,editor}` (default **false**, mtime hot-reloaded, new sessions pick up changes immediately): - -- **Legacy (flag off)**: hard-coded prompts in `prompts/*.py`, direct LLM call (unchanged behavior). -- **Builtin-agent (flag on)**: the node loads the corresponding functional agent from `.deer-flow/agents/ai-writing-/` ? `SOUL.md` becomes the system prompt and `config.yaml`'s `model` overrides the stage model (priority: `agent.model > node explicit fallback (keyword_model/rank_model) > state.model_name`). Admins edit SOUL/model from the agent management page. Runner: `nodes/_agent_runner.py` (`run_writing_agent` / `stream_writing_agent` / `parse_marker_json`). Output contracts (marker + ```json``` blocks, intent.py-style): researcher `[KEYWORDS_READY]` / `[MATERIALS_RANKED]`, outliner `[OUTLINE_READY]`, editor (per-dimension) `[REVIEW_READY]`; writer emits raw section Markdown (keeps per-section streaming) with the legacy `[[SECTION_BLOCKED]]` sentinel in strict mode. **Orchestration stays in the nodes** (search fan-out, per-section loop, 3-dimension review loop, all pause points) ? the agent only does single structured generations, and a missing/empty SOUL silently falls back to the legacy path (`WritingAgentUnavailable`). - -Seeding mirrors the roundtable pattern: verified `config.yaml` + `SOUL.md` ship in `app/gateway/routers/_ai_writing_seed_assets//`; `_ai_writing_seed.py::ensure_ai_writing_functional_agents()` copies them to `.deer-flow/agents/` when missing (never overwrites local edits), called from the startup lifespan (before `_sync_legacy_agents`, so the agents appear in `/api/agents` on first boot) and self-heals from `GET /api/ai-writing/sessions` + `GET /api/ai-writing/article-types`. Tests: `tests/test_ai_writing_seed.py`, `tests/test_ai_writing_agent_runner.py`. - -**Skill-driven retrieval (检索类型).** `material_source` has exactly two values: `general` (通用检索, default) and `notebook` (我的空间); legacy `knowledge_base`/`skill` are normalized to `general` by the researcher node. General retrieval is orchestrated from the **skills configured on the `ai-writing-researcher` agent** (`config.yaml → skills`, editable from the agent management page): the built-in `knowledge-base-search` skill (知识库检索) runs the native keyword fan-out web/intranet search (the old default path), while any other configured skill searches with a `description`-targeted query; skills run in configured order with early-stop at `max_materials`, and an empty/unresolvable skills config falls back to the built-in knowledge search so retrieval always works. The skill itself is seeded from `_ai_writing_seed_assets/skills/knowledge-base-search/SKILL.md` into `skills/public/` (never overwrites), and a pre-existing researcher `config.yaml` lacking the `skills` key is patched in place with the default (explicit `skills: []` is respected). **PAUSE-1 input append flow**: when the user types natural language into the material-confirm input (`re_search`/legacy `enable_skill_search` + `user_query`), the node matches the most relevant configured skill via `SkillPicker(candidates=…)` (skipped when only one skill is configured), searches with the user's words, and **appends** the new materials to the existing package (URL-dedup, original order preserved, no LLM re-rank/truncation, 100-item hard cap). Tests: `tests/test_ai_writing_material_source.py`. - -### Admin Leaderboard Snapshots (`/api/admin/users/leaderboard*`) - -The admin ????? is served from **precomputed per-day snapshots** in `admin_leaderboard_daily_stats` (keyed by `(stat_date, settings_hash)`), not by aggregating raw rows on every request. A background scheduler inside the Gateway (`_leaderboard_snapshot_scheduler_loop` in `app/gateway/app.py`, singleton file-locked) ticks every `ADMIN_LEADERBOARD_BACKGROUND_INTERVAL_SECONDS` (default 300s): it prewarms recent days, then builds any `queued`/`error`/stale-`running` day. `GET /leaderboard` merges the per-day snapshots for the requested range. **Today** is rebuilt whenever its snapshot is older than `ADMIN_LEADERBOARD_TODAY_TTL_SECONDS` (default 43200s = half a day, so the leaderboard refreshes ~twice daily); a past day, once `ready`, is treated as fresh forever. - -Two on-demand/automatic refresh paths layer on top: -- **Manual "????"** ? `POST /leaderboard/refresh-today` force-rebuilds *today's* snapshot **synchronously** (bounded by the same semaphore/timeout the scheduler uses), then returns the merged range. Lets an admin pull the latest real-time records into the stats without waiting for the TTL. Frontend: a "????" button on `AdminUserLeaderboardPage` ? `useRefreshTodayLeaderboard` (`core/admin/hooks.ts`), which primes + invalidates the `["admin-activity","leaderboard"]` query. -- **Daily finalize of the previous day** ? once per day, on/after `ADMIN_LEADERBOARD_FINALIZE_HOUR` (local Beijing, default `1` = ?? 1 ?), the scheduler tick force-recomputes *yesterday* a single time so runs/tool-calls that only landed near midnight are fully captured. The decision is the pure `deerflow.persistence.admin_stats.finalize.should_finalize_previous_day` (status/generated_at/now/hour) ? the rebuild stamps `generated_at` to today, which self-debounces it for the rest of the day (no extra state file). Tests: `tests/test_leaderboard_finalize.py`. - -### Scheduled Task Runtime - -`ScheduledTaskService` (`deerflow/runtime/scheduler/service.py`) polls due tasks every `poll_interval_seconds` and executes each in its own throwaway thread, serialized by a shared `_agent_run_lock`. - -**Stuck-run recovery**: each execution attempt is bounded by `_RUN_ATTEMPT_TIMEOUT_SECONDS` (30 min) via `asyncio.wait_for`. On timeout the run is retried up to `_MAX_RUN_RETRIES` (3) times, then marked `failed`. A watchdog (`_reconcile_stuck_runs`, run every poll) recovers runs left in `running` with no in-process owner ? e.g. orphaned by a crash or restart ? re-driving them through the same retry budget so a task can never stay perpetually "executing". The `scheduled_task_runs.retry_count` column tracks consumed retries. - -**Unique task names**: task names are unique per user (`uq_scheduled_tasks_user_name`). `create_task`/`update_task`/`admin_update_task` proactively check via `_assert_name_available()` and raise `DuplicateTaskNameError` on a collision; the router translates that into HTTP `409` instead of letting the raw DB `IntegrityError` surface as a `500`. - -**HTML page tasks (`task_kind == "html_page"`)**: a second task kind that renders a full HTML document each run instead of a Markdown report. The kind + its config (`html_config`: `page_prompt`, `negative_prompt`, `reference_favorite_id`, `model_name`) live inside `execution_context` ? no schema change to `scheduled_tasks`. Data gathering is delegated to the agent's own search skills (driven by `page_prompt`), so there is no explicit search-query/data-source field; the render model is user-selectable (`model_name`, default `zai-org/GLM-5-FP8`). `_run_task_attempt()` dispatches by kind; `_run_html_page_attempt()` runs the pipeline: gather real data via the lead agent (its search tools) ? optionally load a reference favorite's reusable style/layout ? render with the chosen model (thinking off) ? fault-tolerant JSON parse ? sanitize (BeautifulSoup, strips `