- 智能体支持从广场收藏到「我的」(新增 agent_favorites 表与收藏/取消收藏接口) - 定时任务新增「必须生成Markdown文件」开关:注入提示词并校验生成的 .md 文件且正文超过100字,否则记为失败(存于 execution_context,无需迁移) - 聊天问答展示时间(年月日 时分):问题在右下角、回答在左下角 - 常用提示词改为与账号绑定(新增 user_custom_prompts 表与 /api/user-prompts 接口),仅在首页输入框上方以居中标签 + 轮播展示,齿轮置于最右侧管理 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
226 lines
6.8 KiB
TypeScript
226 lines
6.8 KiB
TypeScript
import {
|
|
keepPreviousData,
|
|
useMutation,
|
|
useQuery,
|
|
useQueryClient,
|
|
} from "@tanstack/react-query";
|
|
|
|
import {
|
|
adminSyncAgentSkills,
|
|
createAgent,
|
|
deleteAgent,
|
|
favoriteAgent,
|
|
getAgent,
|
|
getAgentSelector,
|
|
listAgents,
|
|
listAllAgentsAdmin,
|
|
listPrivateAgents,
|
|
reorderFeaturedAgents,
|
|
setAgentFeatured,
|
|
setAgentPrivate,
|
|
unfavoriteAgent,
|
|
updateAgent,
|
|
} from "./api";
|
|
import type { Agent, CreateAgentRequest, UpdateAgentRequest } from "./types";
|
|
|
|
/** Patch an updated agent into every cached agent listing immediately so
|
|
* cards re-render before the background refetch finishes. Without this the
|
|
* mutation just invalidates the query and the user has to wait for the
|
|
* round-trip + render — which on a big list feels like nothing happened.
|
|
*/
|
|
function applyAgentToCache(
|
|
queryClient: ReturnType<typeof useQueryClient>,
|
|
next: Agent,
|
|
) {
|
|
queryClient.setQueriesData<Agent[] | undefined>(
|
|
{ queryKey: ["agents"] },
|
|
(old) => {
|
|
if (!Array.isArray(old)) return old;
|
|
return old.map((a) => (a.id === next.id ? { ...a, ...next } : a));
|
|
},
|
|
);
|
|
}
|
|
|
|
export function useAgents(params: { search?: string; tagIds?: string[] } = {}) {
|
|
const search = params.search ?? "";
|
|
const tagIds = params.tagIds ?? [];
|
|
const { data, isLoading, error } = useQuery({
|
|
queryKey: ["agents", search, [...tagIds].sort()],
|
|
queryFn: () => listAgents({ search, tagIds }),
|
|
placeholderData: keepPreviousData,
|
|
});
|
|
return { agents: data ?? [], isLoading, error };
|
|
}
|
|
|
|
/**
|
|
* Admin-only listing — returns every agent including unpublished agents owned
|
|
* by other users and private-square entries. Caller must be ``system_role
|
|
* === "admin"`` or the backend will 403.
|
|
*/
|
|
export function useAllAgentsAdmin(enabled: boolean) {
|
|
const { data, isLoading, error } = useQuery({
|
|
queryKey: ["agents", "admin", "all"],
|
|
queryFn: () => listAllAgentsAdmin(),
|
|
enabled,
|
|
staleTime: 30_000,
|
|
});
|
|
return { agents: data ?? [], isLoading, error };
|
|
}
|
|
|
|
export function useAgent(id: string | null | undefined) {
|
|
const { data, isLoading, error } = useQuery({
|
|
queryKey: ["agents", id],
|
|
queryFn: () => getAgent(id!),
|
|
enabled: !!id,
|
|
});
|
|
return { agent: data ?? null, isLoading, error };
|
|
}
|
|
|
|
export function useCreateAgent() {
|
|
const queryClient = useQueryClient();
|
|
return useMutation({
|
|
mutationFn: (request: CreateAgentRequest) => createAgent(request),
|
|
onSuccess: () => {
|
|
void queryClient.invalidateQueries({ queryKey: ["agents"] });
|
|
},
|
|
});
|
|
}
|
|
|
|
export function useUpdateAgent() {
|
|
const queryClient = useQueryClient();
|
|
return useMutation({
|
|
mutationFn: ({
|
|
id,
|
|
request,
|
|
}: {
|
|
id: string;
|
|
request: UpdateAgentRequest;
|
|
}) => updateAgent(id, request),
|
|
onSuccess: (data, { id }) => {
|
|
applyAgentToCache(queryClient, data);
|
|
void queryClient.invalidateQueries({ queryKey: ["agents"] });
|
|
void queryClient.invalidateQueries({ queryKey: ["agents", id] });
|
|
},
|
|
});
|
|
}
|
|
|
|
export function useDeleteAgent() {
|
|
const queryClient = useQueryClient();
|
|
return useMutation({
|
|
mutationFn: (id: string) => deleteAgent(id),
|
|
onSuccess: () => {
|
|
void queryClient.invalidateQueries({ queryKey: ["agents"] });
|
|
},
|
|
});
|
|
}
|
|
|
|
/** Toggle a user's favorite (收藏) on an agent, with an optimistic flip of
|
|
* ``is_favorite`` across every cached agent listing + detail so the star
|
|
* reacts instantly before the round-trip completes. */
|
|
export function useToggleAgentFavorite() {
|
|
const queryClient = useQueryClient();
|
|
return useMutation({
|
|
mutationFn: ({ id, favorite }: { id: string; favorite: boolean }) =>
|
|
favorite ? favoriteAgent(id) : unfavoriteAgent(id),
|
|
onMutate: ({ id, favorite }) => {
|
|
queryClient.setQueriesData<Agent[] | undefined>(
|
|
{ queryKey: ["agents"] },
|
|
(old) =>
|
|
Array.isArray(old)
|
|
? old.map((a) =>
|
|
a.id === id ? { ...a, is_favorite: favorite } : a,
|
|
)
|
|
: old,
|
|
);
|
|
queryClient.setQueryData<Agent | undefined>(["agents", id], (old) =>
|
|
old ? { ...old, is_favorite: favorite } : old,
|
|
);
|
|
},
|
|
onSettled: () => {
|
|
void queryClient.invalidateQueries({ queryKey: ["agents"] });
|
|
},
|
|
});
|
|
}
|
|
|
|
export function useAgentSelector() {
|
|
const { data, isLoading, error } = useQuery({
|
|
queryKey: ["agents", "selector"],
|
|
queryFn: () => getAgentSelector(),
|
|
staleTime: 60_000,
|
|
});
|
|
return {
|
|
agents: data?.agents ?? [],
|
|
isLoading,
|
|
error,
|
|
};
|
|
}
|
|
|
|
function invalidateAgentQueries(queryClient: ReturnType<typeof useQueryClient>) {
|
|
void queryClient.invalidateQueries({ queryKey: ["agents"] });
|
|
}
|
|
|
|
export function useSetAgentFeatured() {
|
|
const queryClient = useQueryClient();
|
|
return useMutation({
|
|
mutationFn: ({ id, order }: { id: string; order: number | null }) =>
|
|
setAgentFeatured(id, order),
|
|
onSuccess: (data, { id }) => {
|
|
applyAgentToCache(queryClient, data);
|
|
invalidateAgentQueries(queryClient);
|
|
void queryClient.invalidateQueries({ queryKey: ["agents", id] });
|
|
},
|
|
});
|
|
}
|
|
|
|
export function useReorderFeaturedAgents() {
|
|
const queryClient = useQueryClient();
|
|
return useMutation({
|
|
mutationFn: (agentIds: string[]) => reorderFeaturedAgents(agentIds),
|
|
onSuccess: () => invalidateAgentQueries(queryClient),
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Query the password-gated private square. ``password`` is sent on every
|
|
* fetch — wire ``enabled`` to false until the user has typed it so we don't
|
|
* hit the endpoint with an empty body and get a 403.
|
|
*/
|
|
export function usePrivateAgents(password: string | null) {
|
|
const { data, isLoading, error, refetch } = useQuery({
|
|
queryKey: ["agents", "private-square", password ?? ""],
|
|
queryFn: () => listPrivateAgents(password!),
|
|
enabled: !!password,
|
|
staleTime: 30_000,
|
|
retry: false,
|
|
});
|
|
return { agents: data ?? [], isLoading, error, refetch };
|
|
}
|
|
|
|
export function useSetAgentPrivate() {
|
|
const queryClient = useQueryClient();
|
|
return useMutation({
|
|
mutationFn: ({ id, isPrivate }: { id: string; isPrivate: boolean }) =>
|
|
setAgentPrivate(id, isPrivate),
|
|
onSuccess: (data, { id }) => {
|
|
applyAgentToCache(queryClient, data);
|
|
invalidateAgentQueries(queryClient);
|
|
void queryClient.invalidateQueries({ queryKey: ["agents", id] });
|
|
void queryClient.invalidateQueries({
|
|
queryKey: ["agents", "private-square"],
|
|
});
|
|
},
|
|
});
|
|
}
|
|
|
|
export function useAdminSyncAgentSkills() {
|
|
const queryClient = useQueryClient();
|
|
return useMutation({
|
|
mutationFn: adminSyncAgentSkills,
|
|
onSuccess: (result) => {
|
|
if (result.modified > 0) {
|
|
invalidateAgentQueries(queryClient);
|
|
}
|
|
},
|
|
});
|
|
}
|