Przeglądaj źródła

可视化修改

刘立冬 3 tygodni temu
rodzic
commit
f36888db7b

+ 10 - 7
sug_v6_1_2_115.py

@@ -800,7 +800,7 @@ async def run_round(
 
     round_data = {
         "round_num": round_num,
-        "input_q_list": [{"text": q.text, "score": q.score_with_o} for q in q_list],
+        "input_q_list": [{"text": q.text, "score": q.score_with_o, "type": "query"} for q in q_list],
         "input_word_list_1_size": len(word_list_1),
         "input_seed_list_size": len(seed_list)
     }
@@ -867,7 +867,8 @@ async def run_round(
                 sug_details[q_text].append({
                     "text": sug.text,
                     "score": sug.score_with_o,
-                    "reason": sug.reason
+                    "reason": sug.reason,
+                    "type": "sug"
                 })
 
     # 3. search_list构建
@@ -1031,7 +1032,9 @@ async def run_round(
                 "text": comb['query'],
                 "score": comb['score'],
                 "reason": comb['reason'],
-                "selected_word": comb['word']
+                "selected_word": comb['word'],
+                "seed_score": seed.score_with_o,  # 添加原始种子的得分
+                "type": "add"
             }
             for comb in top_5
         ]
@@ -1120,7 +1123,7 @@ async def run_round(
         "q_list_next_size": len(q_list_next),
         "seed_list_next_size": len(seed_list_next),
         "total_combinations": len(all_seed_combinations),
-        "output_q_list": [{"text": q.text, "score": q.score_with_o, "reason": q.reason, "from": q.from_source} for q in q_list_next],
+        "output_q_list": [{"text": q.text, "score": q.score_with_o, "reason": q.reason, "from": q.from_source, "type": "query"} for q in q_list_next],
         "seed_list_next": [{"text": seed.text, "from": seed.from_type, "score": seed.score_with_o} for seed in seed_list_next],
         "sug_details": sug_details,
         "add_word_details": add_word_details,
@@ -1164,10 +1167,10 @@ async def iterative_loop(
     context.rounds.append({
         "round_num": 0,
         "type": "initialization",
-        "seg_list": [{"text": s.text, "score": s.score_with_o, "reason": s.reason} for s in seg_list],
+        "seg_list": [{"text": s.text, "score": s.score_with_o, "reason": s.reason, "type": "seg"} for s in seg_list],
         "word_list_1": [{"text": w.text, "score": w.score_with_o} for w in word_list_1],
-        "q_list_1": [{"text": q.text, "score": q.score_with_o, "reason": q.reason} for q in q_list],
-        "seed_list": [{"text": s.text, "from_type": s.from_type, "score": s.score_with_o} for s in seed_list]
+        "q_list_1": [{"text": q.text, "score": q.score_with_o, "reason": q.reason, "type": "query"} for q in q_list],
+        "seed_list": [{"text": s.text, "from_type": s.from_type, "score": s.score_with_o, "type": "seed"} for s in seed_list]
     })
 
     # 收集所有搜索结果

+ 931 - 0
visualization/sug_v6_1_2_8/convert_v8_to_graph_v3.js

@@ -0,0 +1,931 @@
+/**
+ * 将 v6.1.2.8 的 run_context.json 转换成按 Round > 步骤 > 数据 组织的图结构
+ * v3: 增加 [Q] 和 [SUG] 标识前缀
+ */
+
+function convertV8ToGraphV2(runContext, searchResults) {
+  const nodes = {};
+  const edges = [];
+  const iterations = {};
+
+  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,
+        relevance_score: 0,
+        strategy: '初始化',
+        iteration: roundNum,
+        is_selected: true
+      };
+
+      edges.push({
+        from: rootId,
+        to: roundId,
+        edge_type: 'root_to_round',
+        strategy: '初始化'
+      });
+
+      if (!iterations[roundNum]) iterations[roundNum] = [];
+      iterations[roundNum].push(roundId);
+
+      // 创建分词步骤节点
+      const segStepId = `step_seg_r${roundNum}`;
+      nodes[segStepId] = {
+        type: 'step',
+        query: `步骤:分词 (${round.seg_list?.length || 0}个分词)`,
+        level: roundNum,
+        relevance_score: 0,
+        strategy: '分词',
+        iteration: roundNum,
+        is_selected: true
+      };
+
+      edges.push({
+        from: roundId,
+        to: segStepId,
+        edge_type: 'round_to_step',
+        strategy: '分词'
+      });
+
+      iterations[roundNum].push(segStepId);
+
+      // 添加分词结果作为步骤的子节点
+      round.seg_list?.forEach((seg, segIndex) => {
+        const segId = `seg_${seg.text}_${roundNum}_${segIndex}`;
+        nodes[segId] = {
+          type: 'seg',
+          query: seg.text,
+          level: roundNum + 1,
+          relevance_score: seg.score || 0,
+          evaluationReason: seg.reason || '',
+          strategy: '分词结果',
+          iteration: roundNum,
+          is_selected: true
+        };
+
+        edges.push({
+          from: segStepId,
+          to: segId,
+          edge_type: 'step_to_data',
+          strategy: '分词结果'
+        });
+
+        if (!iterations[roundNum + 1]) iterations[roundNum + 1] = [];
+        iterations[roundNum + 1].push(segId);
+      });
+
+    } 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_q_list查找
+          let qData = {};
+          if (roundNum === 0) {
+            qData = round.q_list_1?.find(q => q.text === qText) || {};
+          } else {
+            // 从当前轮的input_q_list中查找
+            qData = round.input_q_list?.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
+          };
+
+          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得分 → 绿色,否则 → 红色
+            const scoreColor = sugScore > qScore ? '#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: 筛选并执行搜索
+      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: `步骤2: 筛选并执行搜索 (${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 || {}
+                };
+
+                edges.push({
+                  from: searchWordId,
+                  to: postId,
+                  edge_type: 'search_word_to_post',
+                  strategy: '搜索结果'
+                });
+
+                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的来源信息 - 从Round 0的seed_list查找基础种子的from_type
+          const round0 = rounds.find(r => r.round_num === 0 || r.type === 'initialization');
+          const seedInfo = round0?.seed_list?.find(s => s.text === seedText) || {};
+          const fromType = seedInfo.from_type || '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_q_list?.find(q => q.text === qText) || {};
+                }
+                parentQScore = qData.score || 0;
+                break;
+              }
+            }
+          }
+
+          // 比较得分决定颜色:SUG得分 > Q得分 → 绿色,否则 → 红色
+          const scoreColor = sugScore > parentQScore ? '#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);
+        });
+      }
+
+      // 步骤5: 构建下一轮
+      const nextRoundStepId = `step_next_round_r${roundNum}`;
+      const nextQCount = round.output_q_list?.length || 0;
+      const nextSeedCount = round.seed_list_next_size || 0;
+
+      nodes[nextRoundStepId] = {
+        type: 'step',
+        query: `步骤5: 构建下一轮 (${nextQCount}个查询, ${nextSeedCount}个种子)`,
+        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);
+
+      // 5.1: 构建下轮查询
+      if (round.output_q_list && round.output_q_list.length > 0) {
+        const nextQStepId = `step_next_q_r${roundNum}`;
+        nodes[nextQStepId] = {
+          type: 'step',
+          query: `构建下轮查询 (${nextQCount}个)`,
+          level: roundNum * 10 + 2,
+          relevance_score: 0,
+          strategy: '下轮查询',
+          iteration: roundNum,
+          is_selected: true
+        };
+
+        edges.push({
+          from: nextRoundStepId,
+          to: nextQStepId,
+          edge_type: 'step_to_step',
+          strategy: '查询'
+        });
+
+        if (!iterations[roundNum * 10 + 2]) iterations[roundNum * 10 + 2] = [];
+        iterations[roundNum * 10 + 2].push(nextQStepId);
+
+        // 添加下轮查询列表
+        round.output_q_list.forEach((q, qIndex) => {
+          const nextQId = `next_q_${q.text}_r${roundNum}_${qIndex}`;
+
+          // 根据来源设置strategy
+          let strategy;
+          if (q.from === 'seg') {
+            strategy = '初始分词';
+          } else if (q.from === 'add') {
+            strategy = '加词';
+          } else if (q.from === 'sug') {
+            strategy = '调用sug';
+          } else {
+            strategy = 'Query'; // 默认
+          }
+
+          nodes[nextQId] = {
+            type: 'next_q',
+            query: '[Q] ' + q.text,
+            level: roundNum * 10 + 3,
+            relevance_score: q.score || 0,
+            evaluationReason: q.reason || '',
+            strategy: strategy,
+            iteration: roundNum,
+            is_selected: true,
+            from_source: q.from
+          };
+
+          edges.push({
+            from: nextQStepId,
+            to: nextQId,
+            edge_type: 'step_to_next_q',
+            strategy: strategy
+          });
+
+          if (!iterations[roundNum * 10 + 3]) iterations[roundNum * 10 + 3] = [];
+          iterations[roundNum * 10 + 3].push(nextQId);
+        });
+      }
+
+      // 5.2: 构建下轮种子(如果有数据的话)
+      if (nextSeedCount > 0 && round.seed_list_next) {
+        const nextSeedStepId = `step_next_seed_r${roundNum}`;
+        nodes[nextSeedStepId] = {
+          type: 'step',
+          query: `构建下轮种子 (${nextSeedCount}个)`,
+          level: roundNum * 10 + 2,
+          relevance_score: 0,
+          strategy: '下轮种子',
+          iteration: roundNum,
+          is_selected: true
+        };
+
+        edges.push({
+          from: nextRoundStepId,
+          to: nextSeedStepId,
+          edge_type: 'step_to_step',
+          strategy: '种子'
+        });
+
+        if (!iterations[roundNum * 10 + 2]) iterations[roundNum * 10 + 2] = [];
+        iterations[roundNum * 10 + 2].push(nextSeedStepId);
+
+        // 添加下轮种子列表
+        round.seed_list_next.forEach((seed, seedIndex) => {
+          const nextSeedId = `next_seed_${seed.text}_r${roundNum}_${seedIndex}`;
+
+          // 根据来源设置strategy
+          let strategy;
+          if (seed.from === 'seg') {
+            strategy = '初始分词';
+          } else if (seed.from === 'add') {
+            strategy = '加词';
+          } else if (seed.from === 'sug') {
+            strategy = '调用sug';
+          } else {
+            strategy = 'Seed'; // 默认
+          }
+
+          nodes[nextSeedId] = {
+            type: 'next_seed',
+            query: seed.text,
+            level: roundNum * 10 + 3,
+            relevance_score: seed.score || 0,
+            strategy: strategy,
+            iteration: roundNum,
+            is_selected: true,
+            from_source: seed.from
+          };
+
+          edges.push({
+            from: nextSeedStepId,
+            to: nextSeedId,
+            edge_type: 'step_to_next_seed',
+            strategy: strategy
+          });
+
+          if (!iterations[roundNum * 10 + 3]) iterations[roundNum * 10 + 3] = [];
+          iterations[roundNum * 10 + 3].push(nextSeedId);
+        });
+      }
+    }
+  });
+
+  return {
+    nodes,
+    edges,
+    iterations
+  };
+}
+
+/**
+ * 简化版转换:专注于query和post的演化
+ * - 合并所有query节点(不区分seg/sug/add_word)
+ * - 合并相同的帖子节点
+ * - 步骤信息放在边上
+ * - 隐藏Round/Step节点
+ */
+function convertV8ToGraphSimplified(runContext, searchResults) {
+  const mergedNodes = {};
+  const edges = [];
+  const iterations = {};
+
+  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
+        });
+      });
+    } 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
+          });
+          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  // 添加原始种子的得分
+          });
+          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  // 添加原始种子的得分
+    };
+
+    // 添加到对应的轮次
+    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)
+    };
+
+    if (!iterations[100]) iterations[100] = [];
+    iterations[100].push(postId);
+  });
+
+  // 第三遍:创建边
+  // 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 };

+ 13 - 3
visualization/sug_v6_1_2_8/index.js

@@ -4,7 +4,7 @@ const fs = require('fs');
 const path = require('path');
 const { build } = require('esbuild');
 const { convertV8ToGraph } = require('./convert_v8_to_graph');
-const { convertV8ToGraphV2, convertV8ToGraphSimplified } = require('./convert_v8_to_graph_v2');
+const { convertV8ToGraphV2, convertV8ToGraphSimplified } = require('./convert_v8_to_graph_v3');
 
 // 读取命令行参数
 const args = process.argv.slice(2);
@@ -351,6 +351,14 @@ function QueryNode({ id, data, sourcePosition, targetPosition }) {
               }}>
                 <strong style={{ color: '#4b5563' }}>选择词:</strong>
                 <span style={{ marginLeft: '4px', color: '#3b82f6', fontWeight: '500' }}>{data.selectedWord}</span>
+                {data.seed_score !== undefined && (
+                  <div style={{ marginTop: '4px' }}>
+                    <strong style={{ color: '#4b5563' }}>种子得分:</strong>
+                    <span style={{ marginLeft: '4px', color: '#16a34a', fontWeight: '500' }}>
+                      {typeof data.seed_score === 'number' ? data.seed_score.toFixed(2) : data.seed_score}
+                    </span>
+                  </div>
+                )}
               </div>
             )}
             {data.evaluationReason && (
@@ -853,7 +861,7 @@ function TreeNode({ node, level, children, isCollapsed, onToggle, isSelected, on
               maxWidth: '180px',
               flex: 1,
               minWidth: 0,
-              color: (node.type === 'note' ? node.data.matchLevel === 'unsatisfied' : node.data.isSelected === false) ? '#ef4444' : '#374151',
+              color: node.data.scoreColor || ((node.type === 'note' ? node.data.matchLevel === 'unsatisfied' : node.data.isSelected === false) ? '#ef4444' : '#374151'),
             }}
             title={node.data.title || node.id}
             >
@@ -1072,6 +1080,8 @@ function transformData(data) {
             searchCount: node.search_count, // search 节点特有
             totalPosts: node.total_posts, // search 节点特有
             selectedWord: node.selected_word || '', // 加词节点特有 - 显示选择的词
+            scoreColor: node.scoreColor || null,        // SUG节点的颜色标识
+            parentQScore: node.parentQScore || 0,       // 父Q得分(用于调试)
           },
           position: { x: 0, y: 0 },
         });
@@ -1901,7 +1911,7 @@ function FlowContent() {
       <div style={{ display: 'flex', flex: 1, overflow: 'hidden' }}>
         {/* 左侧目录树 */}
         <div style={{
-          width: '320px',
+          width: '400px',
           background: 'white',
           borderRight: '1px solid #e5e7eb',
           display: 'flex',