useChat.js 8.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329
  1. import { computed, reactive } from "vue";
  2. import { api, errorMessage, jsonOptions, USER } from "../lib/api";
  3. import { readSSE } from "../lib/sse";
  4. const state = reactive({
  5. sessions: [],
  6. agents: [],
  7. messages: [],
  8. sessionId: null,
  9. agentName: "main",
  10. mode: "full",
  11. title: "新对话",
  12. input: "",
  13. attachment: null,
  14. streaming: false,
  15. loading: false,
  16. status: "正在连接",
  17. statusKind: "busy",
  18. model: "",
  19. });
  20. let controller;
  21. const pending = computed(() =>
  22. state.messages.some((message) =>
  23. message.segments?.some(
  24. (segment) => segment.type === "interrupt" && !segment.resolved,
  25. ),
  26. ),
  27. );
  28. const busy = computed(() => state.streaming || state.loading);
  29. const status = (text, kind = "ready") => {
  30. state.status = text;
  31. state.statusKind = kind;
  32. };
  33. const assistant = () => {
  34. const message = reactive({
  35. id: crypto.randomUUID(),
  36. role: "assistant",
  37. segments: [],
  38. });
  39. state.messages.push(message);
  40. return message;
  41. };
  42. const card = (message, title, content = "", kind = "") =>
  43. message.segments.push({ type: "card", title, content, kind });
  44. function handle(event, message) {
  45. const segments = message.segments;
  46. switch (event.type) {
  47. case "token": {
  48. let segment = segments.at(-1);
  49. if (segment?.type !== "text") {
  50. segment = reactive({ type: "text", content: "" });
  51. segments.push(segment);
  52. }
  53. segment.content += event.content || "";
  54. break;
  55. }
  56. case "agent_start":
  57. status(`${event.cn_agent || event.agent} 已启动`, "busy");
  58. break;
  59. case "thinking":
  60. status(event.message || "分析中", "busy");
  61. break;
  62. case "thinking_token": {
  63. let segment = segments.find((item) => item.type === "thinking");
  64. if (!segment) {
  65. segment = reactive({ type: "thinking", content: "" });
  66. segments.push(segment);
  67. }
  68. segment.content += event.content || "";
  69. break;
  70. }
  71. case "executing":
  72. status(event.message || "正在执行工具", "busy");
  73. break;
  74. case "tool_call":
  75. segments.push({
  76. type: "tool",
  77. id: event.tool_call_id || event.tool,
  78. title:
  79. event.tool === "MainFanResult"
  80. ? "整理研判报告"
  81. : event.cn_tool || event.tool,
  82. args: event.args,
  83. kind: "pending",
  84. result: null,
  85. });
  86. break;
  87. case "tool_result": {
  88. const key = event.tool_call_id || event.tool;
  89. const segment = [...segments]
  90. .reverse()
  91. .find((item) => item.type === "tool" && item.id === key);
  92. if (segment) {
  93. segment.result = event.content;
  94. segment.kind = event.error ? "error" : "done";
  95. } else
  96. segments.push({
  97. type: "tool",
  98. title: event.cn_tool || event.tool,
  99. result: event.content,
  100. kind: event.error ? "error" : "done",
  101. });
  102. break;
  103. }
  104. case "updated_todo_list": {
  105. const segment = segments.find((item) => item.type === "todos");
  106. if (segment) segment.items = event.todos || [];
  107. else segments.push({ type: "todos", items: event.todos || [] });
  108. break;
  109. }
  110. case "citations":
  111. segments.push({
  112. type: "citations",
  113. items: event.items || [],
  114. modelOnly: event.model_only,
  115. });
  116. break;
  117. case "interrupt":
  118. segments.push({
  119. type: "interrupt",
  120. event,
  121. answers: (event.ask_user?.questions || []).map(() => ""),
  122. resolved: false,
  123. busy: false,
  124. });
  125. status("等待你的确认", "busy");
  126. break;
  127. case "agent_error":
  128. case "error":
  129. message.failed = true;
  130. card(
  131. message,
  132. "执行失败",
  133. event.message || event.code || "未知错误",
  134. "error",
  135. );
  136. status("执行失败", "error");
  137. break;
  138. case "done":
  139. if (event.usage) segments.push({ type: "usage", usage: event.usage });
  140. break;
  141. }
  142. }
  143. async function refreshSessions() {
  144. try {
  145. state.sessions = (await api(`/api/sessions?user_name=${USER}`)).items || [];
  146. } catch (error) {
  147. status(error.message, "error");
  148. }
  149. }
  150. async function stream(url, body, accepted) {
  151. const message = assistant();
  152. controller = new AbortController();
  153. state.streaming = true;
  154. status("正在连接", "busy");
  155. try {
  156. const response = await fetch(url, {
  157. method: "POST",
  158. body: body instanceof FormData ? body : JSON.stringify(body),
  159. headers:
  160. body instanceof FormData ? {} : { "Content-Type": "application/json" },
  161. signal: controller.signal,
  162. });
  163. if (!response.ok) {
  164. let data;
  165. try {
  166. data = await response.json();
  167. } catch {
  168. data = null;
  169. }
  170. throw new Error(errorMessage(data, `请求失败(${response.status})`));
  171. }
  172. state.sessionId = response.headers.get("X-Session-Id") || state.sessionId;
  173. state.agentName = response.headers.get("X-Agent-Name") || state.agentName;
  174. accepted?.();
  175. await readSSE(response, (event) => handle(event, message));
  176. if (!message.failed)
  177. status(
  178. pending.value ? "等待你的确认" : "回复已完成",
  179. pending.value ? "busy" : "ready",
  180. );
  181. } catch (error) {
  182. if (error.name === "AbortError") {
  183. card(message, "已停止生成");
  184. status("已停止");
  185. } else {
  186. card(message, "请求未完成", error.message, "error");
  187. status(error.message, "error");
  188. }
  189. } finally {
  190. state.streaming = false;
  191. controller = null;
  192. await refreshSessions();
  193. }
  194. }
  195. async function send() {
  196. const text = state.input.trim();
  197. if (!text || busy.value || pending.value) return;
  198. const body = new FormData();
  199. body.append("message", text);
  200. body.append("request_id", crypto.randomUUID());
  201. body.append("mode", state.mode);
  202. body.append("user_name", USER);
  203. body.append("agent_name", state.agentName);
  204. if (state.sessionId) body.append("session_id", state.sessionId);
  205. if (state.attachment) body.append("file", state.attachment);
  206. state.messages.push({
  207. id: crypto.randomUUID(),
  208. role: "user",
  209. content: text,
  210. filename: state.attachment?.name,
  211. });
  212. if (!state.sessionId) state.title = text.slice(0, 30);
  213. state.input = "";
  214. state.attachment = null;
  215. await stream("/api/chat", body);
  216. }
  217. async function resume(segment, extra) {
  218. if (busy.value || segment.resolved || segment.busy) return;
  219. segment.busy = true;
  220. const event = segment.event;
  221. try {
  222. await stream(
  223. "/api/chat/resume",
  224. {
  225. session_id: state.sessionId,
  226. thread_id: state.sessionId,
  227. user_name: USER,
  228. interrupt_token:
  229. event.tool_approval?.interrupt_token || event.interrupt_token,
  230. ...extra,
  231. },
  232. () => {
  233. segment.resolved = true;
  234. },
  235. );
  236. } finally {
  237. segment.busy = false;
  238. }
  239. }
  240. async function openSession(session) {
  241. if (busy.value) return;
  242. state.loading = true;
  243. status("正在加载对话", "busy");
  244. try {
  245. const data = await api(
  246. `/api/chat/history/${session.id}?user_name=${USER}&limit=500`,
  247. );
  248. state.sessionId = session.id;
  249. state.title = session.title || "未命名对话";
  250. state.agentName = data.agent_name || "main";
  251. state.mode = session.mode === "plan" ? "plan" : "full";
  252. state.input = "";
  253. state.attachment = null;
  254. state.messages = (data.items || [])
  255. .filter((item) => ["user", "assistant"].includes(item.msg_role))
  256. .map((item) => ({
  257. id: item.id,
  258. role: item.msg_role,
  259. content: item.content,
  260. segments:
  261. item.msg_role === "assistant"
  262. ? [
  263. { type: "text", content: item.content || "" },
  264. ...(item.citations?.length
  265. ? [{ type: "citations", items: item.citations }]
  266. : []),
  267. ]
  268. : undefined,
  269. }));
  270. const interrupts = await api(
  271. `/api/chat/interrupt?session_id=${session.id}&user_name=${USER}`,
  272. );
  273. for (const event of interrupts?.interrupts || [])
  274. handle(event, assistant());
  275. status(
  276. pending.value ? "等待你的确认" : "对话已加载",
  277. pending.value ? "busy" : "ready",
  278. );
  279. } catch (error) {
  280. status(error.message, "error");
  281. } finally {
  282. state.loading = false;
  283. }
  284. }
  285. function newChat() {
  286. if (busy.value) return;
  287. state.sessionId = null;
  288. state.title = "新对话";
  289. state.messages = [];
  290. state.input = "";
  291. state.attachment = null;
  292. status("服务已就绪");
  293. }
  294. async function initialize() {
  295. await refreshSessions();
  296. try {
  297. const [model, agents] = await Promise.all([
  298. api("/api/model"),
  299. api("/api/business-agents"),
  300. ]);
  301. state.model = model.model || "";
  302. state.agents = agents.items || [];
  303. status("服务已就绪");
  304. } catch (error) {
  305. status(error.message, "error");
  306. }
  307. }
  308. export const useChat = () => ({
  309. state,
  310. pending,
  311. busy,
  312. send,
  313. resume,
  314. openSession,
  315. newChat,
  316. initialize,
  317. refreshSessions,
  318. stop: () => controller?.abort(),
  319. });