feat(agents): 内置智能体筛选(仅管理员)+ 内置 agent 默认用途标签
- 后端 scope=builtin(user_id IS NULL)加入 sql/memory 的 scope 过滤; /api/agents 扩展 scope 正则并加 admin 闸门(非管理员请求 403) - 新增 _builtin_agent_tag_seed:启动时幂等给功能型内置 agent 按用途打标签 (roundtable-* → 多智能体规划,ai-writing-* → AI写作),只追加不覆盖; 无用途的内置 agent 不打标签 - 前端 agent-gallery 新增「内置智能体」Tab(仅 system_role==admin 可见), scope 类型在 api.ts/hooks.ts 同步 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
391409b6c0
commit
cd3a71baab
8 files changed
+163
-9
No files matched your search
@@ -12,6 +12,7 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import { TagSearchBar } from "@/components/workspace/tags/tag-search-bar";
|
||||
import { ViewModeToggle } from "@/components/workspace/view-mode-toggle";
|
||||
import { getStoredAuth } from "@/core/auth";
|
||||
import { useAgentsPaginated, usePrivateAgentsPaginated } from "@/core/agents";
|
||||
import type { Agent, AgentSort } from "@/core/agents";
|
||||
import { useI18n } from "@/core/i18n/hooks";
|
||||
@@ -29,6 +30,8 @@ const PRIVATE_SQUARE_PASSWORD_KEY = "deer-flow.private-square-password";
|
||||
export function AgentGallery() {
|
||||
const { t } = useI18n();
|
||||
const router = useRouter();
|
||||
// 「内置智能体」筛选仅管理员可见(后端 scope=builtin 也做了 admin 闸门兜底)。
|
||||
const isAdmin = getStoredAuth()?.system_role === "admin";
|
||||
const [viewMode, setViewMode] = useViewMode("agent");
|
||||
const [activeTab, setActiveTab] = useState<string>("mine");
|
||||
const [search, setSearch] = useState("");
|
||||
@@ -58,15 +61,21 @@ export function AgentGallery() {
|
||||
// If the active square tab disappears (admin removed that square), fall back
|
||||
// to "我的" so the gallery never points at a dead tab.
|
||||
useEffect(() => {
|
||||
// 非管理员落到「内置」标签(理论上点不到,兜底)→ 回到「我的」。
|
||||
if (activeTab === "builtin" && !isAdmin) {
|
||||
setActiveTab("mine");
|
||||
return;
|
||||
}
|
||||
if (
|
||||
activeTab !== "mine" &&
|
||||
activeTab !== "private" &&
|
||||
activeTab !== "builtin" &&
|
||||
squareTabs.length > 0 &&
|
||||
!squareTabs.some((sq) => sq.id === activeTab)
|
||||
) {
|
||||
setActiveTab("mine");
|
||||
}
|
||||
}, [activeTab, squareTabs]);
|
||||
}, [activeTab, squareTabs, isAdmin]);
|
||||
|
||||
const { data: privateStatus } = usePrivateSquareStatus();
|
||||
const privateFeatureAvailable = !!privateStatus?.enabled;
|
||||
@@ -80,9 +89,16 @@ export function AgentGallery() {
|
||||
const [passwordInput, setPasswordInput] = useState("");
|
||||
|
||||
// Only the active tab fetches. "广场" maps to the server "square" scope
|
||||
// (built-in/published, never private), "我的" to "mine" (owned + favorited).
|
||||
const isSquareTab = activeTab !== "mine" && activeTab !== "private";
|
||||
const mainScope = activeTab === "mine" ? "mine" : "square";
|
||||
// (built-in/published, never private), "我的" to "mine" (owned + favorited),
|
||||
// "内置" to "builtin" (admin-only, user_id IS NULL).
|
||||
const isSquareTab =
|
||||
activeTab !== "mine" && activeTab !== "private" && activeTab !== "builtin";
|
||||
const mainScope =
|
||||
activeTab === "mine"
|
||||
? "mine"
|
||||
: activeTab === "builtin"
|
||||
? "builtin"
|
||||
: "square";
|
||||
const mainPage = pages[activeTab] ?? 1;
|
||||
const {
|
||||
agents: mainAgents,
|
||||
@@ -393,6 +409,12 @@ export function AgentGallery() {
|
||||
私密广场
|
||||
</TabsTrigger>
|
||||
)}
|
||||
{isAdmin && (
|
||||
<TabsTrigger value="builtin" title="内置/功能型智能体(仅管理员可见)">
|
||||
<BotIcon className="mr-1 h-3.5 w-3.5" />
|
||||
内置智能体
|
||||
</TabsTrigger>
|
||||
)}
|
||||
</TabsList>
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="text-muted-foreground text-sm whitespace-nowrap">
|
||||
@@ -490,6 +512,19 @@ export function AgentGallery() {
|
||||
})}
|
||||
</TabsContent>
|
||||
|
||||
{isAdmin && (
|
||||
<TabsContent value="builtin" className="flex-1 overflow-y-auto p-6 mt-0">
|
||||
{renderListBody({
|
||||
list: mainAgents,
|
||||
total: mainTotal,
|
||||
isLoading: mainLoading,
|
||||
isFetching: mainFetching,
|
||||
tab: "builtin",
|
||||
emptyDescription: "暂时没有内置智能体。",
|
||||
})}
|
||||
</TabsContent>
|
||||
)}
|
||||
|
||||
{privateFeatureAvailable && (
|
||||
<TabsContent
|
||||
value="private"
|
||||
|
||||
@@ -25,8 +25,9 @@ export class AgentNameCheckError extends Error {
|
||||
export interface ListAgentsParams {
|
||||
search?: string;
|
||||
tagIds?: string[];
|
||||
/** Pagination scope. Omit for the legacy combined-visibility list. */
|
||||
scope?: "mine" | "square";
|
||||
/** Pagination scope. Omit for the legacy combined-visibility list.
|
||||
* "builtin" = 内置智能体 (user_id IS NULL), admin only — backend 403s non-admins. */
|
||||
scope?: "mine" | "square" | "builtin";
|
||||
/** 1-based page index. Providing this (or scope) enables server-side pagination. */
|
||||
page?: number;
|
||||
pageSize?: number;
|
||||
|
||||
@@ -79,7 +79,7 @@ export function useAgents(params: { search?: string; tagIds?: string[] } = {}) {
|
||||
/** Server-side paginated agent listing for one gallery tab (scope). Returns
|
||||
* the page of agents plus the total count for the "共 N 个" header. */
|
||||
export function useAgentsPaginated(params: {
|
||||
scope: "mine" | "square";
|
||||
scope: "mine" | "square" | "builtin";
|
||||
search?: string;
|
||||
tagIds?: string[];
|
||||
sort?: AgentSort;
|
||||
|
||||
Reference in new issue
Block a user