useChatApproval.ts 9.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251
  1. import { computed, ref } from 'vue';
  2. import { message } from 'ant-design-vue';
  3. import { chatResumeStream, StreamError } from '/@/views/ventAI/manageAssistent/api';
  4. import type { Message, SseEvent, Task } from '/@/views/ventAI/manageAssistent/types/index';
  5. /**
  6. * 待审批(ask_user 中断)流程:按任务登记审批信息、提交答案续跑、取消提问。
  7. *
  8. * 审批状态按任务 id 存放;提交/取消都要走一次 resume 流,因此需要注入 SSE 事件处理、
  9. * AbortController 管理与滚动能力,保持与发送流程一致的行为。
  10. */
  11. export interface ChatApprovalDeps {
  12. getTaskList: () => Task[];
  13. getCurrentTaskId: () => string;
  14. switchTask: (taskId: string) => void | Promise<void>;
  15. setTaskAbortController: (taskId: string, controller: AbortController | null) => void;
  16. handleStreamEvent: (data: SseEvent, aiMsg: Message, taskId: string) => void;
  17. scrollToBottom: (force?: boolean) => Promise<void>;
  18. scrollToBottomThrottled: (force?: boolean) => void;
  19. refreshUsage: () => void;
  20. resetTodos: () => void;
  21. setCurrentSessionId: (sessionId: string) => void;
  22. /** 补齐消息主键:续跑完成后的回答同样依赖它才有删除入口,同时把这一轮被中断的前半段挂到消息上 */
  23. hydrateMessageIds: (sessionId: string, msgs: Message[]) => Promise<void>;
  24. }
  25. export const useChatApproval = (deps: ChatApprovalDeps) => {
  26. const {
  27. getTaskList,
  28. getCurrentTaskId,
  29. switchTask,
  30. setTaskAbortController,
  31. handleStreamEvent,
  32. scrollToBottom,
  33. scrollToBottomThrottled,
  34. refreshUsage,
  35. resetTodos,
  36. setCurrentSessionId,
  37. hydrateMessageIds,
  38. } = deps;
  39. // 按任务维度的状态存储
  40. const taskPendingApprovals = ref<Map<string, { sessionId: string; threadId: string }>>(new Map());
  41. // 获取当前任务的待审批状态
  42. const currentPendingApproval = computed(() => {
  43. return taskPendingApprovals.value.get(getCurrentTaskId());
  44. });
  45. // 设置任务的审批状态
  46. const setTaskPendingApproval = (taskId: string, sessionId: string, threadId: string) => {
  47. const map = new Map(taskPendingApprovals.value);
  48. map.set(taskId, { sessionId, threadId });
  49. taskPendingApprovals.value = map;
  50. };
  51. // 清除任务的审批状态
  52. const clearTaskPendingApproval = (taskId: string) => {
  53. const map = new Map(taskPendingApprovals.value);
  54. map.delete(taskId);
  55. taskPendingApprovals.value = map;
  56. };
  57. // 任务 ID 迁移(流式期间临时 id → 后端 session_id)时同步审批状态的 key,否则审批会跟丢任务
  58. const migrateTaskApproval = (fromTaskId: string, toTaskId: string) => {
  59. const entry = taskPendingApprovals.value.get(fromTaskId);
  60. if (!entry) return;
  61. const map = new Map(taskPendingApprovals.value);
  62. map.delete(fromTaskId);
  63. map.set(toTaskId, entry);
  64. taskPendingApprovals.value = map;
  65. };
  66. const findPendingApprovalTask = (): { taskId: string; approval: { sessionId: string; threadId?: string } } | null => {
  67. // 优先用当前任务 ID 查找
  68. const approval = currentPendingApproval.value;
  69. if (approval) return { taskId: getCurrentTaskId(), approval };
  70. // 遍历所有审批(可能任务 ID 已变,用 sessionId 做回退匹配)
  71. for (const [taskId, a] of taskPendingApprovals.value.entries()) {
  72. return { taskId, approval: a };
  73. }
  74. return null;
  75. };
  76. const handleAskUserSubmit = async (answers: string[]) => {
  77. // 防重复提交:如果当前任务正在流式中,忽略
  78. const currentTask = getTaskList().find((t) => t.id === getCurrentTaskId());
  79. if (currentTask?.isStreaming) return;
  80. const found = findPendingApprovalTask();
  81. // 优先从 taskPendingApprovals 获取,若被清除则从消息的 pendingApprovalData 回退
  82. let sessionId = found?.approval.sessionId;
  83. let threadId = found?.approval.threadId;
  84. const foundTaskId = found?.taskId;
  85. // 查找任务:优先用 foundTaskId,回退到 currentTaskId
  86. const task = (foundTaskId ? getTaskList().find((t) => t.id === foundTaskId) : null) || getTaskList().find((t) => t.id === getCurrentTaskId());
  87. if (!task) return;
  88. // 切换到目标任务(如果不在当前视图)
  89. if (task.id !== getCurrentTaskId()) {
  90. await switchTask(task.id);
  91. }
  92. // 单一数据源:消息只存在于任务自己的 messages 数组里(当前任务的 messages.value 就是它)
  93. const sourceArr = task.messages;
  94. const lastMsg = sourceArr[sourceArr.length - 1];
  95. if (!lastMsg || !lastMsg.isPendingApproval) return;
  96. // 若 taskPendingApprovals 已被清除,从消息的 pendingApprovalData 回退取值
  97. if (!sessionId && lastMsg.pendingApprovalData) {
  98. sessionId = lastMsg.pendingApprovalData.sessionId;
  99. threadId = lastMsg.pendingApprovalData.threadId;
  100. }
  101. if (!sessionId) return;
  102. lastMsg.isPendingApproval = false;
  103. lastMsg.pendingApprovalData = undefined;
  104. lastMsg.isLoading = true;
  105. lastMsg.generateStartTime = Date.now();
  106. task.isStreaming = true;
  107. resetTodos();
  108. const controller = new AbortController();
  109. setTaskAbortController(task.id, controller);
  110. try {
  111. const result = await chatResumeStream(
  112. {
  113. session_id: sessionId,
  114. thread_id: threadId || undefined,
  115. status: 'answered',
  116. answers,
  117. signal: controller.signal,
  118. },
  119. (data: SseEvent) => {
  120. handleStreamEvent(data, lastMsg, task.id);
  121. if (getCurrentTaskId() === task.id) scrollToBottomThrottled();
  122. }
  123. );
  124. if (result.session_id) {
  125. setCurrentSessionId(result.session_id);
  126. }
  127. // 续跑完成,这一轮的回答刚由后端持久化:补主键(删除按钮依赖它),
  128. // 并把本次打断落库的前半段挂到这条消息上(删除时一起删,详见 hydrateMessageIds)
  129. await hydrateMessageIds(sessionId, task.messages);
  130. } catch (error) {
  131. if ((error as Error)?.name === 'AbortError') {
  132. lastMsg.isLoading = false;
  133. lastMsg.durationMs = (lastMsg.baseDurationMs || 0) + (lastMsg.generateStartTime ? Date.now() - lastMsg.generateStartTime : 0);
  134. if (!lastMsg.content) {
  135. lastMsg.content = '已停止生成。';
  136. }
  137. } else {
  138. console.error('提交答案失败:', error);
  139. const isStreamError = error instanceof StreamError;
  140. message.error(isStreamError ? error.message : '提交失败,请重试');
  141. if (isStreamError) refreshUsage();
  142. lastMsg.isLoading = false;
  143. lastMsg.durationMs = (lastMsg.baseDurationMs || 0) + (lastMsg.generateStartTime ? Date.now() - lastMsg.generateStartTime : 0);
  144. if (!lastMsg.content) {
  145. lastMsg.content = isStreamError ? error.message : '抱歉,提交请求失败,请稍后重试。';
  146. }
  147. }
  148. } finally {
  149. setTaskAbortController(task.id, null);
  150. task.isStreaming = false;
  151. // 仅当消息不再处于审批状态时才清除(避免 resume 流返回新 interrupt 被误清)
  152. if (!lastMsg.isPendingApproval) {
  153. clearTaskPendingApproval(task.id);
  154. }
  155. if (getCurrentTaskId() === task.id) {
  156. await scrollToBottom();
  157. }
  158. }
  159. };
  160. const handleAskUserCancel = async () => {
  161. // 防重复操作:如果当前任务正在流式中,忽略
  162. const currentTask = getTaskList().find((t) => t.id === getCurrentTaskId());
  163. if (currentTask?.isStreaming) return;
  164. const found = findPendingApprovalTask();
  165. // 优先从 taskPendingApprovals 获取,若被清除则从消息的 pendingApprovalData 回退
  166. let sessionId = found?.approval.sessionId;
  167. let threadId = found?.approval.threadId;
  168. const foundTaskId = found?.taskId;
  169. // 查找任务:优先用 foundTaskId,回退到 currentTaskId
  170. const task = (foundTaskId ? getTaskList().find((t) => t.id === foundTaskId) : null) || getTaskList().find((t) => t.id === getCurrentTaskId());
  171. if (!task) return;
  172. // 切换到目标任务(如果不在当前视图)
  173. if (task.id !== getCurrentTaskId()) {
  174. await switchTask(task.id);
  175. }
  176. // 单一数据源:消息只存在于任务自己的 messages 数组里
  177. const sourceArr = task.messages;
  178. const lastMsg = sourceArr[sourceArr.length - 1];
  179. if (!lastMsg || !lastMsg.isPendingApproval) return;
  180. // 若 taskPendingApprovals 已被清除,从消息的 pendingApprovalData 回退取值
  181. if (!sessionId && lastMsg.pendingApprovalData) {
  182. sessionId = lastMsg.pendingApprovalData.sessionId;
  183. threadId = lastMsg.pendingApprovalData.threadId;
  184. }
  185. if (!sessionId) return;
  186. // 冻结累计耗时到 durationMs,避免取消后计时消失
  187. lastMsg.durationMs = (lastMsg.baseDurationMs || 0) + (lastMsg.generateStartTime ? Date.now() - lastMsg.generateStartTime : 0);
  188. lastMsg.isPendingApproval = false;
  189. lastMsg.pendingApprovalData = undefined;
  190. lastMsg.isLoading = false;
  191. lastMsg.content = (lastMsg.content ? lastMsg.content + '\n\n' : '') + '已取消提问。';
  192. clearTaskPendingApproval(task.id);
  193. try {
  194. await chatResumeStream(
  195. {
  196. session_id: sessionId,
  197. thread_id: threadId || undefined,
  198. status: 'cancelled',
  199. },
  200. () => {}
  201. );
  202. } catch (error) {
  203. console.error('取消操作失败:', error);
  204. }
  205. task.isStreaming = false;
  206. if (getCurrentTaskId() === task.id) {
  207. await scrollToBottom();
  208. }
  209. };
  210. return {
  211. setTaskPendingApproval,
  212. clearTaskPendingApproval,
  213. migrateTaskApproval,
  214. currentPendingApproval,
  215. findPendingApprovalTask,
  216. handleAskUserSubmit,
  217. handleAskUserCancel,
  218. };
  219. };