diff --git a/frontend-web/src/components/workspace/agents/agent-card.tsx b/frontend-web/src/components/workspace/agents/agent-card.tsx index 86f32274..bbae87fd 100644 --- a/frontend-web/src/components/workspace/agents/agent-card.tsx +++ b/frontend-web/src/components/workspace/agents/agent-card.tsx @@ -91,7 +91,7 @@ export function AgentCard({ agent }: AgentCardProps) { ]); function handleChat() { - router.push(`/workspace/agents/${agent.id}/chats/new`); + router.push(`/page/workspace/agents/${agent.id}/chats/new`); } async function handleDelete() { diff --git a/frontend-web/src/pages/NewAgentPage.tsx b/frontend-web/src/pages/NewAgentPage.tsx index 23adff1f..f60e7642 100644 --- a/frontend-web/src/pages/NewAgentPage.tsx +++ b/frontend-web/src/pages/NewAgentPage.tsx @@ -30,7 +30,6 @@ import { AgentNameCheckError, checkAgentName, createAgent, - getAgent, updateAgent, } from "@/core/agents/api"; import { useI18n } from "@/core/i18n/hooks"; @@ -40,27 +39,8 @@ import { useThreadStream } from "@/core/threads/hooks"; import { uuid } from "@/core/utils/uuid"; import { cn } from "@/lib/utils"; -const AGENT_READ_RETRY_DELAYS_MS = [200, 500, 1_000, 2_000]; const DEFAULT_SELECTED_SKILL_COUNT = 3; -function wait(ms: number) { - return new Promise((resolve) => window.setTimeout(resolve, ms)); -} - -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(agentId); - } catch { - // Retry until the file/database write settles. - } - } - return null; -} - function getCreateAgentErrorMessage( error: unknown, networkErrorMessage: string, @@ -116,19 +96,11 @@ export default function NewAgentPage() { mode: "flash", is_bootstrap: true, }, - onToolEnd({ name }) { - if (name !== "setup_agent" || !agentId) { - return; - } - void getAgentWithRetry(agentId).then((fetched) => { - if (fetched) { - setAgent(fetched); - setDescriptionInput(fetched.description ?? ""); - setSoulInput(fetched.soul ?? ""); - setSelectedSkills(fetched.skills ?? []); - toast.success("专家配置已创建"); - } - }); + onToolEnd() { + // setup_agent never actually executes in this flow — it gets + // intercepted by SetupAgentApprovalMiddleware and the form is filled + // from the approval ToolMessage payload (handleApproveSetupAgent). + // No agent record is fetched/refreshed during chat. }, }); @@ -159,10 +131,13 @@ export default function NewAgentPage() { } let name = provided; if (!name) { - // Names are display-only and may collide, so a timestamped placeholder is - // safe. Reflect it in the form so the user sees what got created. + // Names are display-only and may collide, so a timestamped placeholder + // is safe to send to the backend just to satisfy the required-field + // validation. Intentionally NOT written back into the visible input — + // the user wants the name box to stay blank while they chat, and we + // refill it later (with the model's proposed name) on setup_agent + // approval. name = `未命名专家-${Date.now().toString(36)}`; - setNameInput(name); } if (agentId) { @@ -239,63 +214,37 @@ export default function NewAgentPage() { if (!trimmed || thread.isLoading) { return; } - // Lazily create the agent on first message so the user can just start - // typing — ensureAgent generates a placeholder name when none is provided. - let activeAgentId = agentId; - if (!activeAgentId) { - const created = await ensureAgent(); - if (!created) { - return; - } - activeAgentId = created.id; - } + // Chat is purely a discussion channel. The agent record is only + // created when the user explicitly clicks Save (top-right). If the + // model proposes a config via setup_agent, our approval handler + // prefills the form locally — it does NOT persist anything. await ensureThreadExists(); - await sendMessage( - threadId, - { text: trimmed, files: [] }, - { agent_id: activeAgentId }, - ); + await sendMessage(threadId, { text: trimmed, files: [] }); }, - [agentId, ensureAgent, ensureThreadExists, sendMessage, thread.isLoading, threadId], + [ensureThreadExists, sendMessage, thread.isLoading, threadId], ); const handleApproveSetupAgent = useCallback( async (payload: SetupAgentApprovalPayload) => { - if (!agentId) { - return; - } - // Approval persists directly to the backend — no LLM round-trip. The - // backend's PUT /api/agents/{id} writes both the DB record and SOUL.md / - // config.yaml on disk, which is exactly what setup_agent_tool would have - // done if we'd let the model re-invoke it. + // Approving the model's setup_agent proposal **does not** persist + // anything — it only mirrors the proposal into the form so the user + // can review it. Creation/update happens later when the user clicks + // the top-right Save button. const proposedName = payload.name?.trim() ?? ""; const newDescription = payload.description ?? ""; const newSoul = payload.soul ?? ""; const newSkills = payload.skills ?? []; - // Prefer the model's proposed name when present; fall back to whatever - // the user already typed, or the existing agent record. - const resolvedName = proposedName || nameInput.trim() || agent?.name || agentId; - setNameInput(resolvedName); + if (proposedName) { + setNameInput(proposedName); + } setDescriptionInput(newDescription); setSoulInput(newSoul); setSelectedSkills(newSkills); setIsSkillSelectionTouched(true); - - try { - const updated = await updateAgent(agentId, { - name: resolvedName, - description: newDescription, - soul: newSoul, - skills: newSkills, - }); - setAgent(updated); - toast.success("专家配置已保存"); - } catch (error) { - toast.error(error instanceof Error ? error.message : "保存失败"); - } + toast.success("已套用模型建议,可继续编辑或点保存"); }, - [agent, agentId, nameInput], + [], ); const handleRejectSetupAgent = useCallback( @@ -326,7 +275,7 @@ export default function NewAgentPage() { @@ -336,7 +285,7 @@ export default function NewAgentPage() {