/** * 将 v6.1.2.8 的 run_context.json 转换成按 Round > 步骤 > 数据 组织的图结构 * v3: 增加 [Q] 和 [SUG] 标识前缀 */ // 得分提升阈值:与Python代码保持一致 const REQUIRED_SCORE_GAIN = 0.02; function convertV8ToGraphV2(runContext, searchResults, extractionData) { const nodes = {}; const edges = []; const iterations = {}; extractionData = extractionData || {}; // 默认为空对象 const o = runContext.o || '原始问题'; const rounds = runContext.rounds || []; // 添加原始问题根节点 const rootId = 'root_o'; nodes[rootId] = { type: 'root', query: o, level: 0, relevance_score: 1.0, strategy: '原始问题', iteration: 0, is_selected: true }; iterations[0] = [rootId]; // 处理每一轮 rounds.forEach((round, roundIndex) => { if (round.type === 'initialization') { // Round 0: 初始化阶段(新架构) const roundNum = 0; const roundId = `round_${roundNum}`; // 创建 Round 节点 nodes[roundId] = { type: 'round', query: `Round ${roundNum} (初始化)`, level: roundNum * 10, relevance_score: 0, strategy: '初始化', iteration: roundNum, is_selected: true }; edges.push({ from: rootId, to: roundId, edge_type: 'root_to_round', strategy: '初始化' }); if (!iterations[roundNum * 10]) iterations[roundNum * 10] = []; iterations[roundNum * 10].push(roundId); // 步骤1: 分段 if (round.segments && round.segments.length > 0) { const segStepId = `step_seg_r${roundNum}`; nodes[segStepId] = { type: 'step', query: `步骤1: 分段 (${round.segments.length}个segment)`, level: roundNum * 10 + 1, relevance_score: 0, strategy: '分段', iteration: roundNum, is_selected: true }; edges.push({ from: roundId, to: segStepId, edge_type: 'round_to_step', strategy: '分段' }); iterations[roundNum * 10 + 1] = [segStepId]; // 为每个 Segment 创建节点 round.segments.forEach((seg, segIndex) => { const segId = `segment_${segIndex}_r${roundNum}`; nodes[segId] = { type: 'segment', query: `[${seg.type}] ${seg.text}`, level: roundNum * 10 + 2, relevance_score: seg.score || 0, evaluationReason: seg.reason || '', strategy: seg.type, iteration: roundNum, is_selected: true, segment_type: seg.type, domain_index: seg.domain_index, domain_type: seg.type // 新增:让可视化显示类型而不是D0 }; edges.push({ from: segStepId, to: segId, edge_type: 'step_to_segment', strategy: seg.type }); if (!iterations[roundNum * 10 + 2]) iterations[roundNum * 10 + 2] = []; iterations[roundNum * 10 + 2].push(segId); // 为每个 Word 创建节点 if (seg.words && seg.words.length > 0) { seg.words.forEach((word, wordIndex) => { const wordId = `word_${word.text}_seg${segIndex}_${wordIndex}`; nodes[wordId] = { type: 'word', query: word.text, level: roundNum * 10 + 3, relevance_score: word.score || 0, evaluationReason: word.reason || '', strategy: 'Word', iteration: roundNum, is_selected: true }; edges.push({ from: segId, to: wordId, edge_type: 'segment_to_word', strategy: 'Word' }); if (!iterations[roundNum * 10 + 3]) iterations[roundNum * 10 + 3] = []; iterations[roundNum * 10 + 3].push(wordId); }); } }); } // 步骤2: 域内组词 if (round.step2_domain_combinations && round.step2_domain_combinations.length > 0) { const combStepId = `step_comb_r${roundNum}`; nodes[combStepId] = { type: 'step', query: `步骤2: 域内组词 (${round.step2_domain_combinations.length}个组合)`, level: roundNum * 10 + 1, relevance_score: 0, strategy: '域内组词', iteration: roundNum, is_selected: true }; edges.push({ from: roundId, to: combStepId, edge_type: 'round_to_step', strategy: '域内组词' }); if (!iterations[roundNum * 10 + 1]) iterations[roundNum * 10 + 1] = []; iterations[roundNum * 10 + 1].push(combStepId); // 为每个域内组合创建节点 round.step2_domain_combinations.forEach((comb, combIndex) => { const combId = `comb_${comb.text}_r${roundNum}_${combIndex}`; const sourceWordsStr = comb.source_words ? comb.source_words.map(words => words.join(',')).join(' + ') : ''; nodes[combId] = { type: 'domain_combination', query: `${comb.text} ${comb.type_label}`, level: roundNum * 10 + 2, relevance_score: comb.score || 0, evaluationReason: comb.reason || '', strategy: '域内组合', iteration: roundNum, is_selected: true, type_label: comb.type_label, source_words: comb.source_words, source_segment: comb.source_segment, domain_index: comb.domain_index }; edges.push({ from: combStepId, to: combId, edge_type: 'step_to_comb', strategy: '域内组合' }); if (!iterations[roundNum * 10 + 2]) iterations[roundNum * 10 + 2] = []; iterations[roundNum * 10 + 2].push(combId); }); } } else { // 普通轮次 const roundNum = round.round_num; const roundId = `round_${roundNum}`; // 创建 Round 节点 nodes[roundId] = { type: 'round', query: `Round ${roundNum}`, level: roundNum * 10, // 使用10的倍数作为层级 relevance_score: 0, strategy: `第${roundNum}轮`, iteration: roundNum, is_selected: true }; edges.push({ from: rootId, to: roundId, edge_type: 'root_to_round', strategy: `第${roundNum}轮` }); if (!iterations[roundNum * 10]) iterations[roundNum * 10] = []; iterations[roundNum * 10].push(roundId); // 步骤1: 请求&评估推荐词 if (round.sug_details && Object.keys(round.sug_details).length > 0) { const sugStepId = `step_sug_r${roundNum}`; const totalSugs = Object.values(round.sug_details).reduce((sum, list) => sum + list.length, 0); nodes[sugStepId] = { type: 'step', query: `步骤1: 请求&评估推荐词 (${totalSugs}个)`, level: roundNum * 10 + 1, relevance_score: 0, strategy: '请求&评估推荐词', iteration: roundNum, is_selected: true }; edges.push({ from: roundId, to: sugStepId, edge_type: 'round_to_step', strategy: '推荐词' }); iterations[roundNum * 10].push(sugStepId); // 为每个 Q 创建节点 Object.keys(round.sug_details).forEach((qText, qIndex) => { // 从q_list_1中查找对应的q获取分数和理由 // Round 0: 从q_list_1查找; Round 1+: 从input_queries查找 let qData = {}; if (roundNum === 0) { qData = round.q_list_1?.find(q => q.text === qText) || {}; } else { // 从当前轮的input_queries中查找 qData = round.input_queries?.find(q => q.text === qText) || {}; } const qId = `q_${qText}_r${roundNum}_${qIndex}`; nodes[qId] = { type: 'q', query: '[Q] ' + qText, level: roundNum * 10 + 2, relevance_score: qData.score || 0, evaluationReason: qData.reason || '', strategy: 'Query', iteration: roundNum, is_selected: true, type_label: qData.type_label || qData.typeLabel || '', domain_index: qData.domain_index, domain_type: qData.domain_type || '' }; edges.push({ from: sugStepId, to: qId, edge_type: 'step_to_q', strategy: 'Query' }); if (!iterations[roundNum * 10 + 2]) iterations[roundNum * 10 + 2] = []; iterations[roundNum * 10 + 2].push(qId); // 为每个 Q 的 sug 创建节点 const sugs = round.sug_details[qText] || []; const qScore = qData.score || 0; // 获取父Q的得分 sugs.forEach((sug, sugIndex) => { const sugScore = sug.score || 0; // 比较得分决定颜色:SUG得分 >= Q得分 + 0.05 → 绿色,否则 → 红色 const scoreColor = (sugScore >= qScore + REQUIRED_SCORE_GAIN) ? '#22c55e' : '#ef4444'; const sugId = `sug_${sug.text}_r${roundNum}_q${qIndex}_${sugIndex}`; nodes[sugId] = { type: 'sug', query: '[SUG] ' + sug.text, level: roundNum * 10 + 3, relevance_score: sugScore, evaluationReason: sug.reason || '', strategy: '推荐词', iteration: roundNum, is_selected: true, scoreColor: scoreColor, // 新增:用于控制文字颜色 parentQScore: qScore // 新增:保存父Q得分用于调试 }; edges.push({ from: qId, to: sugId, edge_type: 'q_to_sug', strategy: '推荐词' }); if (!iterations[roundNum * 10 + 3]) iterations[roundNum * 10 + 3] = []; iterations[roundNum * 10 + 3].push(sugId); }); }); } // 步骤2: 域内组词(Round 1+) // 兼容旧字段名 domain_combinations_top10 const domainCombinations = round.domain_combinations || round.domain_combinations_top10 || []; if (domainCombinations.length > 0) { const combStepId = `step_comb_r${roundNum}`; nodes[combStepId] = { type: 'step', query: roundNum === 1 ? `步骤2: 域内组合 (${domainCombinations.length}个组合)` : `步骤2: 跨${roundNum}个域组合 (${domainCombinations.length}个组合)`, level: roundNum * 10 + 1, relevance_score: 0, strategy: '域内组词', iteration: roundNum, is_selected: true }; edges.push({ from: roundId, to: combStepId, edge_type: 'round_to_step', strategy: '域内组词' }); iterations[roundNum * 10].push(combStepId); // 为每个域内组合创建节点 domainCombinations.forEach((comb, combIndex) => { const combId = `comb_${comb.text}_r${roundNum}_${combIndex}`; const domainsStr = comb.domains ? comb.domains.map(d => `D${d}`).join(',') : ''; const wordDetails = comb.source_word_details || []; const isAboveSources = comb.is_above_source_scores || false; const scoreColor = wordDetails.length > 0 ? (isAboveSources ? '#22c55e' : '#ef4444') : null; nodes[combId] = { type: 'domain_combination', query: `${comb.text}`, // 移除 type_label,稍后在UI中单独显示 level: roundNum * 10 + 2, relevance_score: comb.score || 0, evaluationReason: comb.reason || '', strategy: '域内组合', iteration: roundNum, is_selected: true, type_label: comb.type_label || '', source_words: comb.source_words || [], from_segments: comb.from_segments || [], domains: comb.domains || [], domains_str: domainsStr, source_word_details: wordDetails, source_scores: comb.source_scores || [], is_above_sources: isAboveSources, max_source_score: comb.max_source_score ?? null, scoreColor: scoreColor }; edges.push({ from: combStepId, to: combId, edge_type: 'step_to_comb', strategy: '域内组合' }); if (!iterations[roundNum * 10 + 2]) iterations[roundNum * 10 + 2] = []; iterations[roundNum * 10 + 2].push(combId); }); } // 步骤3: 筛选并执行搜索 const searchStepId = `step_search_r${roundNum}`; const searchCountText = round.search_count > 0 ? `筛选${round.high_score_sug_count}个高分词,搜索${round.search_count}次,${round.total_posts}个帖子` : `无高分推荐词,未执行搜索`; nodes[searchStepId] = { type: 'step', query: `步骤3: 筛选并执行搜索 (${searchCountText})`, level: roundNum * 10 + 1, relevance_score: 0, strategy: '筛选并执行搜索', iteration: roundNum, is_selected: true }; edges.push({ from: roundId, to: searchStepId, edge_type: 'round_to_step', strategy: '搜索' }); iterations[roundNum * 10].push(searchStepId); // 只有在有搜索结果时才添加搜索词和帖子 // 优先使用 round.search_results(新格式),否则使用外部传入的 searchResults(兼容旧版本) const roundSearchResults = round.search_results || searchResults; if (round.search_count > 0 && roundSearchResults) { if (Array.isArray(roundSearchResults)) { roundSearchResults.forEach((search, searchIndex) => { const searchWordId = `search_${search.text}_r${roundNum}_${searchIndex}`; nodes[searchWordId] = { type: 'search_word', query: '[SEARCH] ' + search.text, level: roundNum * 10 + 2, relevance_score: search.score_with_o || 0, strategy: '搜索词', iteration: roundNum, is_selected: true }; edges.push({ from: searchStepId, to: searchWordId, edge_type: 'step_to_search_word', strategy: '搜索词' }); if (!iterations[roundNum * 10 + 2]) iterations[roundNum * 10 + 2] = []; iterations[roundNum * 10 + 2].push(searchWordId); // 添加帖子 if (search.post_list && search.post_list.length > 0) { search.post_list.forEach((post, postIndex) => { const postId = `post_${post.note_id}_${searchIndex}_${postIndex}`; // 准备图片列表,将URL字符串转换为对象格式供轮播图使用 const imageList = (post.images || []).map(url => ({ image_url: url })); nodes[postId] = { type: 'post', query: '[R] ' + post.title, level: roundNum * 10 + 3, relevance_score: 0, strategy: '帖子', iteration: roundNum, is_selected: true, note_id: post.note_id, note_url: post.note_url, body_text: post.body_text || '', images: post.images || [], image_list: imageList, interact_info: post.interact_info || {}, // 附加多模态提取数据 extraction: extractionData && extractionData[post.note_id] ? extractionData[post.note_id] : null, // 评估数据 (V2) is_knowledge: post.is_knowledge !== undefined ? post.is_knowledge : null, knowledge_reason: post.knowledge_reason || '', knowledge_score: post.knowledge_score !== undefined ? post.knowledge_score : null, knowledge_level: post.knowledge_level !== undefined ? post.knowledge_level : null, knowledge_evaluation: post.knowledge_evaluation || null, post_relevance_score: post.relevance_score !== undefined ? post.relevance_score : null, relevance_level: post.relevance_level || '', relevance_reason: post.relevance_reason || '', relevance_conclusion: post.relevance_conclusion || '', relevance_evaluation: post.relevance_evaluation || null }; edges.push({ from: searchWordId, to: postId, edge_type: 'search_word_to_post', strategy: '搜索结果' }); // 如果有提取数据,创建对应的分析节点 if (extractionData && extractionData[post.note_id]) { const analysisId = `analysis_${post.note_id}_${searchIndex}_${postIndex}`; nodes[analysisId] = { type: 'analysis', query: '[AI分析] ' + post.title, level: roundNum * 10 + 4, relevance_score: 0, strategy: 'AI分析', iteration: roundNum, is_selected: true, note_id: post.note_id, note_url: post.note_url, title: post.title, body_text: post.body_text || '', interact_info: post.interact_info || {}, extraction: extractionData[post.note_id], image_list: imageList }; edges.push({ from: postId, to: analysisId, edge_type: 'post_to_analysis', strategy: 'AI分析' }); if (!iterations[roundNum * 10 + 4]) iterations[roundNum * 10 + 4] = []; iterations[roundNum * 10 + 4].push(analysisId); } if (!iterations[roundNum * 10 + 3]) iterations[roundNum * 10 + 3] = []; iterations[roundNum * 10 + 3].push(postId); }); } }); } } // 步骤3: 加词生成新查询 if (round.add_word_details && Object.keys(round.add_word_details).length > 0) { const addWordStepId = `step_add_r${roundNum}`; const totalAddWords = Object.values(round.add_word_details).reduce((sum, list) => sum + list.length, 0); nodes[addWordStepId] = { type: 'step', query: `步骤3: 加词生成新查询 (${totalAddWords}个)`, level: roundNum * 10 + 1, relevance_score: 0, strategy: '加词生成新查询', iteration: roundNum, is_selected: true }; edges.push({ from: roundId, to: addWordStepId, edge_type: 'round_to_step', strategy: '加词' }); iterations[roundNum * 10].push(addWordStepId); // 为每个 Seed 创建节点 Object.keys(round.add_word_details).forEach((seedText, seedIndex) => { const seedId = `seed_${seedText}_r${roundNum}_${seedIndex}`; // 查找seed的来源信息和分数 - 动态从正确的轮次查找 let seedInfo = {}; if (roundNum === 1) { // Round 1:种子来自 Round 0 的 seed_list const round0 = rounds.find(r => r.round_num === 0 || r.type === 'initialization'); seedInfo = round0?.seed_list?.find(s => s.text === seedText) || {}; } else { // Round 2+:种子来自前一轮的 seed_list_next const prevRound = rounds.find(r => r.round_num === roundNum - 1); seedInfo = prevRound?.seed_list_next?.find(s => s.text === seedText) || {}; } const fromType = seedInfo.from_type || seedInfo.from || 'unknown'; // 根据来源设置strategy let strategy; if (fromType === 'seg') { strategy = '初始分词'; } else if (fromType === 'add') { strategy = '加词'; } else if (fromType === 'sug') { strategy = '调用sug'; } else { strategy = 'Seed'; // 默认灰色 } nodes[seedId] = { type: 'seed', query: seedText, level: roundNum * 10 + 2, relevance_score: seedInfo.score || 0, // 从seedInfo读取种子的得分 strategy: strategy, iteration: roundNum, is_selected: true }; edges.push({ from: addWordStepId, to: seedId, edge_type: 'step_to_seed', strategy: 'Seed' }); if (!iterations[roundNum * 10 + 2]) iterations[roundNum * 10 + 2] = []; iterations[roundNum * 10 + 2].push(seedId); // 为每个 Seed 的组合词创建节点 const combinedWords = round.add_word_details[seedText] || []; combinedWords.forEach((word, wordIndex) => { const wordScore = word.score || 0; const seedScore = word.seed_score || 0; // 比较得分决定颜色:组合词得分 > 种子得分 → 绿色,否则 → 红色 const scoreColor = wordScore > seedScore ? '#22c55e' : '#ef4444'; const wordId = `add_${word.text}_r${roundNum}_seed${seedIndex}_${wordIndex}`; nodes[wordId] = { type: 'add_word', query: '[Q] ' + word.text, level: roundNum * 10 + 3, relevance_score: wordScore, evaluationReason: word.reason || '', strategy: '加词生成', iteration: roundNum, is_selected: true, selected_word: word.selected_word, seed_score: seedScore, // 原始种子的得分 scoreColor: scoreColor // 用于控制文字颜色 }; edges.push({ from: seedId, to: wordId, edge_type: 'seed_to_add_word', strategy: '组合词' }); if (!iterations[roundNum * 10 + 3]) iterations[roundNum * 10 + 3] = []; iterations[roundNum * 10 + 3].push(wordId); }); }); } // 步骤4: 筛选推荐词进入下轮 const filteredSugs = round.output_q_list?.filter(q => q.from === 'sug') || []; if (filteredSugs.length > 0) { const filterStepId = `step_filter_r${roundNum}`; nodes[filterStepId] = { type: 'step', query: `步骤4: 筛选推荐词进入下轮 (${filteredSugs.length}个)`, level: roundNum * 10 + 1, relevance_score: 0, strategy: '筛选推荐词进入下轮', iteration: roundNum, is_selected: true }; edges.push({ from: roundId, to: filterStepId, edge_type: 'round_to_step', strategy: '筛选' }); iterations[roundNum * 10].push(filterStepId); // 添加筛选出的sug filteredSugs.forEach((sug, sugIndex) => { const sugScore = sug.score || 0; // 尝试从sug_details中找到这个sug对应的父Q得分 let parentQScore = 0; if (round.sug_details) { for (const [qText, sugs] of Object.entries(round.sug_details)) { const matchingSug = sugs.find(s => s.text === sug.text); if (matchingSug) { // 找到父Q的得分 let qData = {}; if (roundNum === 0) { qData = round.q_list_1?.find(q => q.text === qText) || {}; } else { qData = round.input_queries?.find(q => q.text === qText) || {}; } parentQScore = qData.score || 0; break; } } } // 比较得分决定颜色:SUG得分 > Q得分 → 绿色,否则 → 红色 const scoreColor = (sugScore >= parentQScore + REQUIRED_SCORE_GAIN) ? '#22c55e' : '#ef4444'; const sugId = `filtered_sug_${sug.text}_r${roundNum}_${sugIndex}`; nodes[sugId] = { type: 'filtered_sug', query: '[SUG] ' + sug.text, level: roundNum * 10 + 2, relevance_score: sugScore, strategy: '进入下轮', iteration: roundNum, is_selected: true, scoreColor: scoreColor, // 新增:用于控制文字颜色 parentQScore: parentQScore // 新增:保存父Q得分用于调试 }; edges.push({ from: filterStepId, to: sugId, edge_type: 'step_to_filtered_sug', strategy: '进入下轮' }); if (!iterations[roundNum * 10 + 2]) iterations[roundNum * 10 + 2] = []; iterations[roundNum * 10 + 2].push(sugId); }); } // 步骤4: 构建下一轮(Round 1+) const highScoreCombinations = round.high_score_combinations || []; const highGainSugs = round.high_gain_sugs || []; const nextRoundItems = [...highScoreCombinations, ...highGainSugs]; if (nextRoundItems.length > 0) { const nextRoundStepId = `step_next_round_r${roundNum}`; nodes[nextRoundStepId] = { type: 'step', query: `步骤4: 构建下一轮 (${nextRoundItems.length}个查询)`, level: roundNum * 10 + 1, relevance_score: 0, strategy: '构建下一轮', iteration: roundNum, is_selected: true }; edges.push({ from: roundId, to: nextRoundStepId, edge_type: 'round_to_step', strategy: '构建下一轮' }); iterations[roundNum * 10].push(nextRoundStepId); // 创建查询节点 nextRoundItems.forEach((item, index) => { const itemId = `next_round_${item.text}_r${roundNum}_${index}`; const isSugItem = item.type === 'sug'; nodes[itemId] = { type: 'next_round_item', query: '[Q] ' + item.text, level: roundNum * 10 + 2, relevance_score: item.score || 0, strategy: item.type === 'combination' ? '域内组合' : '高增益SUG', iteration: roundNum, is_selected: true, type_label: item.type_label || '', item_type: item.type, is_suggestion: isSugItem, suggestion_label: isSugItem ? '[suggestion]' : '' }; edges.push({ from: nextRoundStepId, to: itemId, edge_type: 'step_to_next_round', strategy: item.type === 'combination' ? '域内组合' : 'SUG' }); if (!iterations[roundNum * 10 + 2]) iterations[roundNum * 10 + 2] = []; iterations[roundNum * 10 + 2].push(itemId); }); } } }); return { nodes, edges, iterations }; } /** * 简化版转换:专注于query和post的演化 * - 合并所有query节点(不区分seg/sug/add_word) * - 合并相同的帖子节点 * - 步骤信息放在边上 * - 隐藏Round/Step节点 */ function convertV8ToGraphSimplified(runContext, searchResults, extractionData) { const mergedNodes = {}; const edges = []; const iterations = {}; extractionData = extractionData || {}; // 默认为空对象 const o = runContext.o || '原始问题'; const rounds = runContext.rounds || []; // 添加原始问题根节点 const rootId = 'root_o'; mergedNodes[rootId] = { type: 'root', query: o, level: 0, relevance_score: 1.0, strategy: '原始问题', iteration: 0, is_selected: true, occurrences: [{round: 0, role: 'root', score: 1.0}] }; iterations[0] = [rootId]; // 用于记录节点之间的演化关系 const queryEvolution = {}; // {text: {occurrences: [], parentTexts: [], childTexts: []}} const postMap = {}; // {note_id: {...}} // 第一遍:收集所有query和post rounds.forEach((round, roundIndex) => { const roundNum = round.round_num || roundIndex; if (round.type === 'initialization') { // Round 0: 收集分词结果 (round.q_list_1 || []).forEach(q => { if (!queryEvolution[q.text]) { queryEvolution[q.text] = { occurrences: [], parentTexts: new Set([o]), // 来自原始问题 childTexts: new Set() }; } queryEvolution[q.text].occurrences.push({ round: roundNum, role: 'segmentation', strategy: '分词', score: q.score, reason: q.reason, type_label: q.type_label || q.typeLabel || '' }); }); } else { // Round 1+ // 收集sug_details (推荐词) Object.entries(round.sug_details || {}).forEach(([parentText, sugs]) => { sugs.forEach(sug => { if (!queryEvolution[sug.text]) { queryEvolution[sug.text] = { occurrences: [], parentTexts: new Set(), childTexts: new Set() }; } queryEvolution[sug.text].occurrences.push({ round: roundNum, role: 'sug', strategy: '调用sug', score: sug.score, reason: sug.reason, type_label: sug.type_label || sug.typeLabel || '' }); queryEvolution[sug.text].parentTexts.add(parentText); if (queryEvolution[parentText]) { queryEvolution[parentText].childTexts.add(sug.text); } }); }); // 收集add_word_details (加词结果) Object.entries(round.add_word_details || {}).forEach(([seedText, words]) => { words.forEach(word => { if (!queryEvolution[word.text]) { queryEvolution[word.text] = { occurrences: [], parentTexts: new Set(), childTexts: new Set() }; } queryEvolution[word.text].occurrences.push({ round: roundNum, role: 'add_word', strategy: '加词', score: word.score, reason: word.reason, selectedWord: word.selected_word, seedScore: word.seed_score, // 添加原始种子的得分 type_label: word.type_label || word.typeLabel || '' }); queryEvolution[word.text].parentTexts.add(seedText); if (queryEvolution[seedText]) { queryEvolution[seedText].childTexts.add(word.text); } }); }); // 收集搜索结果和帖子 const roundSearchResults = round.search_results || searchResults; if (roundSearchResults && Array.isArray(roundSearchResults)) { roundSearchResults.forEach(search => { const searchText = search.text; // 标记这个query被用于搜索 if (queryEvolution[searchText]) { queryEvolution[searchText].occurrences.push({ round: roundNum, role: 'search', strategy: '执行搜索', score: search.score_with_o, postCount: search.post_list ? search.post_list.length : 0 }); } // 收集帖子 if (search.post_list && search.post_list.length > 0) { search.post_list.forEach(post => { if (!postMap[post.note_id]) { postMap[post.note_id] = { ...post, foundByQueries: new Set(), foundInRounds: new Set() }; } postMap[post.note_id].foundByQueries.add(searchText); postMap[post.note_id].foundInRounds.add(roundNum); // 建立query到post的关系 if (!queryEvolution[searchText].posts) { queryEvolution[searchText].posts = new Set(); } queryEvolution[searchText].posts.add(post.note_id); }); } }); } } }); // 第二遍:创建合并后的节点 Object.entries(queryEvolution).forEach(([text, data]) => { const nodeId = `query_${text}`; // 获取最新的分数 const latestOccurrence = data.occurrences[data.occurrences.length - 1] || {}; const hasSearchResults = data.posts && data.posts.size > 0; mergedNodes[nodeId] = { type: 'query', query: '[Q] ' + text, level: Math.max(...data.occurrences.map(o => o.round), 0) * 10 + 2, relevance_score: latestOccurrence.score || 0, evaluationReason: latestOccurrence.reason || '', strategy: data.occurrences.map(o => o.strategy).join(' + '), primaryStrategy: latestOccurrence.strategy || '未知', // 添加主要策略字段 iteration: Math.max(...data.occurrences.map(o => o.round), 0), is_selected: true, occurrences: data.occurrences, hasSearchResults: hasSearchResults, postCount: data.posts ? data.posts.size : 0, selectedWord: data.occurrences.find(o => o.selectedWord)?.selectedWord || '', seed_score: data.occurrences.find(o => o.seedScore)?.seedScore, // 添加原始种子的得分 type_label: latestOccurrence.type_label || '' // 使用最新的 type_label }; // 添加到对应的轮次 const maxRound = Math.max(...data.occurrences.map(o => o.round), 0); const iterKey = maxRound * 10 + 2; if (!iterations[iterKey]) iterations[iterKey] = []; iterations[iterKey].push(nodeId); }); // 创建帖子节点 Object.entries(postMap).forEach(([noteId, post]) => { const postId = `post_${noteId}`; const imageList = (post.images || []).map(url => ({ image_url: url })); mergedNodes[postId] = { type: 'post', query: '[R] ' + post.title, level: 100, // 放在最后 relevance_score: 0, strategy: '帖子', iteration: Math.max(...Array.from(post.foundInRounds)), is_selected: true, note_id: post.note_id, note_url: post.note_url, body_text: post.body_text || '', images: post.images || [], image_list: imageList, interact_info: post.interact_info || {}, foundByQueries: Array.from(post.foundByQueries), foundInRounds: Array.from(post.foundInRounds), // 附加多模态提取数据 extraction: extractionData && extractionData[post.note_id] ? extractionData[post.note_id] : null, // 评估数据 is_knowledge: post.is_knowledge !== undefined ? post.is_knowledge : null, knowledge_reason: post.knowledge_reason || '', post_relevance_score: post.relevance_score !== undefined ? post.relevance_score : null, relevance_level: post.relevance_level || '', relevance_reason: post.relevance_reason || '' }; if (!iterations[100]) iterations[100] = []; iterations[100].push(postId); // 如果有提取数据,创建对应的分析节点 if (extractionData && extractionData[post.note_id]) { const analysisId = `analysis_${noteId}`; mergedNodes[analysisId] = { type: 'analysis', query: '[AI分析] ' + post.title, level: 101, relevance_score: 0, strategy: 'AI分析', iteration: Math.max(...Array.from(post.foundInRounds)), is_selected: true, note_id: post.note_id, note_url: post.note_url, title: post.title, body_text: post.body_text || '', interact_info: post.interact_info || {}, extraction: extractionData[post.note_id], image_list: imageList }; edges.push({ from: postId, to: analysisId, edge_type: 'post_to_analysis', strategy: 'AI分析', label: 'AI分析', round: Math.max(...Array.from(post.foundInRounds)) }); if (!iterations[101]) iterations[101] = []; iterations[101].push(analysisId); } }); // 第三遍:创建边 // 1. 原始问题 -> 分词结果 Object.entries(queryEvolution).forEach(([text, data]) => { const nodeId = `query_${text}`; const segOccurrence = data.occurrences.find(o => o.role === 'segmentation'); if (segOccurrence && data.parentTexts.has(o)) { edges.push({ from: rootId, to: nodeId, edge_type: 'segmentation', strategy: '分词', label: '分词', round: 0 }); } }); // 2. Query演化关系 Object.entries(queryEvolution).forEach(([text, data]) => { const nodeId = `query_${text}`; data.parentTexts.forEach(parentText => { if (parentText === o) return; // 跳过原始问题(已处理) const parentNodeId = `query_${parentText}`; if (!mergedNodes[parentNodeId]) return; // 找到这个演化的策略和轮次 const occurrence = data.occurrences.find(o => o.role === 'sug' || o.role === 'add_word' ); edges.push({ from: parentNodeId, to: nodeId, edge_type: occurrence?.role || 'evolution', strategy: occurrence?.strategy || '演化', label: `${occurrence?.strategy || '演化'} (R${occurrence?.round || 0})`, round: occurrence?.round || 0 }); }); }); // 3. Query -> Post (搜索关系) Object.entries(queryEvolution).forEach(([text, data]) => { const nodeId = `query_${text}`; if (data.posts && data.posts.size > 0) { const searchOccurrence = data.occurrences.find(o => o.role === 'search'); data.posts.forEach(noteId => { const postId = `post_${noteId}`; edges.push({ from: nodeId, to: postId, edge_type: 'search', strategy: '搜索', label: `搜索 (${data.posts.size}个帖子)`, round: searchOccurrence?.round || 0 }); }); } }); return { nodes: mergedNodes, edges, iterations }; } module.exports = { convertV8ToGraphV2, convertV8ToGraphSimplified };