Agent 初版设计

This commit is contained in:
2026-04-29 17:15:49 +08:00
parent 2c1afdc97c
commit e5ca9e24aa
13 changed files with 1819 additions and 1255 deletions
@@ -0,0 +1,239 @@
"use client";
import { useCallback, useEffect, useRef, useState } from "react";
import { streamAgentChat } from "@/lib/chatStream";
import type { StreamEvent } from "@/lib/chatStream";
import type {
AgentArtifact,
ChatProgress,
Message,
PersistedChatState,
} from "../GlobalChatbox.types";
import { CHAT_STORAGE_KEY, createId, getInitialChatState } from "../GlobalChatbox.utils";
type UseAgentChatSessionOptions = {
onToolCall: (
event: StreamEvent & { type: "tool_call" },
options: {
assistantMessageId: string;
appendArtifact: (messageId: string, artifact: AgentArtifact) => void;
},
) => void;
onBeforeSend?: () => void;
};
const upsertProgress = (
progress: ChatProgress[] | undefined,
event: StreamEvent & { type: "progress" },
) => {
const next = [...(progress ?? [])];
const index = next.findIndex((item) => item.id === event.id);
const nextItem: ChatProgress = {
id: event.id,
phase: event.phase,
status: event.status,
title: event.title,
detail: event.detail,
};
if (index >= 0) {
next[index] = nextItem;
} else {
next.push(nextItem);
}
return next;
};
const completeRunningProgress = (progress: ChatProgress[] | undefined) =>
progress?.map((item) =>
item.status === "running" ? { ...item, status: "completed" as const } : item,
);
export const useAgentChatSession = ({
onToolCall,
onBeforeSend,
}: UseAgentChatSessionOptions) => {
const initialChatStateRef = useRef<PersistedChatState | null>(null);
if (initialChatStateRef.current === null) {
initialChatStateRef.current = getInitialChatState();
}
const [messages, setMessages] = useState<Message[]>(
initialChatStateRef.current.messages,
);
const [sessionId, setSessionId] = useState<string | undefined>(
initialChatStateRef.current.sessionId,
);
const [isStreaming, setIsStreaming] = useState(false);
const abortRef = useRef<AbortController | null>(null);
useEffect(() => {
const state: PersistedChatState = { messages, sessionId };
try {
window.localStorage.setItem(CHAT_STORAGE_KEY, JSON.stringify(state));
} catch (error) {
console.error("[GlobalChatbox] Failed to persist chat state:", error);
}
}, [messages, sessionId]);
const appendArtifact = useCallback((messageId: string, artifact: AgentArtifact) => {
setMessages((prev) =>
prev.map((message) =>
message.id === messageId
? {
...message,
artifacts: [...(message.artifacts ?? []), artifact],
}
: message,
),
);
}, []);
const sendPrompt = useCallback(
async (rawPrompt: string) => {
const prompt = rawPrompt.trim();
if (!prompt || isStreaming) return;
onBeforeSend?.();
const userId = createId();
const assistantId = createId();
setIsStreaming(true);
setMessages((prev) => [
...prev,
{ id: userId, role: "user", content: prompt },
{ id: assistantId, role: "assistant", content: "" },
]);
const controller = new AbortController();
abortRef.current = controller;
try {
await streamAgentChat({
message: prompt,
sessionId,
signal: controller.signal,
onEvent: (event) => {
if ("sessionId" in event && !sessionId && event.sessionId) {
setSessionId(event.sessionId);
}
if (event.type === "token") {
setMessages((prev) =>
prev.map((message) =>
message.id === assistantId
? {
...message,
content: message.content + event.content,
isError: false,
}
: message,
),
);
} else if (event.type === "progress") {
setMessages((prev) =>
prev.map((message) =>
message.id === assistantId
? { ...message, progress: upsertProgress(message.progress, event) }
: message,
),
);
} else if (event.type === "tool_call") {
onToolCall(event, {
assistantMessageId: assistantId,
appendArtifact,
});
} else if (event.type === "done") {
setMessages((prev) =>
prev.map((message) => {
if (message.id !== assistantId) return message;
const completedProgress = completeRunningProgress(message.progress);
if (
message.content.trim().length === 0 &&
!(message.artifacts?.length)
) {
return {
...message,
content:
"Agent 已完成处理,但没有生成文本回答。请查看过程记录,或换个更具体的问题重试。",
progress: completedProgress,
};
}
return { ...message, progress: completedProgress };
}),
);
setIsStreaming(false);
} else if (event.type === "error") {
setMessages((prev) =>
prev.map((message) =>
message.id === assistantId
? {
...message,
content: message.content || `⚠️ **错误:** ${event.message}`,
isError: true,
progress: completeRunningProgress(message.progress),
}
: message,
),
);
setIsStreaming(false);
}
},
});
} catch (error) {
if (abortRef.current?.signal.aborted) {
setMessages((prev) =>
prev.filter(
(message) =>
!(
message.id === assistantId &&
message.role === "assistant" &&
message.content.trim().length === 0 &&
!(message.artifacts?.length)
),
),
);
return;
}
setMessages((prev) =>
prev.map((message) =>
message.id === assistantId
? {
...message,
content: `⚠️ **错误:** ${String(error)}`,
isError: true,
progress: completeRunningProgress(message.progress),
}
: message,
),
);
setIsStreaming(false);
} finally {
abortRef.current = null;
setIsStreaming(false);
}
},
[appendArtifact, isStreaming, onBeforeSend, onToolCall, sessionId],
);
const abort = useCallback(() => {
abortRef.current?.abort();
setIsStreaming(false);
}, []);
const reset = useCallback(() => {
abortRef.current?.abort();
setMessages([]);
setSessionId(undefined);
setIsStreaming(false);
}, []);
return {
messages,
isStreaming,
sessionId,
sendPrompt,
abort,
reset,
};
};