useChatApproval.ts 9.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244
  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. export const useChatApproval = (deps: ChatApprovalDeps) => {
  24. const {
  25. getTaskList,
  26. getCurrentTaskId,
  27. switchTask,
  28. setTaskAbortController,
  29. handleStreamEvent,
  30. scrollToBottom,
  31. scrollToBottomThrottled,
  32. refreshUsage,
  33. resetTodos,
  34. setCurrentSessionId,
  35. } = deps;
  36. // 按任务维度的状态存储
  37. const taskPendingApprovals = ref<Map<string, { sessionId: string; threadId: string }>>(new Map());
  38. // 获取当前任务的待审批状态
  39. const currentPendingApproval = computed(() => {
  40. return taskPendingApprovals.value.get(getCurrentTaskId());
  41. });
  42. // 设置任务的审批状态
  43. const setTaskPendingApproval = (taskId: string, sessionId: string, threadId: string) => {
  44. const map = new Map(taskPendingApprovals.value);
  45. map.set(taskId, { sessionId, threadId });
  46. taskPendingApprovals.value = map;
  47. };
  48. // 清除任务的审批状态
  49. const clearTaskPendingApproval = (taskId: string) => {
  50. const map = new Map(taskPendingApprovals.value);
  51. map.delete(taskId);
  52. taskPendingApprovals.value = map;
  53. };
  54. // 任务 ID 迁移(流式期间临时 id → 后端 session_id)时同步审批状态的 key,否则审批会跟丢任务
  55. const migrateTaskApproval = (fromTaskId: string, toTaskId: string) => {
  56. const entry = taskPendingApprovals.value.get(fromTaskId);
  57. if (!entry) return;
  58. const map = new Map(taskPendingApprovals.value);
  59. map.delete(fromTaskId);
  60. map.set(toTaskId, entry);
  61. taskPendingApprovals.value = map;
  62. };
  63. const findPendingApprovalTask = (): { taskId: string; approval: { sessionId: string; threadId?: string } } | null => {
  64. // 优先用当前任务 ID 查找
  65. const approval = currentPendingApproval.value;
  66. if (approval) return { taskId: getCurrentTaskId(), approval };
  67. // 遍历所有审批(可能任务 ID 已变,用 sessionId 做回退匹配)
  68. for (const [taskId, a] of taskPendingApprovals.value.entries()) {
  69. return { taskId, approval: a };
  70. }
  71. return null;
  72. };
  73. const handleAskUserSubmit = async (answers: string[]) => {
  74. // 防重复提交:如果当前任务正在流式中,忽略
  75. const currentTask = getTaskList().find((t) => t.id === getCurrentTaskId());
  76. if (currentTask?.isStreaming) return;
  77. const found = findPendingApprovalTask();
  78. // 优先从 taskPendingApprovals 获取,若被清除则从消息的 pendingApprovalData 回退
  79. let sessionId = found?.approval.sessionId;
  80. let threadId = found?.approval.threadId;
  81. const foundTaskId = found?.taskId;
  82. // 查找任务:优先用 foundTaskId,回退到 currentTaskId
  83. const task = (foundTaskId ? getTaskList().find((t) => t.id === foundTaskId) : null) || getTaskList().find((t) => t.id === getCurrentTaskId());
  84. if (!task) return;
  85. // 切换到目标任务(如果不在当前视图)
  86. if (task.id !== getCurrentTaskId()) {
  87. await switchTask(task.id);
  88. }
  89. // 单一数据源:消息只存在于任务自己的 messages 数组里(当前任务的 messages.value 就是它)
  90. const sourceArr = task.messages;
  91. const lastMsg = sourceArr[sourceArr.length - 1];
  92. if (!lastMsg || !lastMsg.isPendingApproval) return;
  93. // 若 taskPendingApprovals 已被清除,从消息的 pendingApprovalData 回退取值
  94. if (!sessionId && lastMsg.pendingApprovalData) {
  95. sessionId = lastMsg.pendingApprovalData.sessionId;
  96. threadId = lastMsg.pendingApprovalData.threadId;
  97. }
  98. if (!sessionId) return;
  99. lastMsg.isPendingApproval = false;
  100. lastMsg.pendingApprovalData = undefined;
  101. lastMsg.isLoading = true;
  102. lastMsg.generateStartTime = Date.now();
  103. task.isStreaming = true;
  104. resetTodos();
  105. const controller = new AbortController();
  106. setTaskAbortController(task.id, controller);
  107. try {
  108. const result = await chatResumeStream(
  109. {
  110. session_id: sessionId,
  111. thread_id: threadId || undefined,
  112. status: 'answered',
  113. answers,
  114. signal: controller.signal,
  115. },
  116. (data: SseEvent) => {
  117. handleStreamEvent(data, lastMsg, task.id);
  118. if (getCurrentTaskId() === task.id) scrollToBottomThrottled();
  119. }
  120. );
  121. if (result.session_id) {
  122. setCurrentSessionId(result.session_id);
  123. }
  124. } catch (error) {
  125. if ((error as Error)?.name === 'AbortError') {
  126. lastMsg.isLoading = false;
  127. lastMsg.durationMs = (lastMsg.baseDurationMs || 0) + (lastMsg.generateStartTime ? Date.now() - lastMsg.generateStartTime : 0);
  128. if (!lastMsg.content) {
  129. lastMsg.content = '已停止生成。';
  130. }
  131. } else {
  132. console.error('提交答案失败:', error);
  133. const isStreamError = error instanceof StreamError;
  134. message.error(isStreamError ? error.message : '提交失败,请重试');
  135. if (isStreamError) refreshUsage();
  136. lastMsg.isLoading = false;
  137. lastMsg.durationMs = (lastMsg.baseDurationMs || 0) + (lastMsg.generateStartTime ? Date.now() - lastMsg.generateStartTime : 0);
  138. if (!lastMsg.content) {
  139. lastMsg.content = isStreamError ? error.message : '抱歉,提交请求失败,请稍后重试。';
  140. }
  141. }
  142. } finally {
  143. setTaskAbortController(task.id, null);
  144. task.isStreaming = false;
  145. // 仅当消息不再处于审批状态时才清除(避免 resume 流返回新 interrupt 被误清)
  146. if (!lastMsg.isPendingApproval) {
  147. clearTaskPendingApproval(task.id);
  148. }
  149. if (getCurrentTaskId() === task.id) {
  150. await scrollToBottom();
  151. }
  152. }
  153. };
  154. const handleAskUserCancel = async () => {
  155. // 防重复操作:如果当前任务正在流式中,忽略
  156. const currentTask = getTaskList().find((t) => t.id === getCurrentTaskId());
  157. if (currentTask?.isStreaming) return;
  158. const found = findPendingApprovalTask();
  159. // 优先从 taskPendingApprovals 获取,若被清除则从消息的 pendingApprovalData 回退
  160. let sessionId = found?.approval.sessionId;
  161. let threadId = found?.approval.threadId;
  162. const foundTaskId = found?.taskId;
  163. // 查找任务:优先用 foundTaskId,回退到 currentTaskId
  164. const task = (foundTaskId ? getTaskList().find((t) => t.id === foundTaskId) : null) || getTaskList().find((t) => t.id === getCurrentTaskId());
  165. if (!task) return;
  166. // 切换到目标任务(如果不在当前视图)
  167. if (task.id !== getCurrentTaskId()) {
  168. await switchTask(task.id);
  169. }
  170. // 单一数据源:消息只存在于任务自己的 messages 数组里
  171. const sourceArr = task.messages;
  172. const lastMsg = sourceArr[sourceArr.length - 1];
  173. if (!lastMsg || !lastMsg.isPendingApproval) return;
  174. // 若 taskPendingApprovals 已被清除,从消息的 pendingApprovalData 回退取值
  175. if (!sessionId && lastMsg.pendingApprovalData) {
  176. sessionId = lastMsg.pendingApprovalData.sessionId;
  177. threadId = lastMsg.pendingApprovalData.threadId;
  178. }
  179. if (!sessionId) return;
  180. // 冻结累计耗时到 durationMs,避免取消后计时消失
  181. lastMsg.durationMs = (lastMsg.baseDurationMs || 0) + (lastMsg.generateStartTime ? Date.now() - lastMsg.generateStartTime : 0);
  182. lastMsg.isPendingApproval = false;
  183. lastMsg.pendingApprovalData = undefined;
  184. lastMsg.isLoading = false;
  185. lastMsg.content = (lastMsg.content ? lastMsg.content + '\n\n' : '') + '已取消提问。';
  186. clearTaskPendingApproval(task.id);
  187. try {
  188. await chatResumeStream(
  189. {
  190. session_id: sessionId,
  191. thread_id: threadId || undefined,
  192. status: 'cancelled',
  193. },
  194. () => {}
  195. );
  196. } catch (error) {
  197. console.error('取消操作失败:', error);
  198. }
  199. task.isStreaming = false;
  200. if (getCurrentTaskId() === task.id) {
  201. await scrollToBottom();
  202. }
  203. };
  204. return {
  205. setTaskPendingApproval,
  206. clearTaskPendingApproval,
  207. migrateTaskApproval,
  208. currentPendingApproval,
  209. findPendingApprovalTask,
  210. handleAskUserSubmit,
  211. handleAskUserCancel,
  212. };
  213. };