Files
deerflow-code/frontend-web/src/core/agents/api.ts
T
董宏凯andClaude Opus 4.8 cd3a71baab 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>
2026-06-13 20:42:26 +08:00

407 lines
13 KiB
TypeScript

import { getBackendBaseURL } from "@/core/config";
import { apiFetch } from "@/core/api";
import type {
Agent,
AgentSelectorResponse,
AgentSort,
CreateAgentRequest,
PaginatedAgents,
UpdateAgentRequest,
} from "./types";
const BACKEND_UNAVAILABLE_STATUSES = new Set([502, 503, 504]);
export class AgentNameCheckError extends Error {
constructor(
message: string,
public readonly reason: "backend_unreachable" | "request_failed",
) {
super(message);
this.name = "AgentNameCheckError";
}
}
export interface ListAgentsParams {
search?: string;
tagIds?: string[];
/** 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;
sort?: AgentSort;
/** Keep only agents published by this user id (the card's publisher). */
owner?: string;
/** Keep only agents filed under this publish square (发布广场). */
square?: string;
}
function buildAgentsQuery(params: ListAgentsParams): string {
const qs = new URLSearchParams();
if (params.search) qs.set("search", params.search);
for (const id of params.tagIds ?? []) qs.append("tag_ids", id);
if (params.scope) qs.set("scope", params.scope);
if (params.page != null) qs.set("page", String(params.page));
if (params.pageSize != null) qs.set("page_size", String(params.pageSize));
if (params.sort) qs.set("sort", params.sort);
if (params.owner) qs.set("owner", params.owner);
if (params.square) qs.set("square", params.square);
return qs.toString();
}
export async function listAgents(params: ListAgentsParams = {}): Promise<Agent[]> {
const query = buildAgentsQuery(params);
const res = await apiFetch(
`${getBackendBaseURL()}/api/agents${query ? `?${query}` : ""}`,
);
if (!res.ok) throw new Error(`Failed to load agents: ${res.statusText}`);
const data = (await res.json()) as { agents: Agent[] };
return data.agents;
}
/** Like {@link listAgents} but returns the pagination envelope (total/page). */
export async function listAgentsPaginated(
params: ListAgentsParams = {},
): Promise<PaginatedAgents> {
const query = buildAgentsQuery(params);
const res = await apiFetch(
`${getBackendBaseURL()}/api/agents${query ? `?${query}` : ""}`,
);
if (!res.ok) throw new Error(`Failed to load agents: ${res.statusText}`);
const data = (await res.json()) as {
agents: Agent[];
total?: number;
page?: number;
page_size?: number;
};
return {
agents: data.agents,
total: data.total ?? data.agents.length,
page: data.page ?? params.page ?? 1,
pageSize: data.page_size ?? params.pageSize ?? data.agents.length,
};
}
export async function listAllAgentsAdmin(): Promise<Agent[]> {
const res = await apiFetch(`${getBackendBaseURL()}/api/agents?admin=true`);
if (!res.ok) {
const err = (await res.json().catch(() => ({}))) as { detail?: string };
throw new Error(err.detail ?? `Failed to load agents: ${res.statusText}`);
}
const data = (await res.json()) as { agents: Agent[] };
return data.agents;
}
export async function getAgent(id: string): Promise<Agent> {
const res = await apiFetch(`${getBackendBaseURL()}/api/agents/${id}`);
if (!res.ok) throw new Error(`Agent '${id}' not found`);
return res.json() as Promise<Agent>;
}
export async function createAgent(request: CreateAgentRequest): Promise<Agent> {
const res = await apiFetch(`${getBackendBaseURL()}/api/agents`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(request),
});
if (!res.ok) {
const err = (await res.json().catch(() => ({}))) as { detail?: string };
throw new Error(err.detail ?? `Failed to create agent: ${res.statusText}`);
}
return res.json() as Promise<Agent>;
}
export async function updateAgent(
id: string,
request: UpdateAgentRequest,
): Promise<Agent> {
const res = await apiFetch(`${getBackendBaseURL()}/api/agents/${id}`, {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(request),
});
if (!res.ok) {
const err = (await res.json().catch(() => ({}))) as { detail?: string };
throw new Error(err.detail ?? `Failed to update agent: ${res.statusText}`);
}
return res.json() as Promise<Agent>;
}
export async function deleteAgent(id: string): Promise<void> {
const res = await apiFetch(`${getBackendBaseURL()}/api/agents/${id}`, {
method: "DELETE",
});
if (!res.ok) throw new Error(`Failed to delete agent: ${res.statusText}`);
}
export async function getAgentSelector(): Promise<AgentSelectorResponse> {
const res = await apiFetch(`${getBackendBaseURL()}/api/agents/selector`);
if (!res.ok) throw new Error(`Failed to load agent selector: ${res.statusText}`);
return res.json() as Promise<AgentSelectorResponse>;
}
export async function favoriteAgent(id: string): Promise<void> {
const res = await apiFetch(`${getBackendBaseURL()}/api/agents/${id}/favorite`, {
method: "POST",
});
if (!res.ok) {
const err = (await res.json().catch(() => ({}))) as { detail?: string };
throw new Error(err.detail ?? `Failed to favorite agent: ${res.statusText}`);
}
}
export async function unfavoriteAgent(id: string): Promise<void> {
const res = await apiFetch(`${getBackendBaseURL()}/api/agents/${id}/favorite`, {
method: "DELETE",
});
if (!res.ok) {
const err = (await res.json().catch(() => ({}))) as { detail?: string };
throw new Error(err.detail ?? `Failed to unfavorite agent: ${res.statusText}`);
}
}
export async function pinAgent(id: string): Promise<void> {
const res = await apiFetch(`${getBackendBaseURL()}/api/agents/${id}/pin`, {
method: "POST",
});
if (!res.ok) {
const err = (await res.json().catch(() => ({}))) as { detail?: string };
throw new Error(err.detail ?? `Failed to pin agent: ${res.statusText}`);
}
}
export async function unpinAgent(id: string): Promise<void> {
const res = await apiFetch(`${getBackendBaseURL()}/api/agents/${id}/pin`, {
method: "DELETE",
});
if (!res.ok) {
const err = (await res.json().catch(() => ({}))) as { detail?: string };
throw new Error(err.detail ?? `Failed to unpin agent: ${res.statusText}`);
}
}
export async function reorderFeaturedAgents(agentIds: string[]): Promise<void> {
const res = await apiFetch(
`${getBackendBaseURL()}/api/agents/featured/order`,
{
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ agent_ids: agentIds }),
},
);
if (!res.ok) {
const err = (await res.json().catch(() => ({}))) as { detail?: string };
throw new Error(
err.detail ?? `Failed to reorder featured agents: ${res.statusText}`,
);
}
}
export async function listPrivateAgents(password: string): Promise<Agent[]> {
const res = await apiFetch(
`${getBackendBaseURL()}/api/agents/private-square/list`,
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ password }),
},
);
if (!res.ok) {
const err = (await res.json().catch(() => ({}))) as { detail?: string };
throw new Error(
err.detail ?? `Failed to load private agents: ${res.statusText}`,
);
}
const data = (await res.json()) as { agents: Agent[] };
return data.agents;
}
export interface ListPrivateAgentsParams {
password: string;
search?: string;
tagIds?: string[];
page?: number;
pageSize?: number;
sort?: AgentSort;
/** Keep only agents published by this user id (the card's publisher). */
owner?: string;
}
/** Paginated private-square listing (password-gated). */
export async function listPrivateAgentsPaginated(
params: ListPrivateAgentsParams,
): Promise<PaginatedAgents> {
const res = await apiFetch(
`${getBackendBaseURL()}/api/agents/private-square/list`,
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
password: params.password,
search: params.search || undefined,
tag_ids: params.tagIds && params.tagIds.length ? params.tagIds : undefined,
page: params.page,
page_size: params.pageSize,
sort: params.sort,
owner: params.owner || undefined,
}),
},
);
if (!res.ok) {
const err = (await res.json().catch(() => ({}))) as { detail?: string };
throw new Error(
err.detail ?? `Failed to load private agents: ${res.statusText}`,
);
}
const data = (await res.json()) as {
agents: Agent[];
total?: number;
page?: number;
page_size?: number;
};
return {
agents: data.agents,
total: data.total ?? data.agents.length,
page: data.page ?? params.page ?? 1,
pageSize: data.page_size ?? params.pageSize ?? data.agents.length,
};
}
export async function setAgentPrivate(
id: string,
isPrivate: boolean,
): Promise<Agent> {
const res = await apiFetch(
`${getBackendBaseURL()}/api/agents/${id}/private`,
{
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ is_private: isPrivate }),
},
);
if (!res.ok) {
const err = (await res.json().catch(() => ({}))) as { detail?: string };
throw new Error(
err.detail ?? `Failed to update agent privacy: ${res.statusText}`,
);
}
return res.json() as Promise<Agent>;
}
export async function setAgentFeatured(
id: string,
order: number | null,
): Promise<Agent> {
const res = await apiFetch(
`${getBackendBaseURL()}/api/agents/${id}/featured`,
{
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ order }),
},
);
if (!res.ok) {
const err = (await res.json().catch(() => ({}))) as { detail?: string };
throw new Error(
err.detail ?? `Failed to set featured: ${res.statusText}`,
);
}
return res.json() as Promise<Agent>;
}
export interface AgentSkillsSyncRemoval {
agent_id: string;
agent_name: string | null;
user_id: string | null;
removed_skills: string[];
}
export interface AgentSkillsSyncResult {
checked: number;
modified: number;
removals: AgentSkillsSyncRemoval[];
}
export async function adminSyncAgentSkills(): Promise<AgentSkillsSyncResult> {
const res = await apiFetch(
`${getBackendBaseURL()}/api/agents/admin/sync-skills`,
{ method: "POST" },
);
if (!res.ok) {
const err = (await res.json().catch(() => ({}))) as { detail?: string };
throw new Error(err.detail ?? `Failed to sync agent skills: ${res.statusText}`);
}
return res.json() as Promise<AgentSkillsSyncResult>;
}
export interface KnowledgeIngestAttempt {
title_field: string;
status_code: number | null;
response_text: string;
success: boolean;
error: string | null;
}
export interface KnowledgeIngestPublishResult {
success: boolean;
template_name: string;
node_name: string;
batch_number: string;
attempts: KnowledgeIngestAttempt[];
}
/** 把一份 md 产物(标题 + 全文)发布到外部知识入库接口。后端会先按
* 「名称」字段映射发送,失败后用同一 nodeName 换「标题」字段重试一次。 */
export async function publishArtifactToKnowledge(request: {
agent_id: string;
title: string;
content: string;
}): Promise<KnowledgeIngestPublishResult> {
const res = await apiFetch(
`${getBackendBaseURL()}/api/knowledge-ingest/publish`,
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(request),
},
);
if (!res.ok) {
const err = (await res.json().catch(() => ({}))) as { detail?: string };
throw new Error(err.detail ?? `发布入库失败: ${res.statusText}`);
}
return res.json() as Promise<KnowledgeIngestPublishResult>;
}
export async function checkAgentName(
name: string,
): Promise<{ available: boolean; name: string }> {
let res: Response;
try {
res = await apiFetch(
`${getBackendBaseURL()}/api/agents/check?name=${encodeURIComponent(name)}`,
);
} catch {
throw new AgentNameCheckError(
"Could not reach the CM Assistant backend.",
"backend_unreachable",
);
}
if (!res.ok) {
const err = (await res.json().catch(() => ({}))) as { detail?: string };
if (BACKEND_UNAVAILABLE_STATUSES.has(res.status)) {
throw new AgentNameCheckError(
"Could not reach the CM Assistant backend.",
"backend_unreachable",
);
}
throw new AgentNameCheckError(
err.detail ?? `Failed to check agent name: ${res.statusText}`,
"request_failed",
);
}
return res.json() as Promise<{ available: boolean; name: string }>;
}