import { computed, reactive } from "vue"; import { api, errorMessage, jsonOptions, USER } from "../lib/api"; import { readSSE } from "../lib/sse"; const state = reactive({ sessions: [], agents: [], messages: [], sessionId: null, agentName: "main", mode: "full", title: "新对话", input: "", attachment: null, streaming: false, loading: false, status: "正在连接", statusKind: "busy", model: "", }); let controller; const pending = computed(() => state.messages.some((message) => message.segments?.some( (segment) => segment.type === "interrupt" && !segment.resolved, ), ), ); const busy = computed(() => state.streaming || state.loading); const status = (text, kind = "ready") => { state.status = text; state.statusKind = kind; }; const assistant = () => { const message = reactive({ id: crypto.randomUUID(), role: "assistant", segments: [], }); state.messages.push(message); return message; }; const card = (message, title, content = "", kind = "") => message.segments.push({ type: "card", title, content, kind }); function handle(event, message) { const segments = message.segments; switch (event.type) { case "token": { let segment = segments.at(-1); if (segment?.type !== "text") { segment = reactive({ type: "text", content: "" }); segments.push(segment); } segment.content += event.content || ""; break; } case "agent_start": status(`${event.cn_agent || event.agent} 已启动`, "busy"); break; case "thinking": status(event.message || "分析中", "busy"); break; case "thinking_token": { let segment = segments.find((item) => item.type === "thinking"); if (!segment) { segment = reactive({ type: "thinking", content: "" }); segments.push(segment); } segment.content += event.content || ""; break; } case "executing": status(event.message || "正在执行工具", "busy"); break; case "tool_call": segments.push({ type: "tool", id: event.tool_call_id || event.tool, title: event.tool === "MainFanResult" ? "整理研判报告" : event.cn_tool || event.tool, args: event.args, kind: "pending", result: null, }); break; case "tool_result": { const key = event.tool_call_id || event.tool; const segment = [...segments] .reverse() .find((item) => item.type === "tool" && item.id === key); if (segment) { segment.result = event.content; segment.kind = event.error ? "error" : "done"; } else segments.push({ type: "tool", title: event.cn_tool || event.tool, result: event.content, kind: event.error ? "error" : "done", }); break; } case "updated_todo_list": { const segment = segments.find((item) => item.type === "todos"); if (segment) segment.items = event.todos || []; else segments.push({ type: "todos", items: event.todos || [] }); break; } case "citations": segments.push({ type: "citations", items: event.items || [], modelOnly: event.model_only, }); break; case "interrupt": segments.push({ type: "interrupt", event, answers: (event.ask_user?.questions || []).map(() => ""), resolved: false, busy: false, }); status("等待你的确认", "busy"); break; case "agent_error": case "error": message.failed = true; card( message, "执行失败", event.message || event.code || "未知错误", "error", ); status("执行失败", "error"); break; case "done": if (event.usage) segments.push({ type: "usage", usage: event.usage }); break; } } async function refreshSessions() { try { state.sessions = (await api(`/api/sessions?user_name=${USER}`)).items || []; } catch (error) { status(error.message, "error"); } } async function stream(url, body, accepted) { const message = assistant(); controller = new AbortController(); state.streaming = true; status("正在连接", "busy"); try { const response = await fetch(url, { method: "POST", body: body instanceof FormData ? body : JSON.stringify(body), headers: body instanceof FormData ? {} : { "Content-Type": "application/json" }, signal: controller.signal, }); if (!response.ok) { let data; try { data = await response.json(); } catch { data = null; } throw new Error(errorMessage(data, `请求失败(${response.status})`)); } state.sessionId = response.headers.get("X-Session-Id") || state.sessionId; state.agentName = response.headers.get("X-Agent-Name") || state.agentName; accepted?.(); await readSSE(response, (event) => handle(event, message)); if (!message.failed) status( pending.value ? "等待你的确认" : "回复已完成", pending.value ? "busy" : "ready", ); } catch (error) { if (error.name === "AbortError") { card(message, "已停止生成"); status("已停止"); } else { card(message, "请求未完成", error.message, "error"); status(error.message, "error"); } } finally { state.streaming = false; controller = null; await refreshSessions(); } } async function send() { const text = state.input.trim(); if (!text || busy.value || pending.value) return; const body = new FormData(); body.append("message", text); body.append("request_id", crypto.randomUUID()); body.append("mode", state.mode); body.append("user_name", USER); body.append("agent_name", state.agentName); if (state.sessionId) body.append("session_id", state.sessionId); if (state.attachment) body.append("file", state.attachment); state.messages.push({ id: crypto.randomUUID(), role: "user", content: text, filename: state.attachment?.name, }); if (!state.sessionId) state.title = text.slice(0, 30); state.input = ""; state.attachment = null; await stream("/api/chat", body); } async function resume(segment, extra) { if (busy.value || segment.resolved || segment.busy) return; segment.busy = true; const event = segment.event; try { await stream( "/api/chat/resume", { session_id: state.sessionId, thread_id: state.sessionId, user_name: USER, interrupt_token: event.tool_approval?.interrupt_token || event.interrupt_token, ...extra, }, () => { segment.resolved = true; }, ); } finally { segment.busy = false; } } async function openSession(session) { if (busy.value) return; state.loading = true; status("正在加载对话", "busy"); try { const data = await api( `/api/chat/history/${session.id}?user_name=${USER}&limit=500`, ); state.sessionId = session.id; state.title = session.title || "未命名对话"; state.agentName = data.agent_name || "main"; state.mode = session.mode === "plan" ? "plan" : "full"; state.input = ""; state.attachment = null; state.messages = (data.items || []) .filter((item) => ["user", "assistant"].includes(item.msg_role)) .map((item) => ({ id: item.id, role: item.msg_role, content: item.content, segments: item.msg_role === "assistant" ? [ { type: "text", content: item.content || "" }, ...(item.citations?.length ? [{ type: "citations", items: item.citations }] : []), ] : undefined, })); const interrupts = await api( `/api/chat/interrupt?session_id=${session.id}&user_name=${USER}`, ); for (const event of interrupts?.interrupts || []) handle(event, assistant()); status( pending.value ? "等待你的确认" : "对话已加载", pending.value ? "busy" : "ready", ); } catch (error) { status(error.message, "error"); } finally { state.loading = false; } } function newChat() { if (busy.value) return; state.sessionId = null; state.title = "新对话"; state.messages = []; state.input = ""; state.attachment = null; status("服务已就绪"); } async function initialize() { await refreshSessions(); try { const [model, agents] = await Promise.all([ api("/api/model"), api("/api/business-agents"), ]); state.model = model.model || ""; state.agents = agents.items || []; status("服务已就绪"); } catch (error) { status(error.message, "error"); } } export const useChat = () => ({ state, pending, busy, send, resume, openSession, newChat, initialize, refreshSessions, stop: () => controller?.abort(), });