diff --git a/backend/api-gateway/src/main/java/com/datamate/gateway/security/PermissionRuleMatcher.java b/backend/api-gateway/src/main/java/com/datamate/gateway/security/PermissionRuleMatcher.java index 02b32fc..09fa9b6 100644 --- a/backend/api-gateway/src/main/java/com/datamate/gateway/security/PermissionRuleMatcher.java +++ b/backend/api-gateway/src/main/java/com/datamate/gateway/security/PermissionRuleMatcher.java @@ -50,6 +50,7 @@ public class PermissionRuleMatcher { addModuleRules(permissionRules, "/api/content-generation/**", "module:content-generation:use", "module:content-generation:use"); addModuleRules(permissionRules, "/api/task-meta/**", "module:task-coordination:read", "module:task-coordination:write"); addModuleRules(permissionRules, "/api/knowledge-graph/**", "module:knowledge-graph:read", "module:knowledge-graph:write"); + addModuleRules(permissionRules, "/api/graphrag/**", "module:knowledge-base:read", "module:knowledge-base:write"); permissionRules.add(new PermissionRule(READ_METHODS, "/api/auth/users/**", "system:user:manage")); permissionRules.add(new PermissionRule(WRITE_METHODS, "/api/auth/users/**", "system:user:manage")); diff --git a/backend/services/knowledge-graph-service/src/main/java/com/datamate/knowledgegraph/application/GraphRelationService.java b/backend/services/knowledge-graph-service/src/main/java/com/datamate/knowledgegraph/application/GraphRelationService.java index 04d73d3..9926a8a 100644 --- a/backend/services/knowledge-graph-service/src/main/java/com/datamate/knowledgegraph/application/GraphRelationService.java +++ b/backend/services/knowledge-graph-service/src/main/java/com/datamate/knowledgegraph/application/GraphRelationService.java @@ -178,8 +178,8 @@ public class GraphRelationService { public void deleteRelation(String graphId, String relationId) { validateGraphId(graphId); - // 确认关系存在 - relationRepository.findByIdAndGraphId(relationId, graphId) + // 确认关系存在并保留关系两端实体 ID,用于精准缓存失效 + RelationDetail detail = relationRepository.findByIdAndGraphId(relationId, graphId) .orElseThrow(() -> BusinessException.of(KnowledgeGraphErrorCode.RELATION_NOT_FOUND)); long deleted = relationRepository.deleteByIdAndGraphId(relationId, graphId); @@ -187,7 +187,11 @@ public class GraphRelationService { throw BusinessException.of(KnowledgeGraphErrorCode.RELATION_NOT_FOUND); } log.info("Relation deleted: id={}, graphId={}", relationId, graphId); - cacheService.evictEntityCaches(graphId, relationId); + cacheService.evictEntityCaches(graphId, detail.getSourceEntityId()); + if (detail.getTargetEntityId() != null + && !detail.getTargetEntityId().equals(detail.getSourceEntityId())) { + cacheService.evictEntityCaches(graphId, detail.getTargetEntityId()); + } } @Transactional diff --git a/backend/services/knowledge-graph-service/src/main/java/com/datamate/knowledgegraph/application/GraphSyncService.java b/backend/services/knowledge-graph-service/src/main/java/com/datamate/knowledgegraph/application/GraphSyncService.java index ed0fbbe..3b89cb6 100644 --- a/backend/services/knowledge-graph-service/src/main/java/com/datamate/knowledgegraph/application/GraphSyncService.java +++ b/backend/services/knowledge-graph-service/src/main/java/com/datamate/knowledgegraph/application/GraphSyncService.java @@ -296,7 +296,9 @@ public class GraphSyncService { resultMap.put("HAS_FIELD", stepService.mergeHasFieldRelations(graphId, syncId, changedEntityIds)); resultMap.put("DERIVED_FROM", stepService.mergeDerivedFromRelations(graphId, syncId, changedEntityIds)); if (!orgMapDegraded) { - resultMap.put("BELONGS_TO", stepService.mergeBelongsToRelations(graphId, userOrgMap, syncId, changedEntityIds)); + // BELONGS_TO 依赖全量 userOrgMap,组织映射变更可能影响全部 User/Dataset。 + // 增量同步下也执行全量 BELONGS_TO 重建,避免漏更新。 + resultMap.put("BELONGS_TO", stepService.mergeBelongsToRelations(graphId, userOrgMap, syncId)); } else { log.info("[{}] Skipping BELONGS_TO relation build due to degraded org map fetch", syncId); resultMap.put("BELONGS_TO", SyncResult.builder().syncType("BELONGS_TO").build()); diff --git a/backend/services/knowledge-graph-service/src/main/java/com/datamate/knowledgegraph/application/GraphSyncStepService.java b/backend/services/knowledge-graph-service/src/main/java/com/datamate/knowledgegraph/application/GraphSyncStepService.java index 7395d5f..ce08f78 100644 --- a/backend/services/knowledge-graph-service/src/main/java/com/datamate/knowledgegraph/application/GraphSyncStepService.java +++ b/backend/services/knowledge-graph-service/src/main/java/com/datamate/knowledgegraph/application/GraphSyncStepService.java @@ -584,21 +584,16 @@ public class GraphSyncStepService { return endResult(result); } + if (changedEntityIds != null) { + log.debug("[{}] BELONGS_TO rebuild ignores changedEntityIds(size={}) due to org map dependency", + syncId, changedEntityIds.size()); + } + // User → Org(通过 userOrgMap 查找对应组织) List users = entityRepository.findByGraphIdAndType(graphId, "User"); - if (changedEntityIds != null) { - users = users.stream() - .filter(user -> changedEntityIds.contains(user.getId())) - .toList(); - } // Dataset → Org(通过创建者的组织) List datasets = entityRepository.findByGraphIdAndType(graphId, "Dataset"); - if (changedEntityIds != null) { - datasets = datasets.stream() - .filter(dataset -> changedEntityIds.contains(dataset.getId())) - .toList(); - } // 删除受影响实体的旧 BELONGS_TO 关系,避免组织变更后遗留过时关系 Set affectedEntityIds = new LinkedHashSet<>(); diff --git a/backend/services/knowledge-graph-service/src/test/java/com/datamate/knowledgegraph/application/GraphRelationServiceTest.java b/backend/services/knowledge-graph-service/src/test/java/com/datamate/knowledgegraph/application/GraphRelationServiceTest.java index 1c0117c..4c40aa0 100644 --- a/backend/services/knowledge-graph-service/src/test/java/com/datamate/knowledgegraph/application/GraphRelationServiceTest.java +++ b/backend/services/knowledge-graph-service/src/test/java/com/datamate/knowledgegraph/application/GraphRelationServiceTest.java @@ -263,7 +263,8 @@ class GraphRelationServiceTest { relationService.deleteRelation(GRAPH_ID, RELATION_ID); verify(relationRepository).deleteByIdAndGraphId(RELATION_ID, GRAPH_ID); - verify(cacheService).evictEntityCaches(GRAPH_ID, RELATION_ID); + verify(cacheService).evictEntityCaches(GRAPH_ID, SOURCE_ENTITY_ID); + verify(cacheService).evictEntityCaches(GRAPH_ID, TARGET_ENTITY_ID); } @Test diff --git a/frontend/src/pages/KnowledgeGraph/components/GraphCanvas.tsx b/frontend/src/pages/KnowledgeGraph/components/GraphCanvas.tsx index 27b2f57..8121f22 100644 --- a/frontend/src/pages/KnowledgeGraph/components/GraphCanvas.tsx +++ b/frontend/src/pages/KnowledgeGraph/components/GraphCanvas.tsx @@ -18,6 +18,15 @@ interface GraphCanvasProps { onSelectionChange?: (nodeIds: string[], edgeIds: string[]) => void; } +type GraphElementEvent = { + item?: { + id?: string; + getID?: () => string; + getModel?: () => { id?: string }; + }; + target?: { id?: string }; +}; + function GraphCanvas({ data, loading = false, @@ -126,6 +135,30 @@ function GraphCanvas({ }, [highlightedNodeIds, data]); // Helper: query selected elements from graph and notify parent + const resolveElementId = useCallback( + (event: GraphElementEvent, elementType: "node" | "edge"): string | null => { + const itemId = + event.item?.getID?.() ?? + event.item?.getModel?.()?.id ?? + event.item?.id; + if (itemId) { + return itemId; + } + + const targetId = event.target?.id; + if (!targetId) { + return null; + } + + const existsInData = + elementType === "node" + ? data.nodes.some((node) => node.id === targetId) + : data.edges.some((edge) => edge.id === targetId); + return existsInData ? targetId : null; + }, + [data.nodes, data.edges] + ); + const emitSelectionChange = useCallback(() => { const graph = graphRef.current; if (!graph || !onSelectionChange) return; @@ -149,16 +182,25 @@ function GraphCanvas({ const graph = graphRef.current; if (!graph) return; - const handleNodeClick = (event: { target: { id: string } }) => { - onNodeClick?.(event.target.id); + const handleNodeClick = (event: GraphElementEvent) => { + const nodeId = resolveElementId(event, "node"); + if (nodeId) { + onNodeClick?.(nodeId); + } emitSelectionChange(); }; - const handleEdgeClick = (event: { target: { id: string } }) => { - onEdgeClick?.(event.target.id); + const handleEdgeClick = (event: GraphElementEvent) => { + const edgeId = resolveElementId(event, "edge"); + if (edgeId) { + onEdgeClick?.(edgeId); + } emitSelectionChange(); }; - const handleNodeDblClick = (event: { target: { id: string } }) => { - onNodeDoubleClick?.(event.target.id); + const handleNodeDblClick = (event: GraphElementEvent) => { + const nodeId = resolveElementId(event, "node"); + if (nodeId) { + onNodeDoubleClick?.(nodeId); + } }; const handleCanvasClick = () => { onCanvasClick?.(); @@ -176,7 +218,14 @@ function GraphCanvas({ graph.off("node:dblclick", handleNodeDblClick); graph.off("canvas:click", handleCanvasClick); }; - }, [onNodeClick, onEdgeClick, onNodeDoubleClick, onCanvasClick, emitSelectionChange]); + }, [ + onNodeClick, + onEdgeClick, + onNodeDoubleClick, + onCanvasClick, + emitSelectionChange, + resolveElementId, + ]); // Fit view helper const handleFitView = useCallback(() => { diff --git a/runtime/datamate-python/app/module/kg_graphrag/cache.py b/runtime/datamate-python/app/module/kg_graphrag/cache.py index 829463c..2d3f8e5 100644 --- a/runtime/datamate-python/app/module/kg_graphrag/cache.py +++ b/runtime/datamate-python/app/module/kg_graphrag/cache.py @@ -49,6 +49,24 @@ class CacheStats: } +class _DisabledCache: + """缓存禁用时的 no-op 缓存实现。""" + + maxsize = 0 + + def get(self, key: str) -> None: + return None + + def __setitem__(self, key: str, value: Any) -> None: + return None + + def __len__(self) -> int: + return 0 + + def clear(self) -> None: + return None + + class GraphRAGCache: """GraphRAG 检索结果缓存。 @@ -63,19 +81,27 @@ class GraphRAGCache: embedding_maxsize: int = 512, embedding_ttl: int = 600, ) -> None: - self._kg_cache: TTLCache = TTLCache(maxsize=kg_maxsize, ttl=kg_ttl) - self._embedding_cache: TTLCache = TTLCache(maxsize=embedding_maxsize, ttl=embedding_ttl) + self._kg_cache: TTLCache | _DisabledCache = self._create_cache(kg_maxsize, kg_ttl) + self._embedding_cache: TTLCache | _DisabledCache = self._create_cache( + embedding_maxsize, embedding_ttl + ) self._kg_lock = threading.Lock() self._embedding_lock = threading.Lock() self._kg_stats = CacheStats() self._embedding_stats = CacheStats() + @staticmethod + def _create_cache(maxsize: int, ttl: int) -> TTLCache | _DisabledCache: + if maxsize <= 0: + return _DisabledCache() + return TTLCache(maxsize=maxsize, ttl=max(1, ttl)) + @classmethod def from_settings(cls) -> GraphRAGCache: from app.core.config import settings if not settings.graphrag_cache_enabled: - # 返回一个 maxsize=0 的缓存,所有 get 都会 miss,set 都是 no-op + # 返回禁用缓存实例:不缓存数据,避免 maxsize=0 初始化异常 return cls(kg_maxsize=0, kg_ttl=1, embedding_maxsize=0, embedding_ttl=1) return cls( diff --git a/runtime/datamate-python/app/module/kg_graphrag/interface.py b/runtime/datamate-python/app/module/kg_graphrag/interface.py index 3575f08..6d00cb8 100644 --- a/runtime/datamate-python/app/module/kg_graphrag/interface.py +++ b/runtime/datamate-python/app/module/kg_graphrag/interface.py @@ -260,9 +260,10 @@ async def query_stream( summary="缓存统计", description="返回 GraphRAG 检索缓存的命中率和容量统计。", ) -async def cache_stats(): +async def cache_stats(caller: Annotated[str, Depends(_require_caller_id)]): from app.module.kg_graphrag.cache import get_cache + logger.info("GraphRAG cache stats requested by caller=%s", caller) return StandardResponse(code=200, message="success", data=get_cache().stats()) @@ -272,8 +273,9 @@ async def cache_stats(): summary="清空缓存", description="清空所有 GraphRAG 检索缓存。", ) -async def cache_clear(): +async def cache_clear(caller: Annotated[str, Depends(_require_caller_id)]): from app.module.kg_graphrag.cache import get_cache + logger.info("GraphRAG cache clear requested by caller=%s", caller) get_cache().clear() return StandardResponse(code=200, message="success", data={"cleared": True})