diff --git a/src/api/memory_visualizer_router.py b/src/api/memory_visualizer_router.py index 2e4e1d482..b1ff00e65 100644 --- a/src/api/memory_visualizer_router.py +++ b/src/api/memory_visualizer_router.py @@ -550,20 +550,23 @@ async def search_memories(q: str, limit: int = 50): all_memories = memory_manager.graph_store.get_all_memories() for memory in all_memories: if q.lower() in memory.to_text().lower(): + node_ids = [node.id for node in memory.nodes] results.append( { "id": memory.id, "type": memory.memory_type.value, "importance": memory.importance, "text": memory.to_text(), + "node_ids": node_ids, # 返回关联的节点ID } ) else: # 从文件加载的数据中搜索 (降级方案) + # 注意:此模式下无法直接获取关联节点,前端需要做兼容处理 data = await load_graph_data_from_file() for memory in data.get("memories", []): if q.lower() in memory.get("text", "").lower(): - results.append(memory) + results.append(memory) # node_ids 可能不存在 return JSONResponse( content={ @@ -575,7 +578,11 @@ async def search_memories(q: str, limit: int = 50): } ) except Exception as e: - return JSONResponse(content={"success": False, "error": str(e)}, status_code=500) + # 确保即使在异常情况下也返回 data 字段 + return JSONResponse( + content={"success": False, "error": str(e), "data": {"results": [], "count": 0}}, + status_code=500, + ) @router.get("/api/stats") diff --git a/src/api/templates/visualizer.html b/src/api/templates/visualizer.html index 0f1f29838..a1bd626e3 100644 --- a/src/api/templates/visualizer.html +++ b/src/api/templates/visualizer.html @@ -1240,6 +1240,9 @@ return; } + // 先重置高亮,清除上一次的搜索结果 + resetNodeHighlight(); + try { const response = await fetch(`api/search?q=${encodeURIComponent(query)}&limit=50`); const result = await response.json(); @@ -1257,28 +1260,98 @@ // 高亮搜索结果 function highlightSearchResults(results) { - const memoryIds = results.map(r => r.id); const relatedNodeIds = new Set(); - - // 找出相关的节点 - graphData.memories.forEach(memory => { - if (memoryIds.includes(memory.id)) { - // 这里需要找到该记忆相关的所有节点 - // 简化实现:高亮所有节点 - graphData.nodes.forEach(node => relatedNodeIds.add(node.id)); + results.forEach(result => { + if (result.node_ids && result.node_ids.length > 0) { + result.node_ids.forEach(id => relatedNodeIds.add(id)); } }); - // 高亮节点 - if (relatedNodeIds.size > 0) { - network.selectNodes([...relatedNodeIds]); - network.fit({ - nodes: [...relatedNodeIds], - animation: true - }); - } else { - alert('未找到相关节点'); + if (relatedNodeIds.size === 0) { + alert('未找到与搜索结果直接关联的节点。\n这可能发生在从旧版文件加载数据时。'); + return; } + + console.log(`高亮 ${relatedNodeIds.size} 个搜索结果节点`); + + const allNodes = network.body.data.nodes; + const allEdges = network.body.data.edges; + const nodeUpdates = []; + const edgeUpdates = []; + + const nodesInViewIds = new Set(allNodes.getIds()); + const highlightedNodesInView = new Set(); + relatedNodeIds.forEach(id => { + if (nodesInViewIds.has(id)) { + highlightedNodesInView.add(id); + } + }); + + if (highlightedNodesInView.size === 0) { + alert('搜索到的记忆节点不在当前视图中。\n请尝试切换到“完整加载”模式或调整分页/聚类设置。'); + return; + } + + // 找出连接高亮节点的边 + const relatedEdgeIds = new Set(); + allEdges.get().forEach(edge => { + if (highlightedNodesInView.has(edge.from) && highlightedNodesInView.has(edge.to)) { + relatedEdgeIds.add(edge.id); + } + }); + + // 更新节点样式 + allNodes.get().forEach(node => { + const originalNodeData = graphData.nodes.find(n => n.id === node.id); + if (!originalNodeData) return; + + if (highlightedNodesInView.has(node.id)) { + nodeUpdates.push({ + id: node.id, + color: nodeColors[node.group] || '#999', + opacity: 1.0, + label: originalNodeData.label || '', + font: { color: '#333', size: 14, strokeWidth: 2, strokeColor: 'white' } + }); + } else { + const originalColor = nodeColors[node.group] || '#999'; + const dimmedColor = hexToRgba(originalColor, 0.1); + nodeUpdates.push({ + id: node.id, + color: { background: dimmedColor, border: dimmedColor }, + opacity: 0.1, + label: '', + font: { size: 0 } + }); + } + }); + + // 更新边样式 + allEdges.get().forEach(edge => { + if (relatedEdgeIds.has(edge.id)) { + edgeUpdates.push({ + id: edge.id, + color: { color: '#667eea', opacity: 1.0 }, + width: 2.5 + }); + } else { + edgeUpdates.push({ + id: edge.id, + color: { color: '#848484', opacity: 0.05 }, + width: 1 + }); + } + }); + + allNodes.update(nodeUpdates); + allEdges.update(edgeUpdates); + + network.fit({ + nodes: [...highlightedNodesInView], + animation: { duration: 800, easingFunction: 'easeInOutQuad' } + }); + + alert(`高亮了 ${highlightedNodesInView.size} 个相关节点。\n注意:如果处于聚类或分页模式,可能只显示部分节点。`); } // 高亮与选中节点连接的节点(优化版本,使用缓存) diff --git a/src/plugins/built_in/affinity_flow_chatter/core/affinity_interest_calculator.py b/src/plugins/built_in/affinity_flow_chatter/core/affinity_interest_calculator.py index 07d58ef6f..38ae6ad8c 100644 --- a/src/plugins/built_in/affinity_flow_chatter/core/affinity_interest_calculator.py +++ b/src/plugins/built_in/affinity_flow_chatter/core/affinity_interest_calculator.py @@ -307,6 +307,7 @@ class AffinityInterestCalculator(BaseInterestCalculator): self.post_reply_boost_max_count - self.post_reply_boost_remaining ) post_reply_reduction = self.post_reply_threshold_reduction * decay_factor + self.post_reply_boost_remaining -= 1 total_reduction += post_reply_reduction logger.debug( f"[阈值调整] 回复后降低: {post_reply_reduction:.3f} " @@ -319,17 +320,6 @@ class AffinityInterestCalculator(BaseInterestCalculator): return adjusted_reply_threshold, adjusted_action_threshold - def _apply_no_reply_boost(self, base_score: float) -> float: - """【已弃用】应用连续不回复的概率提升 - - 注意:此方法已被 _apply_no_reply_threshold_adjustment 替代 - 保留用于向后兼容 - """ - if self.no_reply_count > 0 and self.no_reply_count < self.max_no_reply_count: - boost = self.no_reply_count * self.probability_boost_per_no_reply - return min(1.0, base_score + boost) - return base_score - def _extract_keywords_from_database(self, message: "DatabaseMessages") -> list[str]: """从数据库消息中提取关键词""" keywords = [] @@ -394,7 +384,7 @@ class AffinityInterestCalculator(BaseInterestCalculator): def on_reply_sent(self): """当机器人发送回复后调用,激活回复后阈值降低机制""" - if self.enable_post_reply_boost: + if self.enable_post_reply_boost and not self.post_reply_boost_remaining: # 重置回复后降低计数器 self.post_reply_boost_remaining = self.post_reply_boost_max_count logger.debug( diff --git a/src/plugins/built_in/affinity_flow_chatter/planner/planner.py b/src/plugins/built_in/affinity_flow_chatter/planner/planner.py index 301bafb57..0ac27bb93 100644 --- a/src/plugins/built_in/affinity_flow_chatter/planner/planner.py +++ b/src/plugins/built_in/affinity_flow_chatter/planner/planner.py @@ -156,9 +156,6 @@ class ChatterActionPlanner: except Exception as e: logger.warning(f"Focus模式 - 处理消息 {message.message_id} 失败: {e}") - message.interest_value = 0.0 - message.should_reply = False - message.should_act = False # 2. 检查兴趣度是否达到非回复动作阈值 non_reply_action_interest_threshold = global_config.affinity_flow.non_reply_action_interest_threshold @@ -202,9 +199,9 @@ class ChatterActionPlanner: filtered_plan = await plan_filter.filter(initial_plan) # 检查reply动作是否可用 - reply_action_available = "reply" in available_actions or "respond" in available_actions - if filtered_plan.decided_actions and not reply_action_available: - logger.info("Focus模式 - 回复动作不可用,移除所有回复相关动作") + has_reply_action = "reply" in available_actions or "respond" in available_actions + if filtered_plan.decided_actions and has_reply_action and reply_not_available: + logger.info("Focus模式 - 未达到回复动作阈值,移除所有回复相关动作") filtered_plan.decided_actions = [ action for action in filtered_plan.decided_actions if action.action_type not in ["reply", "respond"]