import { useMemo, useState } from 'react'; import type { Agent, BranchInfo, ChatAttachment, ChatMessage, ModelOverrides, RetrievedSnippet, ToolCallTrace } from '../../../api'; import { ChatAPI, ChatAttachmentsAPI, ImageAPI, regenerateMessage, streamChat } from '../../../api'; import { buildAttachmentsText } from '../utils/attachments'; import { parseAgentModels } from '../utils/agentModels'; export interface StreamingState { active: boolean; reasoningText: string; answerText: string; errorMessage: string | null; retryInfo: any | null; retrieved: RetrievedSnippet[]; toolCalls: ToolCallTrace[]; } export function useChatSender(args: { agentId?: string; agent: Agent | null; sessionId: string | null; overrides: ModelOverrides; setOverrides: (updater: (prev: ModelOverrides) => ModelOverrides) => void; messages: ChatMessage[]; setMessages: (updater: (prev: ChatMessage[]) => ChatMessage[]) => void; setBranches: (v: Record) => void; loadMessages: () => Promise; scrollBottom: (force?: boolean) => void; notify: { success: (t: string) => void; error: (t: string) => void }; abortRef: { current: AbortController | null }; }) { const { agentId, agent, sessionId, overrides, setOverrides, setBranches, loadMessages, scrollBottom, notify, abortRef } = args; const [input, setInput] = useState(''); const [sending, setSending] = useState(false); const [useStream, setUseStream] = useState(true); const [attachments, setAttachments] = useState([]); const [imageUrls, setImageUrls] = useState([]); const [uploadingAtt, setUploadingAtt] = useState(false); const [streaming, setStreaming] = useState({ active: false, reasoningText: '', answerText: '', errorMessage: null, retryInfo: null, retrieved: [], toolCalls: [] }); const [sessionRefresh, setSessionRefresh] = useState(0); const agentModels = useMemo(() => parseAgentModels(agent?.model), [agent?.model]); const modelOptions = useMemo(() => agentModels.map((model) => ({ value: model.id, label: model.name })), [agentModels]); const activeModelValue = overrides.model_id || ''; const handleSendStream = async (text: string) => { if (!agentId) return; if (!sessionId) { notify.error('会话未初始化,请稍后重试'); return; } const tempUser: ChatMessage = { id: 'tmp-' + Date.now(), role: 'user', content: text, createdAt: Date.now() }; args.setMessages((m) => [...(m || []), tempUser]); setStreaming({ active: true, reasoningText: '', answerText: '', errorMessage: null, retryInfo: null, retrieved: [], toolCalls: [] }); scrollBottom(true); abortRef.current?.abort(); const ctrl = new AbortController(); abortRef.current = ctrl; const model = overrides.model || agentModels[0]?.name || ''; const modelId = overrides.model_id || agentModels[0]?.id || ''; const attText = buildAttachmentsText(attachments); const content = attText ? `${text}\n\n${attText}` : text; try { await streamChat( agentId, content, { onMeta: (m) => setStreaming((s) => ({ ...s, retrieved: m.retrieved || [] })), onRetry: (data) => { setStreaming((s) => ({ ...s, retryInfo: data })); if (data?.stage === 'fallback_model' && data?.toModel) { setOverrides((o) => ({ ...o, model: String(data.toModel) })); } }, onReasoningDelta: (chunk) => setStreaming((s) => { const next = { ...s, reasoningText: s.reasoningText + chunk }; scrollBottom(); return next; }), onDelta: (chunk) => setStreaming((s) => { const next = { ...s, answerText: s.answerText + chunk }; scrollBottom(); return next; }), onToolCall: (data) => setStreaming((s) => ({ ...s, toolCalls: [...s.toolCalls, { name: data.name, args: data.args, result: { pending: true } }] })), onToolResult: (data) => setStreaming((s) => { const list = [...s.toolCalls]; for (let i = list.length - 1; i >= 0; i--) { if (list[i].name === data.name && (list[i].result as any)?.pending) { list[i] = { ...list[i], result: data.result }; break; } } return { ...s, toolCalls: list }; }), onDone: (data) => { setStreaming((s) => { const assistant: any = data.assistant; const reasoningText = assistant?.reasoning || assistant?.meta?.reasoning || assistant?.meta?.reasoningText || s.reasoningText; const nextAssistant: ChatMessage = { ...data.assistant, meta: { ...(data.assistant.meta || {}), retrieved: data.assistant.meta?.retrieved || s.retrieved, toolCalls: data.assistant.meta?.toolCalls || s.toolCalls, reasoning: reasoningText || undefined } }; args.setMessages((m) => [...(m || []).filter((x) => x.id !== tempUser.id), data.user, nextAssistant]); return { active: false, reasoningText: '', answerText: '', errorMessage: null, retryInfo: null, retrieved: [], toolCalls: [] }; }); setSessionRefresh((t) => t + 1); setAttachments([]); setImageUrls([]); scrollBottom(); }, onAborted: (data) => { const assistant: ChatMessage = { ...data.assistant, meta: { ...(data.assistant.meta || {}), aborted: true } }; args.setMessages((m) => [...(m || []).filter((x) => x.id !== tempUser.id), { ...tempUser, id: 'u-' + data.assistant.id }, assistant]); setStreaming({ active: false, reasoningText: '', answerText: '', errorMessage: null, retryInfo: null, retrieved: [], toolCalls: [] }); setSessionRefresh((t) => t + 1); loadMessages(); }, onError: (errMsg) => { notify.error('流式失败:' + errMsg); const errorMessage: ChatMessage = { id: 'error-' + Date.now(), role: 'assistant', content: `❌ 请求失败:${errMsg}`, createdAt: Date.now(), meta: { error: errMsg } }; args.setMessages((m) => [...(m || []), errorMessage]); setStreaming({ active: false, reasoningText: '', answerText: '', errorMessage: errMsg, retryInfo: null, retrieved: [], toolCalls: [] }); } }, ctrl.signal, sessionId, model, modelId, imageUrls ); } catch (e: any) { if (e?.name !== 'AbortError') { notify.error('请求失败:' + (e?.message ?? e)); const errorMessage: ChatMessage = { id: 'error-' + Date.now(), role: 'assistant', content: `❌ 请求失败:${e?.message ?? String(e)}`, createdAt: Date.now(), meta: { error: e?.message ?? String(e) } }; args.setMessages((m) => [...(m || []), errorMessage]); } setStreaming({ active: false, reasoningText: '', answerText: '', errorMessage: e?.message ?? String(e), retryInfo: null, retrieved: [], toolCalls: [] }); } }; const handleSendNonStream = async (text: string) => { if (!agentId) return; if (!sessionId) { notify.error('会话未初始化,请稍后重试'); return; } const tempUser: ChatMessage = { id: 'tmp-' + Date.now(), role: 'user', content: text, createdAt: Date.now() }; args.setMessages((m) => [...(m || []), tempUser]); scrollBottom(true); const attText = buildAttachmentsText(attachments); const content = attText ? `${text}\n\n${attText}` : text; const model = overrides.model || agentModels[0]?.name || ''; try { const res = await ChatAPI.send(agentId, content, sessionId, model, imageUrls); args.setMessages((m) => [...(m || []).filter((x) => x.id !== tempUser.id), res.user, res.assistant]); setSessionRefresh((t) => t + 1); setAttachments([]); setImageUrls([]); scrollBottom(); } catch (e: any) { notify.error('发送失败:' + (e?.message ?? e)); args.setMessages((m) => (m || []).filter((x) => x.id !== tempUser.id)); } }; const handleSend = async () => { const text = input.trim(); if (!text || !agentId || sending) return; setInput(''); setSending(true); try { if (useStream) await handleSendStream(text); else await handleSendNonStream(text); } finally { setSending(false); } }; const handleStop = () => { abortRef.current?.abort(); setSending(false); }; const handleClear = async () => { if (!agentId) return; if (!sessionId) { notify.error('会话未初始化,请稍后重试'); return; } await ChatAPI.clear(agentId, sessionId); args.setMessages(() => []); setBranches({}); notify.success('对话已清空'); }; const handleRegenerate = async (assistantId: string) => { if (!agentId || sending) return; setSending(true); setStreaming({ active: true, reasoningText: '', answerText: '', errorMessage: null, retryInfo: null, retrieved: [], toolCalls: [] }); abortRef.current?.abort(); const ctrl = new AbortController(); abortRef.current = ctrl; try { await regenerateMessage( agentId, assistantId, { onMeta: (m) => setStreaming((s) => ({ ...s, retrieved: m.retrieved || [] })), onRetry: (data) => { setStreaming((s) => ({ ...s, retryInfo: data })); if (data?.stage === 'fallback_model' && data?.toModel) { setOverrides((o) => ({ ...o, model: String(data.toModel) })); } }, onReasoningDelta: (chunk) => setStreaming((s) => { const next = { ...s, reasoningText: s.reasoningText + chunk }; scrollBottom(); return next; }), onDelta: (chunk) => setStreaming((s) => { const next = { ...s, answerText: s.answerText + chunk }; scrollBottom(); return next; }), onToolCall: (data) => setStreaming((s) => ({ ...s, toolCalls: [...s.toolCalls, { name: data.name, args: data.args, result: { pending: true } }] })), onToolResult: (data) => setStreaming((s) => { const list = [...s.toolCalls]; for (let i = list.length - 1; i >= 0; i--) { if (list[i].name === data.name && (list[i].result as any)?.pending) { list[i] = { ...list[i], result: data.result }; break; } } return { ...s, toolCalls: list }; }), onDone: () => { setStreaming({ active: false, reasoningText: '', answerText: '', errorMessage: null, retryInfo: null, retrieved: [], toolCalls: [] }); loadMessages(); }, onAborted: () => { setStreaming({ active: false, reasoningText: '', answerText: '', errorMessage: null, retryInfo: null, retrieved: [], toolCalls: [] }); loadMessages(); }, onError: (errMsg) => { notify.error('重新生成失败:' + errMsg); setStreaming({ active: false, reasoningText: '', answerText: '', errorMessage: errMsg, retryInfo: null, retrieved: [], toolCalls: [] }); loadMessages(); } }, ctrl.signal, overrides ); } finally { setSending(false); } }; const handleSwitchBranch = async (userMsgId: string, branchId: string) => { if (!agentId) return; await ChatAPI.switchBranch(agentId, userMsgId, branchId); await loadMessages(); }; const handleAttach = async (files: File[]) => { if (!files.length) return; setUploadingAtt(true); try { const images = files.filter((f) => f.type.startsWith('image/')); const docs = files.filter((f) => !f.type.startsWith('image/')); let imgN = 0; let docN = 0; if (images.length) { const r = await ImageAPI.upload(images); setImageUrls((arr) => [...arr, ...r.files.map((f) => f.url)]); imgN = r.files.length; } if (docs.length) { const r = await ChatAttachmentsAPI.upload(docs); setAttachments((a) => [...a, ...r.files]); docN = r.files.length; } const parts = []; if (imgN) parts.push(`${imgN} 张图片`); if (docN) parts.push(`${docN} 个文档`); if (parts.length) notify.success(`已附加 ${parts.join(' + ')}`); } catch (e: any) { notify.error('附件上传失败:' + (e?.message ?? e)); } finally { setUploadingAtt(false); } }; return { input, setInput, sending, useStream, setUseStream, sessionRefresh, overrides, setOverrides, attachments, setAttachments, imageUrls, setImageUrls, uploadingAtt, streaming, handleSend, handleStop, handleClear, handleRegenerate, handleSwitchBranch, handleAttach, modelOptions, activeModelValue }; }