You've already forked DataMate
feat(api): 添加 graphrag 权限规则和优化知识图谱缓存失效
Some checks failed
Some checks failed
- 在权限规则匹配器中添加 /api/graphrag/** 的读写权限控制 - 修改图关系服务中的删除操作以精确失效相关实体缓存 - 更新图同步服务确保 BELONGS_TO 关系在增量同步时正确重建 - 重构图同步步骤服务中的组织归属关系构建逻辑 - 修复前端图_canvas 组件中的元素点击事件处理逻辑 - 实现 Python GraphRAG 缓存的启用/禁用功能 - 为 GraphRAG 缓存统计和清除接口添加调用方日志记录
This commit is contained in:
@@ -50,6 +50,7 @@ public class PermissionRuleMatcher {
|
|||||||
addModuleRules(permissionRules, "/api/content-generation/**", "module:content-generation:use", "module:content-generation:use");
|
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/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/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(READ_METHODS, "/api/auth/users/**", "system:user:manage"));
|
||||||
permissionRules.add(new PermissionRule(WRITE_METHODS, "/api/auth/users/**", "system:user:manage"));
|
permissionRules.add(new PermissionRule(WRITE_METHODS, "/api/auth/users/**", "system:user:manage"));
|
||||||
|
|||||||
@@ -178,8 +178,8 @@ public class GraphRelationService {
|
|||||||
public void deleteRelation(String graphId, String relationId) {
|
public void deleteRelation(String graphId, String relationId) {
|
||||||
validateGraphId(graphId);
|
validateGraphId(graphId);
|
||||||
|
|
||||||
// 确认关系存在
|
// 确认关系存在并保留关系两端实体 ID,用于精准缓存失效
|
||||||
relationRepository.findByIdAndGraphId(relationId, graphId)
|
RelationDetail detail = relationRepository.findByIdAndGraphId(relationId, graphId)
|
||||||
.orElseThrow(() -> BusinessException.of(KnowledgeGraphErrorCode.RELATION_NOT_FOUND));
|
.orElseThrow(() -> BusinessException.of(KnowledgeGraphErrorCode.RELATION_NOT_FOUND));
|
||||||
|
|
||||||
long deleted = relationRepository.deleteByIdAndGraphId(relationId, graphId);
|
long deleted = relationRepository.deleteByIdAndGraphId(relationId, graphId);
|
||||||
@@ -187,7 +187,11 @@ public class GraphRelationService {
|
|||||||
throw BusinessException.of(KnowledgeGraphErrorCode.RELATION_NOT_FOUND);
|
throw BusinessException.of(KnowledgeGraphErrorCode.RELATION_NOT_FOUND);
|
||||||
}
|
}
|
||||||
log.info("Relation deleted: id={}, graphId={}", relationId, graphId);
|
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
|
@Transactional
|
||||||
|
|||||||
@@ -296,7 +296,9 @@ public class GraphSyncService {
|
|||||||
resultMap.put("HAS_FIELD", stepService.mergeHasFieldRelations(graphId, syncId, changedEntityIds));
|
resultMap.put("HAS_FIELD", stepService.mergeHasFieldRelations(graphId, syncId, changedEntityIds));
|
||||||
resultMap.put("DERIVED_FROM", stepService.mergeDerivedFromRelations(graphId, syncId, changedEntityIds));
|
resultMap.put("DERIVED_FROM", stepService.mergeDerivedFromRelations(graphId, syncId, changedEntityIds));
|
||||||
if (!orgMapDegraded) {
|
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 {
|
} else {
|
||||||
log.info("[{}] Skipping BELONGS_TO relation build due to degraded org map fetch", syncId);
|
log.info("[{}] Skipping BELONGS_TO relation build due to degraded org map fetch", syncId);
|
||||||
resultMap.put("BELONGS_TO", SyncResult.builder().syncType("BELONGS_TO").build());
|
resultMap.put("BELONGS_TO", SyncResult.builder().syncType("BELONGS_TO").build());
|
||||||
|
|||||||
@@ -584,21 +584,16 @@ public class GraphSyncStepService {
|
|||||||
return endResult(result);
|
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 查找对应组织)
|
// User → Org(通过 userOrgMap 查找对应组织)
|
||||||
List<GraphEntity> users = entityRepository.findByGraphIdAndType(graphId, "User");
|
List<GraphEntity> users = entityRepository.findByGraphIdAndType(graphId, "User");
|
||||||
if (changedEntityIds != null) {
|
|
||||||
users = users.stream()
|
|
||||||
.filter(user -> changedEntityIds.contains(user.getId()))
|
|
||||||
.toList();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Dataset → Org(通过创建者的组织)
|
// Dataset → Org(通过创建者的组织)
|
||||||
List<GraphEntity> datasets = entityRepository.findByGraphIdAndType(graphId, "Dataset");
|
List<GraphEntity> datasets = entityRepository.findByGraphIdAndType(graphId, "Dataset");
|
||||||
if (changedEntityIds != null) {
|
|
||||||
datasets = datasets.stream()
|
|
||||||
.filter(dataset -> changedEntityIds.contains(dataset.getId()))
|
|
||||||
.toList();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 删除受影响实体的旧 BELONGS_TO 关系,避免组织变更后遗留过时关系
|
// 删除受影响实体的旧 BELONGS_TO 关系,避免组织变更后遗留过时关系
|
||||||
Set<String> affectedEntityIds = new LinkedHashSet<>();
|
Set<String> affectedEntityIds = new LinkedHashSet<>();
|
||||||
|
|||||||
@@ -263,7 +263,8 @@ class GraphRelationServiceTest {
|
|||||||
relationService.deleteRelation(GRAPH_ID, RELATION_ID);
|
relationService.deleteRelation(GRAPH_ID, RELATION_ID);
|
||||||
|
|
||||||
verify(relationRepository).deleteByIdAndGraphId(RELATION_ID, GRAPH_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
|
@Test
|
||||||
|
|||||||
@@ -18,6 +18,15 @@ interface GraphCanvasProps {
|
|||||||
onSelectionChange?: (nodeIds: string[], edgeIds: string[]) => void;
|
onSelectionChange?: (nodeIds: string[], edgeIds: string[]) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type GraphElementEvent = {
|
||||||
|
item?: {
|
||||||
|
id?: string;
|
||||||
|
getID?: () => string;
|
||||||
|
getModel?: () => { id?: string };
|
||||||
|
};
|
||||||
|
target?: { id?: string };
|
||||||
|
};
|
||||||
|
|
||||||
function GraphCanvas({
|
function GraphCanvas({
|
||||||
data,
|
data,
|
||||||
loading = false,
|
loading = false,
|
||||||
@@ -126,6 +135,30 @@ function GraphCanvas({
|
|||||||
}, [highlightedNodeIds, data]);
|
}, [highlightedNodeIds, data]);
|
||||||
|
|
||||||
// Helper: query selected elements from graph and notify parent
|
// 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 emitSelectionChange = useCallback(() => {
|
||||||
const graph = graphRef.current;
|
const graph = graphRef.current;
|
||||||
if (!graph || !onSelectionChange) return;
|
if (!graph || !onSelectionChange) return;
|
||||||
@@ -149,16 +182,25 @@ function GraphCanvas({
|
|||||||
const graph = graphRef.current;
|
const graph = graphRef.current;
|
||||||
if (!graph) return;
|
if (!graph) return;
|
||||||
|
|
||||||
const handleNodeClick = (event: { target: { id: string } }) => {
|
const handleNodeClick = (event: GraphElementEvent) => {
|
||||||
onNodeClick?.(event.target.id);
|
const nodeId = resolveElementId(event, "node");
|
||||||
|
if (nodeId) {
|
||||||
|
onNodeClick?.(nodeId);
|
||||||
|
}
|
||||||
emitSelectionChange();
|
emitSelectionChange();
|
||||||
};
|
};
|
||||||
const handleEdgeClick = (event: { target: { id: string } }) => {
|
const handleEdgeClick = (event: GraphElementEvent) => {
|
||||||
onEdgeClick?.(event.target.id);
|
const edgeId = resolveElementId(event, "edge");
|
||||||
|
if (edgeId) {
|
||||||
|
onEdgeClick?.(edgeId);
|
||||||
|
}
|
||||||
emitSelectionChange();
|
emitSelectionChange();
|
||||||
};
|
};
|
||||||
const handleNodeDblClick = (event: { target: { id: string } }) => {
|
const handleNodeDblClick = (event: GraphElementEvent) => {
|
||||||
onNodeDoubleClick?.(event.target.id);
|
const nodeId = resolveElementId(event, "node");
|
||||||
|
if (nodeId) {
|
||||||
|
onNodeDoubleClick?.(nodeId);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
const handleCanvasClick = () => {
|
const handleCanvasClick = () => {
|
||||||
onCanvasClick?.();
|
onCanvasClick?.();
|
||||||
@@ -176,7 +218,14 @@ function GraphCanvas({
|
|||||||
graph.off("node:dblclick", handleNodeDblClick);
|
graph.off("node:dblclick", handleNodeDblClick);
|
||||||
graph.off("canvas:click", handleCanvasClick);
|
graph.off("canvas:click", handleCanvasClick);
|
||||||
};
|
};
|
||||||
}, [onNodeClick, onEdgeClick, onNodeDoubleClick, onCanvasClick, emitSelectionChange]);
|
}, [
|
||||||
|
onNodeClick,
|
||||||
|
onEdgeClick,
|
||||||
|
onNodeDoubleClick,
|
||||||
|
onCanvasClick,
|
||||||
|
emitSelectionChange,
|
||||||
|
resolveElementId,
|
||||||
|
]);
|
||||||
|
|
||||||
// Fit view helper
|
// Fit view helper
|
||||||
const handleFitView = useCallback(() => {
|
const handleFitView = useCallback(() => {
|
||||||
|
|||||||
@@ -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:
|
class GraphRAGCache:
|
||||||
"""GraphRAG 检索结果缓存。
|
"""GraphRAG 检索结果缓存。
|
||||||
|
|
||||||
@@ -63,19 +81,27 @@ class GraphRAGCache:
|
|||||||
embedding_maxsize: int = 512,
|
embedding_maxsize: int = 512,
|
||||||
embedding_ttl: int = 600,
|
embedding_ttl: int = 600,
|
||||||
) -> None:
|
) -> None:
|
||||||
self._kg_cache: TTLCache = TTLCache(maxsize=kg_maxsize, ttl=kg_ttl)
|
self._kg_cache: TTLCache | _DisabledCache = self._create_cache(kg_maxsize, kg_ttl)
|
||||||
self._embedding_cache: TTLCache = TTLCache(maxsize=embedding_maxsize, ttl=embedding_ttl)
|
self._embedding_cache: TTLCache | _DisabledCache = self._create_cache(
|
||||||
|
embedding_maxsize, embedding_ttl
|
||||||
|
)
|
||||||
self._kg_lock = threading.Lock()
|
self._kg_lock = threading.Lock()
|
||||||
self._embedding_lock = threading.Lock()
|
self._embedding_lock = threading.Lock()
|
||||||
self._kg_stats = CacheStats()
|
self._kg_stats = CacheStats()
|
||||||
self._embedding_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
|
@classmethod
|
||||||
def from_settings(cls) -> GraphRAGCache:
|
def from_settings(cls) -> GraphRAGCache:
|
||||||
from app.core.config import settings
|
from app.core.config import settings
|
||||||
|
|
||||||
if not settings.graphrag_cache_enabled:
|
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(kg_maxsize=0, kg_ttl=1, embedding_maxsize=0, embedding_ttl=1)
|
||||||
|
|
||||||
return cls(
|
return cls(
|
||||||
|
|||||||
@@ -260,9 +260,10 @@ async def query_stream(
|
|||||||
summary="缓存统计",
|
summary="缓存统计",
|
||||||
description="返回 GraphRAG 检索缓存的命中率和容量统计。",
|
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
|
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())
|
return StandardResponse(code=200, message="success", data=get_cache().stats())
|
||||||
|
|
||||||
|
|
||||||
@@ -272,8 +273,9 @@ async def cache_stats():
|
|||||||
summary="清空缓存",
|
summary="清空缓存",
|
||||||
description="清空所有 GraphRAG 检索缓存。",
|
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
|
from app.module.kg_graphrag.cache import get_cache
|
||||||
|
|
||||||
|
logger.info("GraphRAG cache clear requested by caller=%s", caller)
|
||||||
get_cache().clear()
|
get_cache().clear()
|
||||||
return StandardResponse(code=200, message="success", data={"cleared": True})
|
return StandardResponse(code=200, message="success", data={"cleared": True})
|
||||||
|
|||||||
Reference in New Issue
Block a user