智能体分权
This commit is contained in:
1 parent
e7a455f22e
commit
cf2e4bdd9c
31 files changed
+1237
-436
No files matched your search
@@ -134,11 +134,11 @@ export function App() {
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/workspace/agents/:agent_name/chats"
|
||||
path="/workspace/agents/:agent_id/chats"
|
||||
element={<Navigate to="/workspace/agents" replace />}
|
||||
/>
|
||||
<Route
|
||||
path="/workspace/agents/:agent_name/chats/:thread_id"
|
||||
path="/workspace/agents/:agent_id/chats/:thread_id"
|
||||
element={
|
||||
<WorkspaceLayout>
|
||||
<ChatRuntime>
|
||||
|
||||
@@ -1,8 +1,15 @@
|
||||
"use client";
|
||||
|
||||
import { BotIcon, MessageSquareIcon, SquarePenIcon, Trash2Icon } from "lucide-react";
|
||||
import {
|
||||
BotIcon,
|
||||
EyeIcon,
|
||||
EyeOffIcon,
|
||||
MessageSquareIcon,
|
||||
SquarePenIcon,
|
||||
Trash2Icon,
|
||||
} from "lucide-react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
@@ -24,9 +31,11 @@ import {
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { useDeleteAgent, useUpdateAgent } from "@/core/agents";
|
||||
import type { Agent } from "@/core/agents";
|
||||
import { getStoredAuth } from "@/core/auth";
|
||||
import { useI18n } from "@/core/i18n/hooks";
|
||||
import { useSkills } from "@/core/skills/hooks";
|
||||
|
||||
@@ -42,28 +51,45 @@ export function AgentCard({ agent }: AgentCardProps) {
|
||||
const { skills, isLoading: isSkillsLoading } = useSkills();
|
||||
const [deleteOpen, setDeleteOpen] = useState(false);
|
||||
const [editOpen, setEditOpen] = useState(false);
|
||||
const [editName, setEditName] = useState(agent.name);
|
||||
const [editDescription, setEditDescription] = useState(agent.description ?? "");
|
||||
const [editSoul, setEditSoul] = useState(agent.soul ?? "");
|
||||
const [editPublished, setEditPublished] = useState(agent.published);
|
||||
const [selectedSkills, setSelectedSkills] = useState<string[]>(
|
||||
agent.skills ?? [],
|
||||
);
|
||||
|
||||
const currentUserId = useMemo(() => getStoredAuth()?.user_id ?? null, []);
|
||||
const isBuiltin = agent.builtin;
|
||||
const isOwned =
|
||||
!isBuiltin && agent.user_id !== null && agent.user_id === currentUserId;
|
||||
const isFromOthers = !isBuiltin && !isOwned;
|
||||
|
||||
useEffect(() => {
|
||||
if (!editOpen) {
|
||||
return;
|
||||
}
|
||||
setEditName(agent.name);
|
||||
setEditDescription(agent.description ?? "");
|
||||
setEditSoul(agent.soul ?? "");
|
||||
setEditPublished(agent.published);
|
||||
setSelectedSkills(agent.skills ?? []);
|
||||
}, [agent.description, agent.skills, agent.soul, editOpen]);
|
||||
}, [
|
||||
agent.description,
|
||||
agent.name,
|
||||
agent.published,
|
||||
agent.skills,
|
||||
agent.soul,
|
||||
editOpen,
|
||||
]);
|
||||
|
||||
function handleChat() {
|
||||
router.push(`/workspace/agents/${agent.name}/chats/new`);
|
||||
router.push(`/workspace/agents/${agent.id}/chats/new`);
|
||||
}
|
||||
|
||||
async function handleDelete() {
|
||||
try {
|
||||
await deleteAgent.mutateAsync(agent.name);
|
||||
await deleteAgent.mutateAsync(agent.id);
|
||||
toast.success(t.agents.deleteSuccess);
|
||||
setDeleteOpen(false);
|
||||
} catch (err) {
|
||||
@@ -72,12 +98,19 @@ export function AgentCard({ agent }: AgentCardProps) {
|
||||
}
|
||||
|
||||
async function handleSaveEdit() {
|
||||
const trimmedName = editName.trim();
|
||||
if (!trimmedName) {
|
||||
toast.error(t.agents.nameStepInvalidError ?? "名称不能为空");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await updateAgent.mutateAsync({
|
||||
name: agent.name,
|
||||
id: agent.id,
|
||||
request: {
|
||||
name: trimmedName,
|
||||
description: editDescription,
|
||||
soul: editSoul,
|
||||
published: editPublished,
|
||||
skills: selectedSkills,
|
||||
},
|
||||
});
|
||||
@@ -88,6 +121,18 @@ export function AgentCard({ agent }: AgentCardProps) {
|
||||
}
|
||||
}
|
||||
|
||||
async function handleTogglePublished() {
|
||||
try {
|
||||
await updateAgent.mutateAsync({
|
||||
id: agent.id,
|
||||
request: { published: !agent.published },
|
||||
});
|
||||
toast.success(agent.published ? "已取消发布" : "已发布");
|
||||
} catch (err) {
|
||||
toast.error(err instanceof Error ? err.message : String(err));
|
||||
}
|
||||
}
|
||||
|
||||
function toggleSkill(skillName: string) {
|
||||
setSelectedSkills((prev) => {
|
||||
if (prev.includes(skillName)) {
|
||||
@@ -119,6 +164,39 @@ export function AgentCard({ agent }: AgentCardProps) {
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex shrink-0 items-center gap-1">
|
||||
{isBuiltin && (
|
||||
<Badge variant="secondary" className="text-xs">
|
||||
内置
|
||||
</Badge>
|
||||
)}
|
||||
{isOwned && (
|
||||
<Badge
|
||||
variant="default"
|
||||
className="bg-emerald-500/15 text-emerald-700 hover:bg-emerald-500/15 dark:text-emerald-300 text-xs"
|
||||
>
|
||||
我的
|
||||
</Badge>
|
||||
)}
|
||||
{isFromOthers && (
|
||||
<Badge
|
||||
variant="outline"
|
||||
className="border-blue-500/30 text-blue-700 dark:text-blue-300 text-xs"
|
||||
title={agent.user_id ? `来自用户 ${agent.user_id}` : "已发布"}
|
||||
>
|
||||
来自他人
|
||||
</Badge>
|
||||
)}
|
||||
{isOwned && agent.published && (
|
||||
<Badge
|
||||
variant="outline"
|
||||
className="border-sky-500/30 text-sky-700 dark:text-sky-300 text-xs"
|
||||
title="该智能体已对所有用户可见"
|
||||
>
|
||||
已发布
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{agent.description && (
|
||||
<CardDescription className="mt-2 line-clamp-2 text-sm">
|
||||
@@ -157,41 +235,61 @@ export function AgentCard({ agent }: AgentCardProps) {
|
||||
<MessageSquareIcon className="mr-1.5 h-3.5 w-3.5" />
|
||||
{t.agents.chat}
|
||||
</Button>
|
||||
<div className="flex gap-1">
|
||||
<Button
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
className="h-8 w-8 shrink-0"
|
||||
onClick={() => setEditOpen(true)}
|
||||
title="编辑智能体"
|
||||
>
|
||||
<SquarePenIcon className="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
<Button
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
className="text-destructive hover:text-destructive h-8 w-8 shrink-0"
|
||||
onClick={() => setDeleteOpen(true)}
|
||||
title={t.agents.delete}
|
||||
>
|
||||
<Trash2Icon className="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
</div>
|
||||
{isOwned && (
|
||||
<div className="flex gap-1">
|
||||
<Button
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
className="h-8 w-8 shrink-0"
|
||||
onClick={() => void handleTogglePublished()}
|
||||
disabled={updateAgent.isPending}
|
||||
title={agent.published ? "取消发布" : "发布"}
|
||||
>
|
||||
{agent.published ? (
|
||||
<EyeOffIcon className="h-3.5 w-3.5" />
|
||||
) : (
|
||||
<EyeIcon className="h-3.5 w-3.5" />
|
||||
)}
|
||||
</Button>
|
||||
<Button
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
className="h-8 w-8 shrink-0"
|
||||
onClick={() => setEditOpen(true)}
|
||||
title="编辑智能体"
|
||||
>
|
||||
<SquarePenIcon className="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
<Button
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
className="text-destructive hover:text-destructive h-8 w-8 shrink-0"
|
||||
onClick={() => setDeleteOpen(true)}
|
||||
title={t.agents.delete}
|
||||
>
|
||||
<Trash2Icon className="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</CardFooter>
|
||||
</Card>
|
||||
|
||||
<Dialog open={editOpen} onOpenChange={setEditOpen}>
|
||||
<DialogContent className="max-h-[85vh] overflow-y-auto sm:max-w-2xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>编辑智能体:{agent.name}</DialogTitle>
|
||||
<DialogTitle>编辑智能体:{agent.name}</DialogTitle>
|
||||
<DialogDescription>
|
||||
可以修改描述、技能白名单和 SOUL 内容。
|
||||
可以修改名称、描述、技能白名单、SOUL 内容与发布状态。
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="space-y-4 py-2">
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium">名称</label>
|
||||
<Input value={agent.name} disabled />
|
||||
<Input
|
||||
value={editName}
|
||||
onChange={(e) => setEditName(e.target.value)}
|
||||
placeholder="输入智能体名称(允许中文)"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium">描述</label>
|
||||
@@ -238,7 +336,7 @@ export function AgentCard({ agent }: AgentCardProps) {
|
||||
)}
|
||||
</div>
|
||||
<p className="text-muted-foreground text-xs">
|
||||
不选任何技能会保存为无技能(skills = [])。
|
||||
不选任何技能会保存为无技能(skills = [])。
|
||||
</p>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
@@ -250,6 +348,18 @@ export function AgentCard({ agent }: AgentCardProps) {
|
||||
className="min-h-40 font-mono text-xs"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center justify-between rounded-md border p-3">
|
||||
<div>
|
||||
<p className="text-sm font-medium">公开发布</p>
|
||||
<p className="text-muted-foreground text-xs">
|
||||
打开后,该智能体对所有用户可见。
|
||||
</p>
|
||||
</div>
|
||||
<Switch
|
||||
checked={editPublished}
|
||||
onCheckedChange={setEditPublished}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button
|
||||
|
||||
@@ -59,7 +59,7 @@ export function AgentGallery() {
|
||||
) : (
|
||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
|
||||
{agents.map((agent) => (
|
||||
<AgentCard key={agent.name} agent={agent} />
|
||||
<AgentCard key={agent.id} agent={agent} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -62,10 +62,10 @@ export function RecentChatList() {
|
||||
const { t } = useI18n();
|
||||
const router = useRouter();
|
||||
const pathname = usePathname();
|
||||
const { thread_id: threadIdFromPath, agent_name: agentNameFromPath } =
|
||||
const { thread_id: threadIdFromPath, agent_id: agentIdFromPath } =
|
||||
useParams<{
|
||||
thread_id: string;
|
||||
agent_name?: string;
|
||||
agent_id?: string;
|
||||
}>();
|
||||
const { data: threads = [] } = useThreads();
|
||||
const { mutate: deleteThread } = useDeleteThread();
|
||||
@@ -82,7 +82,7 @@ export function RecentChatList() {
|
||||
if (threadId === threadIdFromPath) {
|
||||
const threadIndex = threads.findIndex((t) => t.thread_id === threadId);
|
||||
let nextThreadPath = pathOfThread("new", {
|
||||
agent_name: agentNameFromPath,
|
||||
agent_id: agentIdFromPath,
|
||||
});
|
||||
if (threadIndex > -1) {
|
||||
if (threads[threadIndex + 1]) {
|
||||
@@ -94,7 +94,7 @@ export function RecentChatList() {
|
||||
void router.push(nextThreadPath);
|
||||
}
|
||||
},
|
||||
[agentNameFromPath, deleteThread, router, threadIdFromPath, threads],
|
||||
[agentIdFromPath, deleteThread, router, threadIdFromPath, threads],
|
||||
);
|
||||
|
||||
const handleRenameClick = useCallback(
|
||||
|
||||
@@ -22,9 +22,9 @@ export async function listAgents(): Promise<Agent[]> {
|
||||
return data.agents;
|
||||
}
|
||||
|
||||
export async function getAgent(name: string): Promise<Agent> {
|
||||
const res = await apiFetch(`${getBackendBaseURL()}/api/agents/${name}`);
|
||||
if (!res.ok) throw new Error(`Agent '${name}' not found`);
|
||||
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>;
|
||||
}
|
||||
|
||||
@@ -42,10 +42,10 @@ export async function createAgent(request: CreateAgentRequest): Promise<Agent> {
|
||||
}
|
||||
|
||||
export async function updateAgent(
|
||||
name: string,
|
||||
id: string,
|
||||
request: UpdateAgentRequest,
|
||||
): Promise<Agent> {
|
||||
const res = await apiFetch(`${getBackendBaseURL()}/api/agents/${name}`, {
|
||||
const res = await apiFetch(`${getBackendBaseURL()}/api/agents/${id}`, {
|
||||
method: "PUT",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(request),
|
||||
@@ -57,8 +57,8 @@ export async function updateAgent(
|
||||
return res.json() as Promise<Agent>;
|
||||
}
|
||||
|
||||
export async function deleteAgent(name: string): Promise<void> {
|
||||
const res = await apiFetch(`${getBackendBaseURL()}/api/agents/${name}`, {
|
||||
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}`);
|
||||
|
||||
@@ -17,11 +17,11 @@ export function useAgents() {
|
||||
return { agents: data ?? [], isLoading, error };
|
||||
}
|
||||
|
||||
export function useAgent(name: string | null | undefined) {
|
||||
export function useAgent(id: string | null | undefined) {
|
||||
const { data, isLoading, error } = useQuery({
|
||||
queryKey: ["agents", name],
|
||||
queryFn: () => getAgent(name!),
|
||||
enabled: !!name,
|
||||
queryKey: ["agents", id],
|
||||
queryFn: () => getAgent(id!),
|
||||
enabled: !!id,
|
||||
});
|
||||
return { agent: data ?? null, isLoading, error };
|
||||
}
|
||||
@@ -40,15 +40,15 @@ export function useUpdateAgent() {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: ({
|
||||
name,
|
||||
id,
|
||||
request,
|
||||
}: {
|
||||
name: string;
|
||||
id: string;
|
||||
request: UpdateAgentRequest;
|
||||
}) => updateAgent(name, request),
|
||||
onSuccess: (_data, { name }) => {
|
||||
}) => updateAgent(id, request),
|
||||
onSuccess: (_data, { id }) => {
|
||||
void queryClient.invalidateQueries({ queryKey: ["agents"] });
|
||||
void queryClient.invalidateQueries({ queryKey: ["agents", name] });
|
||||
void queryClient.invalidateQueries({ queryKey: ["agents", id] });
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -56,7 +56,7 @@ export function useUpdateAgent() {
|
||||
export function useDeleteAgent() {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation({
|
||||
mutationFn: (name: string) => deleteAgent(name),
|
||||
mutationFn: (id: string) => deleteAgent(id),
|
||||
onSuccess: () => {
|
||||
void queryClient.invalidateQueries({ queryKey: ["agents"] });
|
||||
},
|
||||
|
||||
@@ -1,25 +1,34 @@
|
||||
export interface Agent {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
model: string | null;
|
||||
tool_groups: string[] | null;
|
||||
skills: string[] | null;
|
||||
user_id: string | null;
|
||||
published: boolean;
|
||||
builtin: boolean;
|
||||
created_at?: string | null;
|
||||
updated_at?: string | null;
|
||||
soul?: string | null;
|
||||
model?: string | null;
|
||||
tool_groups?: string[] | null;
|
||||
skills?: string[] | null;
|
||||
}
|
||||
|
||||
export interface CreateAgentRequest {
|
||||
name: string;
|
||||
description?: string;
|
||||
soul?: string;
|
||||
published?: boolean;
|
||||
skills?: string[] | null;
|
||||
model?: string | null;
|
||||
tool_groups?: string[] | null;
|
||||
skills?: string[] | null;
|
||||
soul?: string;
|
||||
}
|
||||
|
||||
export interface UpdateAgentRequest {
|
||||
name?: string | null;
|
||||
description?: string | null;
|
||||
soul?: string | null;
|
||||
published?: boolean | null;
|
||||
skills?: string[] | null;
|
||||
model?: string | null;
|
||||
tool_groups?: string[] | null;
|
||||
skills?: string[] | null;
|
||||
soul?: string | null;
|
||||
}
|
||||
@@ -28,6 +28,7 @@ export type ThreadStreamOptions = {
|
||||
threadId?: string | null | undefined;
|
||||
context: LocalSettings["context"];
|
||||
isMock?: boolean;
|
||||
assistantId?: string;
|
||||
onStart?: (threadId: string) => void;
|
||||
onFinish?: (state: AgentThreadState) => void;
|
||||
onToolEnd?: (event: ToolEndEvent) => void;
|
||||
@@ -139,6 +140,7 @@ export function useThreadStream({
|
||||
threadId,
|
||||
context,
|
||||
isMock,
|
||||
assistantId,
|
||||
onStart,
|
||||
onFinish,
|
||||
onToolEnd,
|
||||
@@ -204,7 +206,7 @@ export function useThreadStream({
|
||||
|
||||
const thread = useStream<AgentThreadState>({
|
||||
client: getAPIClient(isMock),
|
||||
assistantId: "lead_agent",
|
||||
assistantId: assistantId ?? "lead_agent",
|
||||
threadId: onStreamThreadId,
|
||||
reconnectOnMount: runMetadataStorageRef.current
|
||||
? () => runMetadataStorageRef.current!
|
||||
@@ -213,10 +215,11 @@ export function useThreadStream({
|
||||
onCreated(meta) {
|
||||
handleStreamStart(meta.thread_id);
|
||||
setOnStreamThreadId(meta.thread_id);
|
||||
if (context.agent_name && !isMock) {
|
||||
const metadataAgentId = context.agent_id ?? context.agent_name;
|
||||
if (metadataAgentId && !isMock) {
|
||||
void getAPIClient()
|
||||
.threads.update(meta.thread_id, {
|
||||
metadata: { agent_name: context.agent_name },
|
||||
metadata: { agent_id: metadataAgentId },
|
||||
})
|
||||
.catch(() => ({}));
|
||||
}
|
||||
|
||||
@@ -16,6 +16,8 @@ export interface AgentThreadContext extends Record<string, unknown> {
|
||||
is_plan_mode: boolean;
|
||||
subagent_enabled: boolean;
|
||||
reasoning_effort?: "minimal" | "low" | "medium" | "high";
|
||||
agent_id?: string;
|
||||
/** @deprecated kept for reading legacy thread metadata; prefer agent_id */
|
||||
agent_name?: string;
|
||||
}
|
||||
|
||||
|
||||
@@ -2,34 +2,41 @@ import type { Message } from "@langchain/langgraph-sdk";
|
||||
|
||||
import type { AgentThread, AgentThreadContext } from "./types";
|
||||
|
||||
type ThreadAgentRef = Pick<AgentThreadContext, "agent_id" | "agent_name">;
|
||||
|
||||
type ThreadRouteTarget =
|
||||
| string
|
||||
| {
|
||||
thread_id: string;
|
||||
context?: Pick<AgentThreadContext, "agent_name"> | null;
|
||||
context?: ThreadAgentRef | null;
|
||||
metadata?: Record<string, unknown> | null;
|
||||
};
|
||||
|
||||
function pickAgentRef(
|
||||
context: ThreadAgentRef | null | undefined,
|
||||
metadata: Record<string, unknown> | null | undefined,
|
||||
): string | undefined {
|
||||
if (context?.agent_id) return context.agent_id;
|
||||
if (context?.agent_name) return context.agent_name;
|
||||
const metaId = metadata?.agent_id;
|
||||
if (typeof metaId === "string" && metaId) return metaId;
|
||||
const metaName = metadata?.agent_name;
|
||||
if (typeof metaName === "string" && metaName) return metaName;
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export function pathOfThread(
|
||||
thread: ThreadRouteTarget,
|
||||
context?: Pick<AgentThreadContext, "agent_name"> | null,
|
||||
context?: ThreadAgentRef | null,
|
||||
) {
|
||||
const threadId = typeof thread === "string" ? thread : thread.thread_id;
|
||||
let agentName: string | undefined;
|
||||
if (typeof thread === "string") {
|
||||
agentName = context?.agent_name;
|
||||
} else {
|
||||
agentName = thread.context?.agent_name;
|
||||
if (!agentName) {
|
||||
const metaAgent = thread.metadata?.agent_name;
|
||||
if (typeof metaAgent === "string") {
|
||||
agentName = metaAgent;
|
||||
}
|
||||
}
|
||||
}
|
||||
const agentRef =
|
||||
typeof thread === "string"
|
||||
? pickAgentRef(context, null)
|
||||
: pickAgentRef(thread.context, thread.metadata);
|
||||
|
||||
return agentName
|
||||
? `/workspace/agents/${encodeURIComponent(agentName)}/chats/${threadId}`
|
||||
return agentRef
|
||||
? `/workspace/agents/${encodeURIComponent(agentRef)}/chats/${threadId}`
|
||||
: `/workspace/chats/${threadId}`;
|
||||
}
|
||||
|
||||
|
||||
@@ -37,11 +37,11 @@ export default function AgentChatPage() {
|
||||
const [hasStartedConversation, setHasStartedConversation] = useState(false);
|
||||
const router = useRouter();
|
||||
|
||||
const { agent_name } = useParams<{
|
||||
agent_name: string;
|
||||
const { agent_id } = useParams<{
|
||||
agent_id: string;
|
||||
}>();
|
||||
|
||||
const { agent } = useAgent(agent_name);
|
||||
const { agent } = useAgent(agent_id);
|
||||
|
||||
const { threadId, setThreadId, isNewThread, setIsNewThread } =
|
||||
useThreadChat();
|
||||
@@ -57,7 +57,8 @@ export default function AgentChatPage() {
|
||||
const { showNotification } = useNotification();
|
||||
const [thread, sendMessage] = useThreadStream({
|
||||
threadId: isNewThread ? undefined : threadId,
|
||||
context: { ...settings.context, agent_name: agent_name },
|
||||
assistantId: agent_id,
|
||||
context: { ...settings.context, agent_id },
|
||||
onStart: (createdThreadId) => {
|
||||
setThreadId(createdThreadId);
|
||||
setIsNewThread(false);
|
||||
@@ -65,7 +66,7 @@ export default function AgentChatPage() {
|
||||
history.replaceState(
|
||||
null,
|
||||
"",
|
||||
`${window.location.pathname}${window.location.search}#/workspace/agents/${agent_name}/chats/${createdThreadId}`,
|
||||
`${window.location.pathname}${window.location.search}#/workspace/agents/${agent_id}/chats/${createdThreadId}`,
|
||||
);
|
||||
},
|
||||
onFinish: (state) => {
|
||||
@@ -92,9 +93,9 @@ export default function AgentChatPage() {
|
||||
window.dispatchEvent(
|
||||
new CustomEvent("workspace:sandbox-resume-autoscroll"),
|
||||
);
|
||||
void sendMessage(threadId, message, { agent_name });
|
||||
void sendMessage(threadId, message, { agent_id });
|
||||
},
|
||||
[sendMessage, threadId, agent_name],
|
||||
[sendMessage, threadId, agent_id],
|
||||
);
|
||||
|
||||
const handleStop = useCallback(async () => {
|
||||
@@ -123,7 +124,7 @@ export default function AgentChatPage() {
|
||||
<div className="flex shrink-0 items-center gap-1.5 rounded-md border px-2 py-1">
|
||||
<BotIcon className="text-primary h-3.5 w-3.5" />
|
||||
<span className="text-xs font-medium">
|
||||
{agent?.name ?? agent_name}
|
||||
{agent?.name ?? agent_id}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -136,7 +137,7 @@ export default function AgentChatPage() {
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
onClick={() => {
|
||||
router.push(`/workspace/agents/${agent_name}/chats/new`);
|
||||
router.push(`/workspace/agents/${agent_id}/chats/new`);
|
||||
}}
|
||||
>
|
||||
<PlusSquare /> {t.agents.newChat}
|
||||
@@ -199,7 +200,7 @@ export default function AgentChatPage() {
|
||||
context={settings.context}
|
||||
extraHeader={
|
||||
isLandingState && (
|
||||
<AgentWelcome agent={agent} agentName={agent_name} />
|
||||
<AgentWelcome agent={agent} agentName={agent?.name ?? agent_id} />
|
||||
)
|
||||
}
|
||||
disabled={env.NEXT_PUBLIC_STATIC_WEBSITE_ONLY === "true"}
|
||||
|
||||
@@ -49,7 +49,6 @@ import { cn } from "@/lib/utils";
|
||||
type Step = "name" | "chat";
|
||||
type SetupAgentStatus = "idle" | "requested" | "completed";
|
||||
|
||||
const NAME_RE = /^[A-Za-z0-9-]+$/;
|
||||
const SAVE_HINT_STORAGE_KEY = "deerflow.agent-create.save-hint-seen";
|
||||
const AGENT_READ_RETRY_DELAYS_MS = [200, 500, 1_000, 2_000];
|
||||
const DEFAULT_SELECTED_SKILL_COUNT = 3;
|
||||
@@ -58,14 +57,14 @@ function wait(ms: number) {
|
||||
return new Promise((resolve) => window.setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
async function getAgentWithRetry(agentName: string) {
|
||||
async function getAgentWithRetry(agentId: string) {
|
||||
for (const delay of [0, ...AGENT_READ_RETRY_DELAYS_MS]) {
|
||||
if (delay > 0) {
|
||||
await wait(delay);
|
||||
}
|
||||
|
||||
try {
|
||||
return await getAgent(agentName);
|
||||
return await getAgent(agentId);
|
||||
} catch {
|
||||
// Retry until the write settles or the attempts are exhausted.
|
||||
}
|
||||
@@ -97,7 +96,7 @@ export default function NewAgentPage() {
|
||||
const [nameError, setNameError] = useState("");
|
||||
const [isCheckingName, setIsCheckingName] = useState(false);
|
||||
const [isCreatingAgent, setIsCreatingAgent] = useState(false);
|
||||
const [agentName, setAgentName] = useState("");
|
||||
const [agentId, setAgentId] = useState("");
|
||||
const [agent, setAgent] = useState<Agent | null>(null);
|
||||
const [showSaveHint, setShowSaveHint] = useState(false);
|
||||
const [setupAgentStatus, setSetupAgentStatus] =
|
||||
@@ -125,6 +124,7 @@ export default function NewAgentPage() {
|
||||
|
||||
const [thread, sendMessage] = useThreadStream({
|
||||
threadId: step === "chat" ? threadId : undefined,
|
||||
assistantId: agentId || undefined,
|
||||
context: {
|
||||
mode: "flash",
|
||||
is_bootstrap: true,
|
||||
@@ -135,9 +135,9 @@ export default function NewAgentPage() {
|
||||
}
|
||||
},
|
||||
onToolEnd({ name }) {
|
||||
if (name !== "setup_agent" || !agentName) return;
|
||||
if (name !== "setup_agent" || !agentId) return;
|
||||
setSetupAgentStatus("completed");
|
||||
void getAgentWithRetry(agentName).then((fetched) => {
|
||||
void getAgentWithRetry(agentId).then((fetched) => {
|
||||
if (fetched) {
|
||||
setAgent(fetched);
|
||||
return;
|
||||
@@ -171,8 +171,7 @@ export default function NewAgentPage() {
|
||||
|
||||
const handleConfirmName = useCallback(async () => {
|
||||
const trimmed = nameInput.trim();
|
||||
if (!trimmed) return;
|
||||
if (!NAME_RE.test(trimmed)) {
|
||||
if (!trimmed) {
|
||||
setNameError(t.agents.nameStepInvalidError);
|
||||
return;
|
||||
}
|
||||
@@ -185,11 +184,7 @@ export default function NewAgentPage() {
|
||||
setNameError("");
|
||||
setIsCheckingName(true);
|
||||
try {
|
||||
const result = await checkAgentName(trimmed);
|
||||
if (!result.available) {
|
||||
setNameError(t.agents.nameStepAlreadyExistsError);
|
||||
return;
|
||||
}
|
||||
await checkAgentName(trimmed);
|
||||
} catch (err) {
|
||||
if (
|
||||
err instanceof AgentNameCheckError &&
|
||||
@@ -204,14 +199,18 @@ export default function NewAgentPage() {
|
||||
setIsCheckingName(false);
|
||||
}
|
||||
|
||||
let createdId: string;
|
||||
let createdName: string;
|
||||
setIsCreatingAgent(true);
|
||||
try {
|
||||
await createAgent({
|
||||
const created = await createAgent({
|
||||
name: trimmed,
|
||||
description: "",
|
||||
skills: selectedSkills,
|
||||
soul: "",
|
||||
});
|
||||
createdId = created.id;
|
||||
createdName = created.name;
|
||||
} catch (err) {
|
||||
setNameError(
|
||||
getCreateAgentErrorMessage(
|
||||
@@ -225,17 +224,16 @@ export default function NewAgentPage() {
|
||||
setIsCreatingAgent(false);
|
||||
}
|
||||
|
||||
setAgentName(trimmed);
|
||||
setAgentId(createdId);
|
||||
setStep("chat");
|
||||
await ensureThreadExists();
|
||||
await sendMessage(threadId, {
|
||||
text: t.agents.nameStepBootstrapMessage.replace("{name}", trimmed),
|
||||
text: t.agents.nameStepBootstrapMessage.replace("{name}", createdName),
|
||||
files: [],
|
||||
});
|
||||
}, [
|
||||
nameInput,
|
||||
sendMessage,
|
||||
t.agents.nameStepAlreadyExistsError,
|
||||
t.agents.nameStepNetworkError,
|
||||
t.agents.nameStepBootstrapMessage,
|
||||
t.agents.nameStepCheckError,
|
||||
@@ -272,15 +270,15 @@ export default function NewAgentPage() {
|
||||
await sendMessage(
|
||||
threadId,
|
||||
{ text: trimmed, files: [] },
|
||||
{ agent_name: agentName },
|
||||
{ agent_id: agentId },
|
||||
);
|
||||
},
|
||||
[agentName, sendMessage, thread.isLoading, threadId, ensureThreadExists],
|
||||
[agentId, sendMessage, thread.isLoading, threadId, ensureThreadExists],
|
||||
);
|
||||
|
||||
const handleSaveAgent = useCallback(async () => {
|
||||
if (
|
||||
!agentName ||
|
||||
!agentId ||
|
||||
agent ||
|
||||
thread.isLoading ||
|
||||
setupAgentStatus !== "idle"
|
||||
@@ -294,7 +292,7 @@ export default function NewAgentPage() {
|
||||
await sendMessage(
|
||||
threadId,
|
||||
{ text: t.agents.saveCommandMessage, files: [] },
|
||||
{ agent_name: agentName },
|
||||
{ agent_id: agentId },
|
||||
{ additionalKwargs: { hide_from_ui: true } },
|
||||
);
|
||||
toast.success(t.agents.saveRequested);
|
||||
@@ -304,7 +302,7 @@ export default function NewAgentPage() {
|
||||
}
|
||||
}, [
|
||||
agent,
|
||||
agentName,
|
||||
agentId,
|
||||
sendMessage,
|
||||
setupAgentStatus,
|
||||
t.agents.saveCommandMessage,
|
||||
@@ -480,7 +478,7 @@ export default function NewAgentPage() {
|
||||
<Button
|
||||
onClick={() =>
|
||||
router.push(
|
||||
`/workspace/agents/${agentName}/chats/new`,
|
||||
`/workspace/agents/${agentId}/chats/new`,
|
||||
)
|
||||
}
|
||||
>
|
||||
|
||||
Reference in new issue
Block a user