( msg: UIMessage | null | undefined, userQuery?: string | null, locale: Locale = DEFAULT_LOCALE, )
| 309 | } |
| 310 | |
| 311 | export function buildFlowGraph( |
| 312 | msg: UIMessage | null | undefined, |
| 313 | userQuery?: string | null, |
| 314 | locale: Locale = DEFAULT_LOCALE, |
| 315 | ): BuildResult { |
| 316 | if (!msg && !userQuery) return { nodes: [], edges: [] }; |
| 317 | |
| 318 | const nodeMap = new Map<string, FlowNodeData>(); |
| 319 | const orderedKeys: string[] = []; |
| 320 | const callsByKey = new Map<string, ToolCallVizData[]>(); |
| 321 | const edges: GraphEdge[] = []; |
| 322 | let edgeSeq = 0; |
| 323 | let prevKey: string | null = null; |
| 324 | let toolOrder = 0; |
| 325 | |
| 326 | const pushNode = (key: string, data: FlowNodeData) => { |
| 327 | nodeMap.set(key, data); |
| 328 | orderedKeys.push(key); |
| 329 | }; |
| 330 | |
| 331 | // 起始 Query |
| 332 | const QUERY_KEY = "__query__"; |
| 333 | if (userQuery && userQuery.trim()) { |
| 334 | const trimmed = userQuery.trim(); |
| 335 | pushNode(QUERY_KEY, { |
| 336 | key: QUERY_KEY, |
| 337 | kind: "query", |
| 338 | fileType: "unknown", |
| 339 | title: i18nT("flow.node_question", locale), |
| 340 | subtitle: trimmed.length > 80 ? trimmed.slice(0, 80) + "…" : trimmed, |
| 341 | body: trimmed, |
| 342 | hitCount: 1, |
| 343 | status: "ok", |
| 344 | order: 0, |
| 345 | toolCallIds: [], |
| 346 | }); |
| 347 | prevKey = QUERY_KEY; |
| 348 | } |
| 349 | |
| 350 | if (!msg) { |
| 351 | return finalize(orderedKeys, nodeMap, edges); |
| 352 | } |
| 353 | |
| 354 | // Pass 1:收集实际被 tool 调到的 path(用于 ghost 判断) |
| 355 | const toolCalledPaths = new Set<string>(); |
| 356 | for (const part of msg.parts) { |
| 357 | if (part.kind !== "tool-call") continue; |
| 358 | const cls = classifyCall(part.call.name, part.call.args); |
| 359 | if (cls.path) toolCalledPaths.add(cls.path); |
| 360 | } |
| 361 | |
| 362 | // Pass 2:建图 |
| 363 | let thoughtCounter = 0; |
| 364 | let ghostCounter = 0; |
| 365 | const ghostKeyByPath = new Map<string, string>(); // path → ghost node key(去重) |
| 366 | |
| 367 | /** |
| 368 | * 并行检测:两个 tool-call 之间没有任何 text part(或 text 只是空白) |
no test coverage detected