| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329 |
- 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(),
- });
|