From 44c0f6189ba04054f7292c58a4ccef1c590d34e4 Mon Sep 17 00:00:00 2001 From: Administrator Date: Wed, 10 Jun 2026 08:36:47 +0800 Subject: [PATCH] =?UTF-8?q?wip:=20=E5=90=88=E5=B9=B6=20master=20=E5=89=8D?= =?UTF-8?q?=E6=9A=82=E5=AD=98=E6=9C=AC=E5=9C=B0=E6=94=B9=E5=8A=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.8 (1M context) --- .../workspace/agents/agent-card.tsx | 76 ++++++- .../workspace/agents/agent-gallery.tsx | 41 +++- .../settings/skill-settings-page.tsx | 68 +++++- .../workspace/skills/skill-detail-dialog.tsx | 173 ++++++++------- frontend-web/src/core/agents/api.ts | 118 +++++----- frontend-web/src/core/agents/hooks.ts | 209 ++++++++++-------- frontend-web/src/pages/ScheduledTasksPage.tsx | 69 +++++- offline-backend-20260512/backend/CLAUDE.md | 4 +- .../backend/app/gateway/routers/agents.py | 9 + .../backend/app/gateway/routers/skills.py | 5 +- .../deerflow/persistence/agents/base.py | 87 ++++---- .../deerflow/persistence/agents/memory.py | 121 +++++----- .../deerflow/persistence/agents/sql.py | 176 ++++++++------- 13 files changed, 712 insertions(+), 444 deletions(-) diff --git a/frontend-web/src/components/workspace/agents/agent-card.tsx b/frontend-web/src/components/workspace/agents/agent-card.tsx index 3dd8052f..f47b010d 100644 --- a/frontend-web/src/components/workspace/agents/agent-card.tsx +++ b/frontend-web/src/components/workspace/agents/agent-card.tsx @@ -8,6 +8,7 @@ import { LockIcon, MessageSquareIcon, PinIcon, + SearchIcon, ShieldIcon, SparklesIcon, SquarePenIcon, @@ -74,6 +75,9 @@ interface AgentCardProps { /** "card" (default) renders the full gallery card; "list" renders a compact * single-row layout for the list view. All dialogs/actions are shared. */ layout?: "card" | "list"; + /** Click the 发布者 line to filter the gallery to that publisher. Receives the + * owner user_id + its desensitized display label. */ + onPublisherClick?: (publisher: { id: string; label: string }) => void; } interface BadgeItem { @@ -185,7 +189,7 @@ function ClampedBadgeRow({ items }: { items: BadgeItem[] }) { ); } -export function AgentCard({ agent, layout = "card" }: AgentCardProps) { +export function AgentCard({ agent, layout = "card", onPublisherClick }: AgentCardProps) { const isList = layout === "list"; const { t } = useI18n(); const router = useRouter(); @@ -208,6 +212,7 @@ export function AgentCard({ agent, layout = "card" }: AgentCardProps) { const [selectedSkills, setSelectedSkills] = useState( agent.skills ?? [], ); + const [skillSearch, setSkillSearch] = useState(""); const [detailSkillName, setDetailSkillName] = useState(null); // The /api/agents list response intentionally omits `soul` (perf optimization @@ -245,6 +250,7 @@ export function AgentCard({ agent, layout = "card" }: AgentCardProps) { setEditPublished(agent.published); setEditIsPrivate(agent.is_private); setSelectedSkills(agent.skills ?? []); + setSkillSearch(""); }, [ agent.description, agent.is_private, @@ -437,7 +443,24 @@ export function AgentCard({ agent, layout = "card" }: AgentCardProps) { }); } - const enabledSkills = skills.filter((skill) => skill.enabled); + const enabledSkills = useMemo( + () => skills.filter((skill) => skill.enabled), + [skills], + ); + + // Search over English name, Chinese name (name_zh) and description, so both + // 中英文 work. Already-selected skills stay searchable too (selection is + // independent of this filter). + const filteredSkills = useMemo(() => { + const q = skillSearch.trim().toLowerCase(); + if (!q) return enabledSkills; + return enabledSkills.filter( + (skill) => + skill.name.toLowerCase().includes(q) || + (skill.name_zh ?? "").toLowerCase().includes(q) || + (skill.description ?? "").toLowerCase().includes(q), + ); + }, [enabledSkills, skillSearch]); // Status badges (内置 / 我的 / 来自他人 / 已发布 / 私密 / 首页推荐) — shared by // both the card header and the compact list row. @@ -498,16 +521,36 @@ export function AgentCard({ agent, layout = "card" }: AgentCardProps) { ); - // 发布者(仅自定义/已发布的智能体显示;内置无 owner 不显示)。 + // 发布者(仅自定义/已发布的智能体显示;内置无 owner 不显示)。点击可按发布者筛选。 + const publisherLabel = agent.owner_username + ? desensitize(agent.owner_username) + : ""; + const canFilterByPublisher = + !isBuiltin && !!agent.owner_username && !!agent.user_id && !!onPublisherClick; const publisherNode = !isBuiltin && agent.owner_username ? ( -
{ + if (!canFilterByPublisher) return; + e.stopPropagation(); + onPublisherClick!({ id: agent.user_id!, label: publisherLabel }); + }} + className={cn( + "text-muted-foreground flex max-w-full items-center gap-1 text-sm font-medium", + canFilterByPublisher && + "hover:text-primary cursor-pointer transition-colors hover:underline", + )} + title={ + canFilterByPublisher + ? `点击筛选发布者:${publisherLabel}` + : `发布者:${publisherLabel}` + } > - - 发布者:{desensitize(agent.owner_username)} -
+ + 发布者:{publisherLabel} + ) : null; // 技能英文名 → 中文名映射,用于把智能体的技能徽章显示为中文(agent.skills 只存英文名)。 @@ -816,6 +859,17 @@ export function AgentCard({ agent, layout = "card" }: AgentCardProps) { 已选 {selectedSkills.length} 个 + {enabledSkills.length > 0 && ( +
+ + setSkillSearch(e.currentTarget.value)} + className="h-8 pl-7 text-xs" + /> +
+ )}
{isSkillsLoading ? (

技能加载中...

@@ -823,9 +877,11 @@ export function AgentCard({ agent, layout = "card" }: AgentCardProps) {

当前没有可用技能

+ ) : filteredSkills.length === 0 ? ( +

没有匹配的技能

) : (
- {enabledSkills.map((skill) => { + {filteredSkills.map((skill) => { const checked = selectedSkills.includes(skill.name); return (
([]); const [sort, setSort] = useState("time_desc"); + // Publisher filter: click a card's 发布者 to keep only that user's agents. + // {id} is the owner user_id (server filter), {label} the desensitized name. + const [publisher, setPublisher] = useState<{ id: string; label: string } | null>( + null, + ); // Per-tab 1-based page index, preserved while switching tabs. const [pages, setPages] = useState<{ all: number; mine: number; private: number }>( { all: 1, mine: 1, private: 1 }, @@ -48,7 +53,7 @@ export function AgentGallery() { const tagKey = tagIds.join(","); useEffect(() => { setPages({ all: 1, mine: 1, private: 1 }); - }, [search, tagKey, sort]); + }, [search, tagKey, sort, publisher?.id]); const { data: privateStatus } = usePrivateSquareStatus(); const privateFeatureAvailable = !!privateStatus?.enabled; @@ -77,6 +82,7 @@ export function AgentGallery() { page: mainPage, pageSize: PAGE_SIZE, enabled: activeTab !== "private", + owner: publisher?.id, }); const { @@ -91,6 +97,7 @@ export function AgentGallery() { page: pages.private, pageSize: PAGE_SIZE, enabled: activeTab === "private", + owner: publisher?.id, }); // If the server reports the stored password no longer works (likely the @@ -127,17 +134,29 @@ export function AgentGallery() { setPrivatePassword(null); }; + const handlePublisherClick = (p: { id: string; label: string }) => + setPublisher(p); + const renderGrid = (list: Agent[]) => viewMode === "list" ? (
{list.map((agent) => ( - + ))}
) : (
{list.map((agent) => ( - + ))}
); @@ -356,6 +375,20 @@ export function AgentGallery() { } /> + {publisher && ( +
+ 筛选发布者: + +
+ )}
diff --git a/frontend-web/src/components/workspace/settings/skill-settings-page.tsx b/frontend-web/src/components/workspace/settings/skill-settings-page.tsx index f0415ec7..7dc278ec 100644 --- a/frontend-web/src/components/workspace/settings/skill-settings-page.tsx +++ b/frontend-web/src/components/workspace/settings/skill-settings-page.tsx @@ -23,6 +23,7 @@ import { TrendingUpIcon, Upload, UserIcon, + XIcon, } from "lucide-react"; import { useRouter } from "next/navigation"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; @@ -154,6 +155,11 @@ function SkillSettingsList({ onClose }: { onClose?: () => void }) { const [tagIds, setTagIds] = useState([]); const [sort, setSort] = useState("time_desc"); const [page, setPage] = useState(1); + // Publisher filter: click a skill's 发布者 to keep only that user's skills. + // {id} is owner_user_id; {label} the desensitized display name. + const [publisher, setPublisher] = useState<{ id: string; label: string } | null>( + null, + ); const [viewMode, setViewMode] = useViewMode("skill"); const reconcileMutation = useReconcileCustomSkills(); const fillNamesMutation = useGenerateMissingSkillNameZh(); @@ -203,7 +209,14 @@ function SkillSettingsList({ onClose }: { onClose?: () => void }) { }; }, [skills, currentUserId]); - const filteredSkills = filter === "mine" ? activeMine : activeAll; + const scopedSkills = filter === "mine" ? activeMine : activeAll; + const filteredSkills = useMemo( + () => + publisher + ? scopedSkills.filter((s) => s.owner_user_id === publisher.id) + : scopedSkills, + [scopedSkills, publisher], + ); const orderedSkills = useMemo( () => filteredSkills @@ -228,7 +241,7 @@ function SkillSettingsList({ onClose }: { onClose?: () => void }) { }, [page, totalPages]); useEffect(() => { setPage(1); - }, [search, tagIds, sort]); + }, [search, tagIds, sort, publisher?.id, filter]); const pagedSkills = useMemo( () => orderedSkills.slice((page - 1) * PAGE_SIZE, page * PAGE_SIZE), [orderedSkills, page], @@ -344,12 +357,13 @@ function SkillSettingsList({ onClose }: { onClose?: () => void }) {
} /> + {publisher && ( +
+ 筛选发布者: + +
+ )} {isLoading && (
{t.common.loading}
)} @@ -421,6 +449,7 @@ function SkillSettingsList({ onClose }: { onClose?: () => void }) { currentUserId={currentUserId} isAdmin={isAdmin} layout={viewMode === "card" ? "card" : "row"} + onPublisherClick={setPublisher} /> ))}
@@ -478,12 +507,16 @@ export function SkillRow({ currentUserId, isAdmin, layout = "row", + onPublisherClick, }: { skill: Skill; currentUserId: string | null; isAdmin: boolean; /** "row" = full-width horizontal item (default); "card" = grid card cell. */ layout?: "row" | "card"; + /** Click the 发布者 line to filter the list to that publisher. Receives the + * owner_user_id + its desensitized display label. */ + onPublisherClick?: (publisher: { id: string; label: string }) => void; }) { const isCard = layout === "card"; const { t } = useI18n(); @@ -674,10 +707,31 @@ export function SkillRow({ const metaNodes = ( <> {!isBuiltin && skill.owner_username && ( -
- - 发布者:{desensitize(skill.owner_username)} -
+ )} {skill.use_count !== undefined && (
diff --git a/frontend-web/src/components/workspace/skills/skill-detail-dialog.tsx b/frontend-web/src/components/workspace/skills/skill-detail-dialog.tsx index cc7fcdd6..a083d63b 100644 --- a/frontend-web/src/components/workspace/skills/skill-detail-dialog.tsx +++ b/frontend-web/src/components/workspace/skills/skill-detail-dialog.tsx @@ -11,6 +11,7 @@ import { PencilIcon, SlidersHorizontalIcon, SparklesIcon, + XIcon, } from "lucide-react"; import { useEffect, useMemo, useState } from "react"; import { toast } from "sonner"; @@ -106,6 +107,7 @@ export function SkillDetailDialog({ const [editing, setEditing] = useState(false); const [draft, setDraft] = useState(""); + const [editingNameZh, setEditingNameZh] = useState(false); const [nameZhDraft, setNameZhDraft] = useState(""); const [activeTab, setActiveTab] = useState< "detail" | "display" | "source" | "files" | null @@ -137,6 +139,7 @@ export function SkillDetailDialog({ useEffect(() => { setEditing(false); + setEditingNameZh(false); setActiveTab(null); setShowRawSource(false); setOpenedFilePath(null); @@ -169,6 +172,11 @@ export function SkillDetailDialog({ } } + function startEditingNameZh() { + setNameZhDraft(skill?.name_zh ?? ""); + setEditingNameZh(true); + } + async function handleSaveNameZh() { if (!skill) return; try { @@ -177,6 +185,7 @@ export function SkillDetailDialog({ nameZh: nameZhDraft.trim(), }); toast.success("中文名称已保存"); + setEditingNameZh(false); } catch (err) { toast.error(err instanceof Error ? err.message : String(err)); } @@ -215,32 +224,99 @@ export function SkillDetailDialog({
-
- - {skill?.name_zh?.trim() - ? skill.name_zh - : desensitize(skill?.name ?? skillName ?? "")} - - {skill?.name_zh?.trim() && ( - + {editingNameZh ? ( +
+ {desensitize(skill?.name ?? skillName ?? "")} - - )} - {skill?.builtin && ( - - {tx.badgeBuiltin} - - )} - {skill?.published && ( - + setNameZhDraft(event.currentTarget.value)} + onKeyDown={(event) => { + if (event.key === "Enter") void handleSaveNameZh(); + if (event.key === "Escape") setEditingNameZh(false); + }} + placeholder={desensitize(skill?.name ?? skillName ?? "")} + className="h-8 max-w-[18rem] flex-1 text-sm" + autoFocus + disabled={generateNameZh.isPending || setNameZh.isPending} + /> +
+ {generateNameZh.isPending ? ( + + ) : ( + + )} + AI 生成 + + + +
+ ) : ( +
+ + {skill?.name_zh?.trim() + ? skill.name_zh + : desensitize(skill?.name ?? skillName ?? "")} + + {skill?.name_zh?.trim() && ( + + {desensitize(skill?.name ?? skillName ?? "")} + + )} + {canManage && ( + + )} + {skill?.builtin && ( + + {tx.badgeBuiltin} + + )} + {skill?.published && ( + + + {tx.badgePublished} + + )} +
+ )} {skill?.description ? desensitize(skill.description) : "-"} @@ -336,57 +412,6 @@ export function SkillDetailDialog({ className="m-0 min-h-0 flex-1 overscroll-contain px-6 py-5 data-[state=inactive]:hidden" style={{ overflowY: "auto" }} > -
-
- 中文名称 - {canManage && ( - - )} -
- {canManage ? ( -
- setNameZhDraft(event.currentTarget.value)} - placeholder="为该技能设置中文名称(留空则使用英文名)" - className="h-8 text-sm" - disabled={generateNameZh.isPending} - /> - -
- ) : ( -

- {skill?.name_zh?.trim() ? skill.name_zh : "未设置"} -

- )} -
{editing ? (