Переглянути джерело

feat: 支持多版本推导图谱切换

- vite.config.js: 加载所有推导图谱版本
- graph.js: 动态合并选中版本的推导数据,默认v3
- DerivationView.vue: 添加版本选择下拉框,移除边数显示
- build.py: 移除版本参数,自动加载所有版本
- analyze_node_origin_v4.py: 修复节点ID映射问题

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
yangxiaohui 1 день тому
батько
коміт
7148a28086

+ 36 - 18
script/data_processing/analyze_node_origin_v4.py

@@ -416,6 +416,8 @@ def build_origin_graph(all_results: List[Dict], post_id: str) -> Dict:
     """将分析结果转换为图谱格式"""
     nodes = {}
     edges = {}
+    # 特征名到节点ID的映射(用于修正 LLM 返回的类型名不匹配问题)
+    name_to_node_id = {}
 
     for result in all_results:
         target_input = result.get("输入", {})
@@ -433,6 +435,7 @@ def build_origin_graph(all_results: List[Dict], post_id: str) -> Dict:
                 "domain": "帖子",
                 "detail": {}
             }
+        name_to_node_id[target_name] = node_id
 
         # 添加候选特征节点
         for candidate in target_input.get("候选特征", []):
@@ -447,14 +450,15 @@ def build_origin_graph(all_results: List[Dict], post_id: str) -> Dict:
                     "domain": "帖子",
                     "detail": {}
                 }
+            name_to_node_id[c_name] = c_node_id
 
     # 构建推导边
     for result in all_results:
         target_name = result.get("目标特征", "")
-        target_input = result.get("输入", {})
-        target_info = target_input.get("目标特征", {})
-        target_type = target_info.get("特征类型", "关键点")
-        target_node_id = f"帖子:{target_type}:标签:{target_name}"
+        # 使用映射获取正确的节点ID
+        target_node_id = name_to_node_id.get(target_name)
+        if not target_node_id:
+            continue
 
         # V4 的推理分析在顶层,不是在 输出 下面
         reasoning = result.get("推理分析", {})
@@ -462,8 +466,10 @@ def build_origin_graph(all_results: List[Dict], post_id: str) -> Dict:
         # 单独推理的边
         for item in reasoning.get("单独推理", []):
             source_name = item.get("来源特征", "")
-            source_type = item.get("来源特征类型", "关键点")
-            source_node_id = f"帖子:{source_type}:标签:{source_name}"
+            # 使用映射获取正确的节点ID(而非LLM返回的类型名)
+            source_node_id = name_to_node_id.get(source_name)
+            if not source_node_id:
+                continue
             probability = item.get("可能性", 0)
 
             edge_id = f"{source_node_id}|推导|{target_node_id}"
@@ -481,16 +487,30 @@ def build_origin_graph(all_results: List[Dict], post_id: str) -> Dict:
         # 组合推理的边
         for item in reasoning.get("组合推理", []):
             members = item.get("组合成员", [])
-            member_types = item.get("成员类型", [])
             probability = item.get("可能性", 0)
 
-            # 创建组合虚拟节点
-            member_pairs = list(zip(members, member_types)) if len(member_types) == len(members) else [(m, "关键点") for m in members]
-            sorted_pairs = sorted(member_pairs, key=lambda x: x[0])
-            sorted_members = [p[0] for p in sorted_pairs]
-            sorted_types = [p[1] for p in sorted_pairs]
+            # 验证所有成员都存在于映射中
+            member_node_ids = []
+            valid = True
+            for m in members:
+                m_node_id = name_to_node_id.get(m)
+                if not m_node_id:
+                    valid = False
+                    break
+                member_node_ids.append((m, m_node_id))
+            if not valid:
+                continue
+
+            # 按名称排序
+            sorted_member_ids = sorted(member_node_ids, key=lambda x: x[0])
+
+            # 从节点ID中提取实际的维度类型(帖子:灵感点:标签:xxx -> 灵感点)
+            combo_parts = []
+            for m_name, m_node_id in sorted_member_ids:
+                parts = m_node_id.split(":")
+                m_dimension = parts[1] if len(parts) > 1 else "关键点"
+                combo_parts.append(f"{m_dimension}:{m_name}")
 
-            combo_parts = [f"{sorted_types[i]}:{m}" for i, m in enumerate(sorted_members)]
             combo_name = " + ".join(combo_parts)
             combo_node_id = f"帖子:组合:组合:{combo_name}"
             if combo_node_id not in nodes:
@@ -500,8 +520,8 @@ def build_origin_graph(all_results: List[Dict], post_id: str) -> Dict:
                     "dimension": "组合",
                     "domain": "帖子",
                     "detail": {
-                        "成员": sorted_members,
-                        "成员类型": sorted_types
+                        "成员": [m for m, _ in sorted_member_ids],
+                        "成员类型": [m_node_id.split(":")[1] for _, m_node_id in sorted_member_ids]
                     }
                 }
 
@@ -519,9 +539,7 @@ def build_origin_graph(all_results: List[Dict], post_id: str) -> Dict:
             }
 
             # 成员到组合节点的边
-            for i, member in enumerate(sorted_members):
-                m_type = sorted_types[i]
-                m_node_id = f"帖子:{m_type}:标签:{member}"
+            for m_name, m_node_id in sorted_member_ids:
                 m_edge_id = f"{m_node_id}|组成|{combo_node_id}"
                 if m_edge_id not in edges:
                     edges[m_edge_id] = {

+ 18 - 0
script/visualization/build.py

@@ -46,6 +46,24 @@ def main():
         post_graph_count = len(list(post_graph_dir.glob("*_帖子图谱.json")))
     print(f"帖子图谱数量: {post_graph_count}")
 
+    # 统计推导图谱文件数(所有版本)
+    derivation_dir = config.intermediate_dir / "node_origin_analysis"
+    if derivation_dir.exists():
+        derivation_files = list(derivation_dir.glob("*_推导图谱*.json"))
+        # 按版本分组统计
+        version_counts = {}
+        for f in derivation_files:
+            name = f.name
+            if "_推导图谱_" in name:
+                # 提取版本号,如 _推导图谱_v3.json -> v3
+                version = name.split("_推导图谱_")[1].replace(".json", "")
+            else:
+                version = "default"
+            version_counts[version] = version_counts.get(version, 0) + 1
+        print(f"推导图谱数量: {len(derivation_files)} (版本: {version_counts})")
+    else:
+        print("推导图谱数量: 0")
+
     # 2. 检查是否需要安装依赖
     node_modules = viz_dir / "node_modules"
     if not node_modules.exists():

+ 13 - 4
script/visualization/src/components/DerivationView.vue

@@ -114,10 +114,19 @@
           </div>
         </div>
 
-        <span v-if="derivationStats.edges > 0" class="text-primary">
-          {{ derivationStats.displayedEdges }}/{{ derivationStats.totalEdges }} 条边
-        </span>
-        <span v-else class="text-base-content/40">暂无推导数据</span>
+        <!-- 推导版本选择 -->
+        <select
+          v-if="store.currentPostDerivationVersions.length > 0"
+          v-model="store.selectedDerivationVersion"
+          class="select select-xs bg-base-200 min-w-16"
+          title="选择推导图谱版本"
+        >
+          <option v-for="v in store.currentPostDerivationVersions" :key="v" :value="v">
+            {{ v }}
+          </option>
+        </select>
+
+        <span v-if="derivationStats.edges === 0" class="text-base-content/40">暂无推导数据</span>
         <!-- 放大/恢复按钮 -->
         <button
           v-if="store.expandedPanel !== 'derivation'"

+ 58 - 2
script/visualization/src/stores/graph.js

@@ -5,10 +5,15 @@ import { ref, reactive, computed, watch } from 'vue'
 const graphDataRaw = __GRAPH_DATA__
 // eslint-disable-next-line no-undef
 const postGraphListRaw = __POST_GRAPH_LIST__ || []
+// eslint-disable-next-line no-undef
+const derivationGraphsRaw = __DERIVATION_GRAPHS__ || {}
+// eslint-disable-next-line no-undef
+const derivationVersionsRaw = __DERIVATION_VERSIONS__ || []
 
 console.log('人设图谱 loaded:', !!graphDataRaw)
 console.log('人设节点数:', Object.keys(graphDataRaw?.nodes || {}).length)
 console.log('帖子图谱数:', postGraphListRaw.length)
+console.log('推导图谱版本:', derivationVersionsRaw)
 
 export const useGraphStore = defineStore('graph', () => {
   // ==================== 数据 ====================
@@ -18,12 +23,59 @@ export const useGraphStore = defineStore('graph', () => {
   const postGraphList = ref(postGraphListRaw)
   const selectedPostIndex = ref(postGraphListRaw.length > 0 ? 0 : -1)
 
-  // 当前选中的帖子图谱
+  // ==================== 推导图谱数据 ====================
+  const derivationGraphs = ref(derivationGraphsRaw)
+  const derivationVersions = ref(derivationVersionsRaw)
+  // 默认选择 v3,如果没有 v3 则选择第一个版本
+  const selectedDerivationVersion = ref(
+    derivationVersionsRaw.includes('v3') ? 'v3' :
+    derivationVersionsRaw.length > 0 ? derivationVersionsRaw[0] : ''
+  )
+
+  // 当前选中的帖子图谱(动态合并推导数据)
   const currentPostGraph = computed(() => {
     if (selectedPostIndex.value < 0 || selectedPostIndex.value >= postGraphList.value.length) {
       return null
     }
-    return postGraphList.value[selectedPostIndex.value]
+    const postGraph = postGraphList.value[selectedPostIndex.value]
+    const postId = postGraph.meta?.postId
+
+    // 获取当前帖子的推导数据(按选中版本)
+    const postDerivations = derivationGraphs.value[postId]
+    const derivation = postDerivations?.[selectedDerivationVersion.value]
+
+    if (!derivation) {
+      return postGraph
+    }
+
+    // 深拷贝帖子图谱,然后合并推导数据
+    const merged = JSON.parse(JSON.stringify(postGraph))
+
+    // 合并节点(组合节点)
+    for (const [nodeId, node] of Object.entries(derivation.nodes || {})) {
+      if (!merged.nodes[nodeId]) {
+        merged.nodes[nodeId] = node
+      }
+    }
+
+    // 合并边(推导边和组成边)
+    for (const [edgeId, edge] of Object.entries(derivation.edges || {})) {
+      if (!merged.edges[edgeId]) {
+        merged.edges[edgeId] = edge
+      }
+    }
+
+    return merged
+  })
+
+  // 当前帖子可用的推导版本
+  const currentPostDerivationVersions = computed(() => {
+    if (selectedPostIndex.value < 0) return []
+    const postGraph = postGraphList.value[selectedPostIndex.value]
+    const postId = postGraph?.meta?.postId
+    const postDerivations = derivationGraphs.value[postId]
+    if (!postDerivations) return []
+    return Object.keys(postDerivations).sort()
   })
 
   // 帖子列表(用于下拉选择)
@@ -1061,6 +1113,10 @@ export const useGraphStore = defineStore('graph', () => {
     currentPostGraph,
     postTreeData,
     selectPost,
+    // 推导图谱版本
+    derivationVersions,
+    selectedDerivationVersion,
+    currentPostDerivationVersions,
     // 人设节点游走配置
     walkNodeTypes,
     walkSteps,

+ 23 - 28
script/visualization/vite.config.js

@@ -23,53 +23,44 @@ let postGraphList = []
 // 推导图谱目录(与 post_graph 同级的 node_origin_analysis 目录)
 const derivationGraphDir = postGraphDir ? path.join(path.dirname(postGraphDir), 'node_origin_analysis') : null
 
+// 推导图谱数据:{ postId: { v3: data, v4: data, ... } }
+const derivationGraphs = {}
+// 所有可用的版本
+const derivationVersions = new Set()
+
 if (postGraphDir && fs.existsSync(postGraphDir)) {
   console.log('帖子图谱目录:', postGraphDir)
   const files = fs.readdirSync(postGraphDir).filter(f => f.endsWith('_帖子图谱.json'))
   console.log('帖子图谱文件数:', files.length)
 
-  // 读取推导图谱(如果存在)
-  const derivationGraphs = new Map()
+  // 读取所有推导图谱(所有版本)
   if (derivationGraphDir && fs.existsSync(derivationGraphDir)) {
     console.log('推导图谱目录:', derivationGraphDir)
-    const derivationFiles = fs.readdirSync(derivationGraphDir).filter(f => f.endsWith('_推导图谱.json'))
+    // 匹配所有推导图谱文件:*_推导图谱.json 或 *_推导图谱_v*.json
+    const derivationFiles = fs.readdirSync(derivationGraphDir).filter(f => f.includes('_推导图谱') && f.endsWith('.json'))
     console.log('推导图谱文件数:', derivationFiles.length)
+
     for (const file of derivationFiles) {
       const filePath = path.join(derivationGraphDir, file)
       const derivationData = JSON.parse(fs.readFileSync(filePath, 'utf-8'))
       const postId = derivationData.meta?.postId
+      const version = derivationData.meta?.version || 'default'
+
       if (postId) {
-        derivationGraphs.set(postId, derivationData)
+        if (!derivationGraphs[postId]) {
+          derivationGraphs[postId] = {}
+        }
+        derivationGraphs[postId][version] = derivationData
+        derivationVersions.add(version)
+        console.log(`加载推导图谱: ${postId} [${version}]`, derivationData.meta?.stats)
       }
     }
   }
 
-  // 读取所有帖子图谱
+  // 读取所有帖子图谱(不合并推导数据,由前端动态合并)
   for (const file of files) {
     const filePath = path.join(postGraphDir, file)
     const postData = JSON.parse(fs.readFileSync(filePath, 'utf-8'))
-
-    // 合并推导图谱(如果存在)
-    const postId = postData.meta?.postId
-    if (postId && derivationGraphs.has(postId)) {
-      const derivation = derivationGraphs.get(postId)
-      console.log(`合并推导图谱到帖子 ${postId}:`, derivation.meta?.stats)
-
-      // 合并节点(组合节点)
-      for (const [nodeId, node] of Object.entries(derivation.nodes || {})) {
-        if (!postData.nodes[nodeId]) {
-          postData.nodes[nodeId] = node
-        }
-      }
-
-      // 合并边(推导边和组成边)
-      for (const [edgeId, edge] of Object.entries(derivation.edges || {})) {
-        if (!postData.edges[edgeId]) {
-          postData.edges[edgeId] = edge
-        }
-      }
-    }
-
     postGraphList.push(postData)
   }
 
@@ -79,6 +70,8 @@ if (postGraphDir && fs.existsSync(postGraphDir)) {
     const dateB = b.meta?.postDetail?.create_time || 0
     return dateB - dateA
   })
+
+  console.log('推导图谱版本:', Array.from(derivationVersions).sort())
 } else {
   console.log('未设置帖子图谱目录或目录不存在')
 }
@@ -88,7 +81,9 @@ export default defineConfig({
   define: {
     // 将数据注入为全局常量
     __GRAPH_DATA__: JSON.stringify(personaGraphData),
-    __POST_GRAPH_LIST__: JSON.stringify(postGraphList)
+    __POST_GRAPH_LIST__: JSON.stringify(postGraphList),
+    __DERIVATION_GRAPHS__: JSON.stringify(derivationGraphs),
+    __DERIVATION_VERSIONS__: JSON.stringify(Array.from(derivationVersions).sort())
   },
   build: {
     target: 'esnext',