useFlowChartData.ts 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203
  1. import { useCallback, useEffect, useMemo, useState } from "react";
  2. import { useWebSocket } from "../../../hooks/useWebSocket";
  3. import type { Goal } from "../../../types/goal";
  4. import type { Message } from "../../../types/message";
  5. // WebSocket 数据解析与状态聚合(goals + msgGroups)
  6. const isRecord = (value: unknown): value is Record<string, unknown> =>
  7. !!value && typeof value === "object" && !Array.isArray(value);
  8. const isGoalLike = (value: unknown): value is Partial<Goal> & { id: string } =>
  9. isRecord(value) && typeof value.id === "string";
  10. const isMessage = (value: unknown): value is Message =>
  11. isRecord(value) &&
  12. (typeof value.id === "string" || typeof (value as { message_id?: string }).message_id === "string");
  13. const buildSubGoals = (flatGoals: Goal[]): Goal[] => {
  14. const nodeMap = new Map<string, Goal>();
  15. flatGoals.forEach((goal) => {
  16. nodeMap.set(goal.id, { ...goal, sub_goals: [] });
  17. });
  18. flatGoals.forEach((goal) => {
  19. const parentId = typeof goal.parent_id === "string" && goal.parent_id ? goal.parent_id : undefined;
  20. if (!parentId) return;
  21. const parent = nodeMap.get(parentId);
  22. const child = nodeMap.get(goal.id);
  23. if (!parent || !child) return;
  24. if (!Array.isArray(parent.sub_goals)) parent.sub_goals = [];
  25. parent.sub_goals.push(child);
  26. });
  27. return flatGoals.map((goal) => {
  28. const node = nodeMap.get(goal.id);
  29. if (!node) return goal;
  30. if (Array.isArray(node.sub_goals) && node.sub_goals.length === 0) {
  31. delete node.sub_goals;
  32. }
  33. return node;
  34. });
  35. };
  36. // FlowChart 专用数据 Hook:处理实时事件并聚合消息组
  37. export const useFlowChartData = (traceId: string | null, initialGoals: Goal[]) => {
  38. const [goals, setGoals] = useState<Goal[]>(initialGoals);
  39. const [messages, setMessages] = useState<Message[]>([]);
  40. const [msgGroups, setMsgGroups] = useState<Record<string, Message[]>>({});
  41. const messageSortKey = useCallback((message: Message): number => {
  42. const mid =
  43. typeof message.message_id === "string"
  44. ? message.message_id
  45. : typeof message.id === "string"
  46. ? message.id
  47. : undefined;
  48. if (!mid) return 0;
  49. if (!mid.includes("-")) return 0;
  50. const suffix = mid.slice(mid.lastIndexOf("-") + 1);
  51. const num = Number.parseInt(suffix, 10);
  52. return Number.isFinite(num) ? num : 0;
  53. }, []);
  54. const updateMessageGroups = useCallback(
  55. (message: Message) => {
  56. const groupKey = typeof message.goal_id === "string" && message.goal_id ? message.goal_id : "START";
  57. if (groupKey === "START") {
  58. setGoals((prev) => {
  59. if (prev.some((g) => g.id === "START")) return prev;
  60. const startGoal: Goal = {
  61. id: "START",
  62. description: "START",
  63. status: "completed",
  64. created_at: "",
  65. };
  66. return [startGoal, ...prev];
  67. });
  68. }
  69. setMsgGroups((prev) => {
  70. const existing = prev[groupKey] ? [...prev[groupKey]] : [];
  71. existing.push(message);
  72. existing.sort((a, b) => messageSortKey(a) - messageSortKey(b));
  73. return { ...prev, [groupKey]: existing };
  74. });
  75. },
  76. [messageSortKey],
  77. );
  78. useEffect(() => {
  79. setGoals(initialGoals);
  80. setMessages([]);
  81. setMsgGroups({});
  82. }, [initialGoals, traceId]);
  83. const handleWebSocketMessage = useCallback(
  84. (payload: unknown) => {
  85. const raw = isRecord(payload) ? payload : {};
  86. const event = (typeof raw.event === "string" && raw.event) || (typeof raw.type === "string" && raw.type) || "";
  87. const data = isRecord(raw.data) ? raw.data : raw;
  88. if (event === "connected") {
  89. const trace = isRecord(data.trace) ? data.trace : undefined;
  90. const rawTrace = isRecord(raw.trace) ? raw.trace : undefined;
  91. const goalTree =
  92. (isRecord(data.goal_tree) ? data.goal_tree : undefined) ||
  93. (trace && isRecord(trace.goal_tree) ? trace.goal_tree : undefined) ||
  94. (isRecord(raw.goal_tree) ? raw.goal_tree : undefined) ||
  95. (rawTrace && isRecord(rawTrace.goal_tree) ? rawTrace.goal_tree : undefined) ||
  96. {};
  97. const goalList = isRecord(goalTree) ? goalTree.goals : undefined;
  98. const nextGoals = Array.isArray(goalList) ? (goalList as Goal[]) : [];
  99. setGoals((prev) => {
  100. const mergedFlat = nextGoals.map((ng) => {
  101. const existing = prev.find((p) => p.id === ng.id);
  102. if (!existing) return ng;
  103. const merged: Goal = { ...existing, ...ng };
  104. if (existing.sub_trace_ids && !merged.sub_trace_ids) {
  105. merged.sub_trace_ids = existing.sub_trace_ids;
  106. }
  107. if (existing.agent_call_mode && !merged.agent_call_mode) {
  108. merged.agent_call_mode = existing.agent_call_mode;
  109. }
  110. return merged;
  111. });
  112. return buildSubGoals(mergedFlat);
  113. });
  114. return;
  115. }
  116. if (event === "goal_added") {
  117. const goal = isGoalLike(data.goal) ? data.goal : isGoalLike(raw.goal) ? raw.goal : null;
  118. if (!goal) return;
  119. setGoals((prev: Goal[]) => {
  120. const next = [...prev];
  121. const idx = next.findIndex((g) => g.id === goal.id);
  122. if (idx >= 0) {
  123. const existing = next[idx];
  124. const merged = { ...existing, ...goal };
  125. // 保留 sub_trace_ids,如果 WebSocket 数据中缺失但本地已有
  126. if (existing.sub_trace_ids && !merged.sub_trace_ids) {
  127. merged.sub_trace_ids = existing.sub_trace_ids;
  128. }
  129. if (existing.agent_call_mode && !merged.agent_call_mode) {
  130. merged.agent_call_mode = existing.agent_call_mode;
  131. }
  132. next[idx] = merged;
  133. return buildSubGoals(next);
  134. }
  135. next.push(goal as Goal);
  136. return buildSubGoals(next);
  137. });
  138. return;
  139. }
  140. if (event === "goal_updated") {
  141. const goalId =
  142. (typeof data.goal_id === "string" ? data.goal_id : undefined) ||
  143. (isRecord(data.goal) && typeof data.goal.id === "string" ? data.goal.id : undefined) ||
  144. (typeof raw.goal_id === "string" ? raw.goal_id : undefined);
  145. const updates = isRecord(data.updates) ? data.updates : isRecord(raw.updates) ? raw.updates : {};
  146. if (!goalId) return;
  147. setGoals((prev: Goal[]) =>
  148. prev.map((g: Goal) => {
  149. if (g.id !== goalId) return g;
  150. const next: Goal = { ...g };
  151. if ("status" in updates) {
  152. const status = updates.status;
  153. if (typeof status === "string") {
  154. next.status = status as Goal["status"];
  155. }
  156. }
  157. if ("summary" in updates) {
  158. const summary = updates.summary;
  159. if (typeof summary === "string") {
  160. next.summary = summary;
  161. }
  162. }
  163. return next;
  164. }),
  165. );
  166. return;
  167. }
  168. if (event === "message_added") {
  169. const message = isMessage(data.message) ? data.message : isMessage(raw.message) ? raw.message : null;
  170. if (message) {
  171. setMessages((prev) => {
  172. const next = [...prev, message];
  173. next.sort((a, b) => messageSortKey(a) - messageSortKey(b));
  174. return next;
  175. });
  176. updateMessageGroups(message);
  177. }
  178. }
  179. },
  180. [messageSortKey, updateMessageGroups],
  181. );
  182. // 主 Trace 连接
  183. const wsOptions = useMemo(() => ({ onMessage: handleWebSocketMessage }), [handleWebSocketMessage]);
  184. const { connected } = useWebSocket(traceId, wsOptions);
  185. return { goals, messages, msgGroups, connected };
  186. };