From 5df5e09c63023411661f4ceb2bfc061e998b31b4 Mon Sep 17 00:00:00 2001 From: ddcat666 <26564566+ddcat666@users.noreply.github.com> Date: Sun, 27 Sep 2026 08:21:08 +0800 Subject: [PATCH] =?UTF-8?q?feat(web):=20=E5=B7=A5=E4=BD=9C=E5=8F=B0=20-=20?= =?UTF-8?q?=E4=BC=98=E5=8C=96=20Agent=20=E5=B7=A5=E4=BD=9C=E5=8C=BA?= =?UTF-8?q?=E4=B8=8E=E6=8A=80=E8=83=BD=E5=BA=93?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- web/src/components/ai/ai-message-markdown.tsx | 6 +- .../canvas-agent-skill-library-modal.tsx | 139 ++++--- .../canvas/canvas-cloud-agent-chat-ui.tsx | 198 +++++++++- .../canvas/canvas-cloud-agent-panel.tsx | 67 +++- .../components/canvas/canvas-cloud-agent.css | 311 ++++++++++++++-- .../director/canvas-director-workbench.tsx | 136 ++++++- .../canvas/director/director-viewport.tsx | 42 ++- web/src/lib/canvas/agent-tool-presentation.ts | 1 + web/src/pages/canvas/canvas-sync-status.css | 123 +++++++ web/src/pages/canvas/canvas-sync-status.tsx | 157 ++++++-- web/src/pages/canvas/project.tsx | 12 +- web/src/pages/skills/index.tsx | 348 +++++++++++++++--- web/src/services/api/request.ts | 4 +- web/src/services/api/skills.ts | 46 ++- web/src/services/user-data-sync.ts | 73 ++++ web/src/styles/unified-buttons.css | 7 - web/src/styles/workspace-product.css | 75 +++- web/test/agent-chat-node-links.test.tsx | 65 ++++ web/test/agent-operation-feed-view.test.tsx | 22 +- web/test/agent-tool-presentation.test.ts | 1 + web/test/api-request.test.ts | 13 + web/test/canvas-remote-revision.test.ts | 31 +- 22 files changed, 1681 insertions(+), 196 deletions(-) create mode 100644 web/src/pages/canvas/canvas-sync-status.css create mode 100644 web/test/agent-chat-node-links.test.tsx diff --git a/web/src/components/ai/ai-message-markdown.tsx b/web/src/components/ai/ai-message-markdown.tsx index 9e749c6bc..7e6f6bbc2 100644 --- a/web/src/components/ai/ai-message-markdown.tsx +++ b/web/src/components/ai/ai-message-markdown.tsx @@ -9,6 +9,8 @@ type AIMessageMarkdownProps = { isStreaming?: boolean; streamingAnimation?: "word" | "char" | "none"; className?: string; + /** 仅覆盖当前调用方需要定制的 Markdown 渲染器,默认渲染行为保持不变。 */ + components?: Components; }; function buildComponents(isStreaming: boolean): Components { @@ -37,7 +39,7 @@ function buildComponents(isStreaming: boolean): Components { const staticComponents = buildComponents(false); const streamingComponents = buildComponents(true); -export const AIMessageMarkdown = memo(function AIMessageMarkdown({ children, isStreaming = false, streamingAnimation = "word", className = "" }: AIMessageMarkdownProps) { +export const AIMessageMarkdown = memo(function AIMessageMarkdown({ children, isStreaming = false, streamingAnimation = "word", className = "", components }: AIMessageMarkdownProps) { if (!children.trim()) return null; return ( {children} diff --git a/web/src/components/canvas/canvas-agent-skill-library-modal.tsx b/web/src/components/canvas/canvas-agent-skill-library-modal.tsx index a2430222a..40777b05d 100644 --- a/web/src/components/canvas/canvas-agent-skill-library-modal.tsx +++ b/web/src/components/canvas/canvas-agent-skill-library-modal.tsx @@ -4,7 +4,7 @@ import { Check, LoaderCircle, Plus, Search, Sparkles, Users } from "lucide-react import { AppModal } from "@/components/ui/product/app-modal"; import type { CanvasTheme } from "@/lib/canvas-theme"; -import type { Skill, SkillCategory } from "@/services/api/skills"; +import type { Skill, SkillCategory, SkillLibraryCategory } from "@/services/api/skills"; type SkillLibraryTab = "enabled" | "installed" | "market"; @@ -15,6 +15,7 @@ type CanvasAgentSkillLibraryModalProps = { marketSkills: Skill[]; selectedSkillIds: string[]; categories: SkillCategory[]; + libraryCategories: SkillLibraryCategory[]; category: string; search: string; loading: boolean; @@ -34,6 +35,7 @@ export function CanvasAgentSkillLibraryModal({ marketSkills, selectedSkillIds, categories, + libraryCategories, category, search, loading, @@ -46,36 +48,53 @@ export function CanvasAgentSkillLibraryModal({ onLoadMore, }: CanvasAgentSkillLibraryModalProps) { const [tab, setTab] = useState("market"); + const [libraryCategoryId, setLibraryCategoryId] = useState("all"); const listRef = useRef(null); const loadMoreRef = useRef(null); const wasOpenRef = useRef(false); const selectedCount = selectedSkillIds.length; useEffect(() => { - if (open && !wasOpenRef.current) setTab("market"); + if (open && !wasOpenRef.current) { + setTab("market"); + setLibraryCategoryId("all"); + onCategoryChange("all"); + } wasOpenRef.current = open; - }, [open]); + }, [onCategoryChange, open]); useEffect(() => { listRef.current?.scrollTo({ top: 0 }); - }, [category, search, tab]); + }, [category, libraryCategoryId, search, tab]); + + const librarySource = useMemo(() => tab === "enabled" + ? installedSkills.filter((skill) => selectedSkillIds.includes(skill.skillId)) + : installedSkills, [installedSkills, selectedSkillIds, tab]); + + const libraryCategoryCounts = useMemo(() => { + const counts = new Map(); + librarySource.forEach((skill) => { + const categoryId = skill.libraryCategoryId || "__uncategorized__"; + counts.set(categoryId, (counts.get(categoryId) || 0) + 1); + }); + return counts; + }, [librarySource]); const visibleSkills = useMemo(() => { const keyword = search.trim().toLocaleLowerCase("zh-CN"); - const source = tab === "enabled" - ? installedSkills.filter((skill) => selectedSkillIds.includes(skill.skillId)) - : tab === "installed" - ? installedSkills - : marketSkills; - + const source = tab === "market" ? marketSkills : librarySource; return source.filter((skill) => { - if (category !== "all" && skill.tag !== category) return false; + if (tab === "market" && category !== "all" && skill.tag !== category) return false; + if (tab !== "market") { + if (libraryCategoryId === "__uncategorized__" && skill.libraryCategoryId) return false; + if (libraryCategoryId !== "all" && libraryCategoryId !== "__uncategorized__" && skill.libraryCategoryId !== libraryCategoryId) return false; + } if (!keyword) return true; - return `${skill.skillName} ${skill.description || ""} ${skill.effectiveUser?.name || ""}` + return `${skill.skillName} ${skill.description || ""} ${skill.instruction || ""} ${skill.extraInfo || ""} ${skill.effectiveUser?.name || ""} ${categories.find((item) => item.value === skill.tag)?.label || ""}` .toLocaleLowerCase("zh-CN") .includes(keyword); }); - }, [category, installedSkills, marketSkills, search, selectedSkillIds, tab]); + }, [categories, category, libraryCategoryId, librarySource, marketSkills, search, tab]); useEffect(() => { const target = loadMoreRef.current; @@ -95,14 +114,32 @@ export function CanvasAgentSkillLibraryModal({ return [...unique.values()]; }, [categories]); + const marketplaceCategoryTotal = useMemo( + () => categoryItems.reduce((total, item) => total + (item.count || 0), 0), + [categoryItems], + ); + const emptyText = tab === "enabled" - ? "本轮还没有启用技能,可在“我的技能”中选择" + ? selectedCount === 0 ? "本轮还没有启用技能,可在“我的技能”中选择" : "没有匹配的已启用技能" : tab === "installed" ? "还没有匹配的已加入技能,可前往“全部”添加" : "没有匹配的公开技能,换个关键词或分类试试"; const changeTab = (value: SkillLibraryTab) => { setTab(value); + setLibraryCategoryId("all"); + onCategoryChange("all"); + }; + + const selectMarketplaceCategory = (value: string) => { + setTab("market"); + setLibraryCategoryId("all"); + onCategoryChange(value); + }; + + const selectLibraryCategory = (value: string) => { + setTab((current) => current === "enabled" ? "enabled" : "installed"); + setLibraryCategoryId(value); onCategoryChange("all"); }; @@ -144,20 +181,34 @@ export function CanvasAgentSkillLibraryModal({ className="canvas-agent-skill-library-search" /> @@ -199,7 +250,7 @@ export function CanvasAgentSkillLibraryModal({