refactor: migrate from legacy session API to new rooms API for multi-agent mention support

main
sp mac bookpro 2605 2026-06-08 00:34:56 +08:00
parent b6d1c9e08d
commit 8a85c675ef
8 changed files with 76 additions and 67 deletions

View File

@ -42,21 +42,20 @@ export interface ChatHistoryResp {
} }
export const ChatAPI = { export const ChatAPI = {
history: (agentId: string, sessionId?: string) => history: (roomId: string) =>
api.get<ChatHistoryResp>(`/chat/${agentId}/messages`, { params: sessionId ? { sessionId } : {} }).then((r) => r.data), api.get<ChatHistoryResp>(`/rooms/${roomId}/messages`).then((r) => r.data),
send: (agentId: string, content: string, sessionId?: string, model?: string, imageUrls?: string[]) => send: (roomId: string, content: string, targetAgentId: string, model?: string, model_id?: string, imageUrls?: string[]) =>
api api
.post<{ user: ChatMessage; assistant: ChatMessage }>(`/chat/${agentId}/messages`, { .post<{ user: ChatMessage; assistant: ChatMessage }>(`/rooms/${roomId}/messages`, {
content, content,
sessionId, targetAgentId,
model, model,
model_id,
imageUrls imageUrls
}) })
.then((r) => r.data), .then((r) => r.data),
clear: (agentId: string, sessionId?: string) => clear: (roomId: string) =>
api.delete(`/chat/${agentId}/messages`, { params: sessionId ? { sessionId } : {} }).then((r) => r.data), api.delete(`/rooms/${roomId}/messages`).then((r) => r.data)
switchBranch: (agentId: string, userMsgId: string, branchId: string) =>
api.post(`/chat/${agentId}/messages/${userMsgId}/switch-branch`, { branchId }).then((r) => r.data)
}; };
export interface ChatAttachment { export interface ChatAttachment {

View File

@ -30,18 +30,18 @@ export interface SearchResult {
export const SessionAPI = { export const SessionAPI = {
list: (agentId: string, archived: '0' | '1' | 'all' = '0') => list: (agentId: string, archived: '0' | '1' | 'all' = '0') =>
api.get<ChatSession[]>(`/agents/${agentId}/sessions`, { params: { archived } }).then((r) => r.data), api.get<ChatSession[]>(`/agents/${agentId}/rooms`, { params: { archived } }).then((r) => r.data),
create: (agentId: string, title?: string) => api.post<ChatSession>(`/agents/${agentId}/sessions`, { title }).then((r) => r.data), create: (agentId: string, title?: string) => api.post<ChatSession>(`/agents/${agentId}/rooms`, { title: title || '新会话' }).then((r) => r.data),
rename: (agentId: string, sessionId: string, title: string) => api.put(`/agents/${agentId}/sessions/${sessionId}`, { title }).then((r) => r.data), rename: (agentId: string, sessionId: string, title: string) => api.put(`/agents/${agentId}/rooms/${sessionId}`, { title }).then((r) => r.data),
remove: (agentId: string, sessionId: string) => api.delete(`/agents/${agentId}/sessions/${sessionId}`).then((r) => r.data), remove: (agentId: string, sessionId: string) => api.delete(`/agents/${agentId}/rooms/${sessionId}`).then((r) => r.data),
archive: (agentId: string, sessionId: string, archived: boolean) => archive: (agentId: string, sessionId: string, archived: boolean) =>
api.post(`/agents/${agentId}/sessions/${sessionId}/archive`, { archived }).then((r) => r.data), api.post(`/agents/${agentId}/rooms/${sessionId}/archive`, { archived }).then((r) => r.data),
share: (agentId: string, sessionId: string, ttlHours = 0) => share: (agentId: string, sessionId: string, ttlHours = 0) =>
api.post<{ token: string; expiresAt?: number }>(`/agents/${agentId}/sessions/${sessionId}/share`, { ttlHours }).then((r) => r.data), api.post<{ token: string; expiresAt?: number }>(`/agents/${agentId}/rooms/${sessionId}/share`, { ttlHours }).then((r) => r.data),
revokeShare: (agentId: string, sessionId: string) => api.delete(`/agents/${agentId}/sessions/${sessionId}/share`).then((r) => r.data), revokeShare: (agentId: string, sessionId: string) => api.delete(`/agents/${agentId}/rooms/${sessionId}/share`).then((r) => r.data),
search: (agentId: string, q: string, opts: { includeArchived?: boolean; limit?: number } = {}) => search: (agentId: string, q: string, opts: { includeArchived?: boolean; limit?: number } = {}) =>
api api
.get<SearchResult>(`/agents/${agentId}/sessions/search`, { .get<SearchResult>(`/agents/${agentId}/rooms/search`, {
params: { params: {
q, q,
includeArchived: opts.includeArchived ? '1' : '0', includeArchived: opts.includeArchived ? '1' : '0',
@ -50,7 +50,7 @@ export const SessionAPI = {
}) })
.then((r) => r.data), .then((r) => r.data),
exportSession: (agentId: string, sessionId: string, format: 'md' | 'json' = 'md') => { exportSession: (agentId: string, sessionId: string, format: 'md' | 'json' = 'md') => {
const url = withApiBase(`/agents/${agentId}/sessions/${sessionId}/export?format=${format}`); const url = withApiBase(`/agents/${agentId}/rooms/${sessionId}/export?format=${format}`);
const a = document.createElement('a'); const a = document.createElement('a');
a.href = url; a.href = url;
a.rel = 'noopener'; a.rel = 'noopener';

View File

@ -21,21 +21,21 @@ export interface ModelOverrides {
} }
export async function streamChat( export async function streamChat(
agentId: string, roomId: string,
targetAgentId: string,
content: string, content: string,
handlers: StreamEvents, handlers: StreamEvents,
signal?: AbortSignal, signal?: AbortSignal,
sessionId?: string,
model?: string, model?: string,
modelId?: string, modelId?: string,
imageUrls?: string[] imageUrls?: string[]
) { ) {
const resp = await fetch(`https://api.hoyidata.com/aura/v1/chat/${agentId}/messages/stream`, { const resp = await fetch(`https://api.hoyidata.com/aura/v1/rooms/${roomId}/messages/stream`, {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json', Accept: 'text/event-stream' }, headers: { 'Content-Type': 'application/json', Accept: 'text/event-stream' },
body: JSON.stringify({ body: JSON.stringify({
content, content,
sessionId, targetAgentId,
model, model,
model_id: modelId, model_id: modelId,
imageUrls: imageUrls ?? [] imageUrls: imageUrls ?? []

View File

@ -75,8 +75,11 @@ export default function ChatPreview({ agent, agentId }: Props) {
setSessionId(sid); setSessionId(sid);
} }
const model = String(agent?.model || '').split(',')[0]?.trim() || undefined; const model = String(agent?.model || '').split(',')[0]?.trim() || undefined;
const modelId = model ? undefined : undefined;
const targetAgentId = agentId;
await streamChat( await streamChat(
agentId, sid,
targetAgentId,
text, text,
{ {
onMeta: (m) => setStreaming((s) => ({ ...s, retrieved: m.retrieved || [] })), onMeta: (m) => setStreaming((s) => ({ ...s, retrieved: m.retrieved || [] })),
@ -104,7 +107,7 @@ export default function ChatPreview({ agent, agentId }: Props) {
}), }),
onDone: (data) => { onDone: (data) => {
setMessages((m) => [...m.filter((x) => x.id !== tempUser.id), data.user, data.assistant]); setMessages((m) => [...m.filter((x) => x.id !== tempUser.id), data.user, data.assistant]);
setStreaming({ active: false, text: '', retrieved: [], toolCalls: [] }); setStreaming({ active: false, text: '', retrieved: [], toolCalls: [] });
scrollBottom(); scrollBottom();
}, },
onError: (errMsg) => { onError: (errMsg) => {
@ -114,8 +117,8 @@ export default function ChatPreview({ agent, agentId }: Props) {
} }
}, },
ctrl.signal, ctrl.signal,
sid, model,
model modelId
); );
} catch (e: any) { } catch (e: any) {
if (e?.name !== 'AbortError') { if (e?.name !== 'AbortError') {

View File

@ -14,7 +14,7 @@ import { useChatData } from './hooks/useChatData';
import { useChatSender } from './hooks/useChatSender'; import { useChatSender } from './hooks/useChatSender';
import { markdownToPlainText } from './utils/copy'; import { markdownToPlainText } from './utils/copy';
const lastSessionKey = (agentId: string) => `chat:lastSession:${agentId}`; const lastRoomKey = (agentId: string) => `chat:lastRoom:${agentId}`;
export default function ChatPage() { export default function ChatPage() {
const { id } = useParams(); const { id } = useParams();
@ -22,7 +22,7 @@ export default function ChatPage() {
const [searchParams, setSearchParams] = useSearchParams(); const [searchParams, setSearchParams] = useSearchParams();
const { message } = AntApp.useApp(); const { message } = AntApp.useApp();
const [sessionId, setSessionId] = useState<string | null>(null); const [roomId, setRoomId] = useState<string | null>(null);
const [highlightId, setHighlightId] = useState<string | null>(null); const [highlightId, setHighlightId] = useState<string | null>(null);
const [overrides, setOverrides] = useState<ModelOverrides>({}); const [overrides, setOverrides] = useState<ModelOverrides>({});
@ -41,9 +41,9 @@ export default function ChatPage() {
const m = searchParams.get('msg'); const m = searchParams.get('msg');
if (s) { if (s) {
setSessionId(s); setRoomId(s);
try { try {
localStorage.setItem(lastSessionKey(id), s); localStorage.setItem(lastRoomKey(id), s);
} catch { } catch {
// ignore // ignore
} }
@ -58,13 +58,13 @@ export default function ChatPage() {
const saved = (() => { const saved = (() => {
try { try {
return localStorage.getItem(lastSessionKey(id)); return localStorage.getItem(lastRoomKey(id));
} catch { } catch {
return null; return null;
} }
})(); })();
if (saved) { if (saved) {
setSessionId(saved); setRoomId(saved);
return; return;
} }
@ -72,37 +72,37 @@ export default function ChatPage() {
try { try {
const list = await SessionAPI.list(id, '0'); const list = await SessionAPI.list(id, '0');
if (list.length > 0) { if (list.length > 0) {
setSessionId(list[0].id); setRoomId(list[0].id);
return; return;
} }
const created = await SessionAPI.create(id); const created = await SessionAPI.create(id);
setSessionId(created.id); setRoomId(created.id);
} catch (e: any) { } catch (e: any) {
message.error('加载会话失败:' + (e?.message ?? e)); message.error('加载房间失败:' + (e?.message ?? e));
} }
})(); })();
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [id]); }, [id]);
useEffect(() => { useEffect(() => {
if (!id || !sessionId) return; if (!id || !roomId) return;
try { try {
localStorage.setItem(lastSessionKey(id), sessionId); localStorage.setItem(lastRoomKey(id), roomId);
} catch { } catch {
// ignore // ignore
} }
const cur = searchParams.get('session'); const cur = searchParams.get('session');
if (cur === sessionId) return; if (cur === roomId) return;
const next = new URLSearchParams(searchParams); const next = new URLSearchParams(searchParams);
next.set('session', sessionId); next.set('session', roomId);
next.delete('msg'); next.delete('msg');
setSearchParams(next, { replace: true }); setSearchParams(next, { replace: true });
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [id, sessionId]); }, [id, roomId]);
const { agent, agentList, messages, setMessages, branches, setBranches, loadMessages } = useChatData({ const { agent, agentList, messages, setMessages, branches, setBranches, loadMessages } = useChatData({
agentId: id, agentId: id,
sessionId, roomId,
highlightId, highlightId,
setHighlightId, setHighlightId,
scrollBottom, scrollBottom,
@ -115,7 +115,7 @@ export default function ChatPage() {
agentId: id, agentId: id,
agent, agent,
agentList, agentList,
sessionId, roomId,
overrides, overrides,
setOverrides: (updater) => setOverrides(updater), setOverrides: (updater) => setOverrides(updater),
messages, messages,
@ -213,9 +213,9 @@ export default function ChatPage() {
setHighlightId(null); setHighlightId(null);
setMessages(() => []); setMessages(() => []);
setBranches({}); setBranches({});
setSessionId(created.id); setRoomId(created.id);
} catch (e: any) { } catch (e: any) {
message.error('创建会话失败:' + (e?.message ?? e)); message.error('创建房间失败:' + (e?.message ?? e));
} }
}} }}
agentList={agentList} agentList={agentList}
@ -232,8 +232,8 @@ export default function ChatPage() {
setInput={(updater) => sender.setInput(updater(sender.input))} setInput={(updater) => sender.setInput(updater(sender.input))}
overrides={overrides} overrides={overrides}
setOverrides={(updater) => setOverrides(updater)} setOverrides={(updater) => setOverrides(updater)}
sessionId={sessionId} roomId={roomId}
setSessionId={setSessionId} setRoomId={setRoomId}
setHighlightId={setHighlightId} setHighlightId={setHighlightId}
sessionRefresh={sender.sessionRefresh} sessionRefresh={sender.sessionRefresh}
mcpDrawerOpen={mcpDrawerOpen} mcpDrawerOpen={mcpDrawerOpen}

View File

@ -11,8 +11,8 @@ export default function ChatDrawers(props: {
setInput: (updater: (prev: string) => string) => void; setInput: (updater: (prev: string) => string) => void;
overrides: ModelOverrides; overrides: ModelOverrides;
setOverrides: (updater: (prev: ModelOverrides) => ModelOverrides) => void; setOverrides: (updater: (prev: ModelOverrides) => ModelOverrides) => void;
sessionId: string | null; roomId: string | null;
setSessionId: (v: string | null) => void; setRoomId: (v: string | null) => void;
setHighlightId: (v: string | null) => void; setHighlightId: (v: string | null) => void;
sessionRefresh: number; sessionRefresh: number;
mcpDrawerOpen: boolean; mcpDrawerOpen: boolean;
@ -31,8 +31,8 @@ export default function ChatDrawers(props: {
setInput, setInput,
overrides, overrides,
setOverrides, setOverrides,
sessionId, roomId,
setSessionId, setRoomId,
setHighlightId, setHighlightId,
sessionRefresh, sessionRefresh,
mcpDrawerOpen, mcpDrawerOpen,
@ -165,9 +165,9 @@ export default function ChatDrawers(props: {
{agentId ? ( {agentId ? (
<SessionSidebar <SessionSidebar
agentId={agentId} agentId={agentId}
activeSessionId={sessionId} activeSessionId={roomId}
onChange={(sid, opts) => { onChange={(sid, opts) => {
setSessionId(sid); setRoomId(sid);
setHighlightId(opts?.highlightMessageId || null); setHighlightId(opts?.highlightMessageId || null);
setHistoryDrawerOpen(false); setHistoryDrawerOpen(false);
}} }}

View File

@ -5,7 +5,7 @@ import { parseAgentModels } from '../utils/agentModels';
export function useChatData(args: { export function useChatData(args: {
agentId?: string; agentId?: string;
sessionId: string | null; roomId: string | null;
highlightId: string | null; highlightId: string | null;
setHighlightId: (v: string | null) => void; setHighlightId: (v: string | null) => void;
scrollBottom: (force?: boolean) => void; scrollBottom: (force?: boolean) => void;
@ -13,7 +13,7 @@ export function useChatData(args: {
setOverrides: (updater: (prev: ModelOverrides) => ModelOverrides) => void; setOverrides: (updater: (prev: ModelOverrides) => ModelOverrides) => void;
abort: () => void; abort: () => void;
}) { }) {
const { agentId, sessionId, highlightId, setHighlightId, scrollBottom, initialScrollDoneRef, setOverrides, abort } = args; const { agentId, roomId, highlightId, setHighlightId, scrollBottom, initialScrollDoneRef, setOverrides, abort } = args;
const [agent, setAgent] = useState<Agent | null>(null); const [agent, setAgent] = useState<Agent | null>(null);
const [agentList, setAgentList] = useState<Agent[]>([]); const [agentList, setAgentList] = useState<Agent[]>([]);
const [messages, setMessages] = useState<ChatMessage[]>([]); const [messages, setMessages] = useState<ChatMessage[]>([]);
@ -48,8 +48,8 @@ export function useChatData(args: {
}; };
const loadMessages = async () => { const loadMessages = async () => {
if (!agentId || !sessionId) return; if (!roomId) return;
const his = await ChatAPI.history(agentId, sessionId || undefined); const his = await ChatAPI.history(roomId);
setMessages(Array.isArray(his.messages) ? his.messages : []); setMessages(Array.isArray(his.messages) ? his.messages : []);
setBranches(his.branches || {}); setBranches(his.branches || {});
requestAnimationFrame(() => { requestAnimationFrame(() => {
@ -92,10 +92,10 @@ export function useChatData(args: {
}, [agentId]); }, [agentId]);
useEffect(() => { useEffect(() => {
if (!agentId || !sessionId) return; if (!roomId) return;
loadMessages(); loadMessages();
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [sessionId, highlightId, agentId]); }, [roomId, highlightId, agentId]);
return { return {
agent, agent,

View File

@ -84,7 +84,7 @@ export function useChatSender(args: {
agentId?: string; agentId?: string;
agent: Agent | null; agent: Agent | null;
agentList: Agent[]; agentList: Agent[];
sessionId: string | null; roomId: string | null;
overrides: ModelOverrides; overrides: ModelOverrides;
setOverrides: (updater: (prev: ModelOverrides) => ModelOverrides) => void; setOverrides: (updater: (prev: ModelOverrides) => ModelOverrides) => void;
messages: ChatMessage[]; messages: ChatMessage[];
@ -95,7 +95,7 @@ export function useChatSender(args: {
notify: { success: (t: string) => void; error: (t: string) => void }; notify: { success: (t: string) => void; error: (t: string) => void };
abortRef: { current: AbortController | null }; abortRef: { current: AbortController | null };
}) { }) {
const { agentId, agent, agentList, sessionId, overrides, setOverrides, setBranches, loadMessages, scrollBottom, notify, abortRef } = args; const { agentId, agent, agentList, roomId, overrides, setOverrides, setBranches, loadMessages, scrollBottom, notify, abortRef } = args;
const [input, setInput] = useState(''); const [input, setInput] = useState('');
const [sending, setSending] = useState(false); const [sending, setSending] = useState(false);
const [useStream, setUseStream] = useState(true); const [useStream, setUseStream] = useState(true);
@ -153,7 +153,12 @@ export function useChatSender(args: {
const content = attText ? `${text}\n\n${attText}` : text; const content = attText ? `${text}\n\n${attText}` : text;
try { try {
if (!roomId) {
notify.error('房间未初始化');
return;
}
await streamChat( await streamChat(
roomId,
targetAgentId, targetAgentId,
content, content,
{ {
@ -230,7 +235,6 @@ export function useChatSender(args: {
} }
}, },
ctrl.signal, ctrl.signal,
sessionId,
targetModel, targetModel,
targetModelId, targetModelId,
imageUrls imageUrls
@ -253,8 +257,8 @@ export function useChatSender(args: {
const handleSendNonStream = async (text: string) => { const handleSendNonStream = async (text: string) => {
if (!agentId) return; if (!agentId) return;
if (!sessionId) { if (!roomId) {
notify.error('会话未初始化,请稍后重试'); notify.error('房间未初始化,请稍后重试');
return; return;
} }
const tempUser: ChatMessage = { id: 'tmp-' + Date.now(), role: 'user', content: text, createdAt: Date.now() }; const tempUser: ChatMessage = { id: 'tmp-' + Date.now(), role: 'user', content: text, createdAt: Date.now() };
@ -267,17 +271,20 @@ export function useChatSender(args: {
// 如果提及了其他 Agent使用该 Agent 的第一个模型 // 如果提及了其他 Agent使用该 Agent 的第一个模型
let targetModel: string; let targetModel: string;
let targetModelId: string;
if (targetAgent && targetAgent.id !== agentId) { if (targetAgent && targetAgent.id !== agentId) {
const models = parseAgentModels(targetAgent.model); const models = parseAgentModels(targetAgent.model);
targetModel = models[0]?.name || ''; targetModel = models[0]?.name || '';
targetModelId = models[0]?.id || '';
} else { } else {
targetModel = overrides.model || agentModels[0]?.name || ''; targetModel = overrides.model || agentModels[0]?.name || '';
targetModelId = overrides.model_id || agentModels[0]?.id || '';
} }
const attText = buildAttachmentsText(attachments); const attText = buildAttachmentsText(attachments);
const content = attText ? `${text}\n\n${attText}` : text; const content = attText ? `${text}\n\n${attText}` : text;
try { try {
const res = await ChatAPI.send(targetAgentId, content, sessionId, targetModel, imageUrls); const res = await ChatAPI.send(roomId, content, targetAgentId, targetModel, targetModelId, imageUrls);
args.setMessages((m) => [...(m || []).filter((x) => x.id !== tempUser.id), res.user, res.assistant]); args.setMessages((m) => [...(m || []).filter((x) => x.id !== tempUser.id), res.user, res.assistant]);
setSessionRefresh((t) => t + 1); setSessionRefresh((t) => t + 1);
setAttachments([]); setAttachments([]);
@ -309,11 +316,11 @@ export function useChatSender(args: {
const handleClear = async () => { const handleClear = async () => {
if (!agentId) return; if (!agentId) return;
if (!sessionId) { if (!roomId) {
notify.error('会话未初始化,请稍后重试'); notify.error('房间未初始化,请稍后重试');
return; return;
} }
await ChatAPI.clear(agentId, sessionId); await ChatAPI.clear(roomId);
args.setMessages(() => []); args.setMessages(() => []);
setBranches({}); setBranches({});
notify.success('对话已清空'); notify.success('对话已清空');