|
@@ -1,39 +1,104 @@
|
|
|
|
|
+// C2 防护:边类型黑名单(模块级常量,参数化传入,勿拼字符串)
|
|
|
|
|
+// 普通模式:HAS_QUESTION + 能力画像全部边类型(支撑能力/考查能力/支撑维度/属于层次/对应模块)
|
|
|
|
|
+const HIDDEN_REL_TYPES = ['HAS_QUESTION', '支撑能力', '考查能力', '支撑维度', '属于层次', '对应模块'];
|
|
|
|
|
+// 能力画像模式:支撑能力仅由抽屉"展开支撑子图"专用查询(Q3a)消费,通用展开路径同样屏蔽
|
|
|
|
|
+const HIDDEN_REL_TYPES_ABILITY = ['HAS_QUESTION', '考查能力', '支撑维度', '属于层次', '对应模块'];
|
|
|
|
|
+
|
|
|
|
|
+// 边显示名:能力画像的"支撑能力/考查能力"边附加置信度(confidence 是过滤的第一口径)
|
|
|
|
|
+const formatEdgeLabel = (r_info) => {
|
|
|
|
|
+ const type = r_info.type || r_info.props?.label || '相关';
|
|
|
|
|
+ const c = r_info.props?.confidence;
|
|
|
|
|
+ return c ? `${type}·${c}` : type;
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
|
|
+// 节点 data 组装:能力画像属性(ability_id/level/definition 等)一并带上,
|
|
|
|
|
+// 供画布层次上色与能力项抽屉使用
|
|
|
|
|
+const toNodeData = (info) => {
|
|
|
|
|
+ const p = info.props || {};
|
|
|
|
|
+ return {
|
|
|
|
|
+ text: p.name || '',
|
|
|
|
|
+ elementId: info.elementId,
|
|
|
|
|
+ labels: info.labels,
|
|
|
|
|
+ ...(p.ability_id !== undefined && { ability_id: p.ability_id }),
|
|
|
|
|
+ ...(p.level !== undefined && { level: p.level }),
|
|
|
|
|
+ ...(p.definition !== undefined && { definition: p.definition }),
|
|
|
|
|
+ ...(p.knowledge_support !== undefined && { knowledge_support: p.knowledge_support }),
|
|
|
|
|
+ ...(p.star !== undefined && { star: p.star })
|
|
|
|
|
+ };
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
export const graphService = {
|
|
export const graphService = {
|
|
|
/**
|
|
/**
|
|
|
* 获取图谱的根节点
|
|
* 获取图谱的根节点
|
|
|
|
|
+ * 能力画像模式:以 6 个维度(AbilityDimension)为根(维度有"支撑维度"入边,
|
|
|
|
|
+ * 不能用 NOT ()-[]->(n) 判根,必须按标签直查)
|
|
|
*/
|
|
*/
|
|
|
- async findRootNodes(neo4jInstance) {
|
|
|
|
|
- const cypher = `
|
|
|
|
|
|
|
+ async findRootNodes(neo4jInstance, includeAbility = false) {
|
|
|
|
|
+ // 能力画像模式:6 个维度为根,节点大小按支撑知识点总数编码(如 877/818/672/653/637/419);
|
|
|
|
|
+ // 不走 NOT ()-[]->(n) 判根(维度有"支撑维度"入边),按标签直查
|
|
|
|
|
+ const cypher = includeAbility ? `
|
|
|
|
|
+ MATCH (n:AbilityDimension)
|
|
|
|
|
+ OPTIONAL MATCH ()-[r:支撑能力]->( )-[:包含]->(n)
|
|
|
|
|
+ WITH n, count(r) AS supportCount
|
|
|
|
|
+ RETURN id(n) AS id, elementId(n) AS elementId, labels(n) as labels,
|
|
|
|
|
+ properties(n) AS props, supportCount
|
|
|
|
|
+ ` : `
|
|
|
MATCH (n)
|
|
MATCH (n)
|
|
|
WHERE NOT ()-[]->(n) AND NOT n:Question
|
|
WHERE NOT ()-[]->(n) AND NOT n:Question
|
|
|
|
|
+ AND NOT n:AbilityDimension AND NOT n:Ability AND NOT n:AbilityLevel
|
|
|
RETURN id(n) AS id, elementId(n) AS elementId, labels(n) as labels, properties(n) AS props
|
|
RETURN id(n) AS id, elementId(n) AS elementId, labels(n) as labels, properties(n) AS props
|
|
|
`;
|
|
`;
|
|
|
|
|
|
|
|
const records = await neo4jInstance.execute(cypher);
|
|
const records = await neo4jInstance.execute(cypher);
|
|
|
- // console.log(records)
|
|
|
|
|
// 将结果整理为清晰的数组格式
|
|
// 将结果整理为清晰的数组格式
|
|
|
return records.map(record => ({
|
|
return records.map(record => ({
|
|
|
id: record.id,
|
|
id: record.id,
|
|
|
elementId: record.elementId,
|
|
elementId: record.elementId,
|
|
|
labels: record.labels,
|
|
labels: record.labels,
|
|
|
|
|
+ supportCount: record.supportCount ?? 0,
|
|
|
...record.props // 展开所有属性,比如 name, group 等
|
|
...record.props // 展开所有属性,比如 name, group 等
|
|
|
}));
|
|
}));
|
|
|
},
|
|
},
|
|
|
/**
|
|
/**
|
|
|
* 根据节点ID获取节点信息 WITH n,r,m LIMIT 50
|
|
* 根据节点ID获取节点信息 WITH n,r,m LIMIT 50
|
|
|
* */
|
|
* */
|
|
|
- async findNodeById(neo4jInstance, nodeId) {
|
|
|
|
|
- const cypher = `
|
|
|
|
|
|
|
+ async findNodeById(neo4jInstance, nodeId, includeAbility = false) {
|
|
|
|
|
+ // 能力画像模式:双向展开(Entity─支撑能力→Ability 等入边仅由 Q3a 专用查询消费,
|
|
|
|
|
+ // 通用展开路径屏蔽全部能力画像边,画布只出 维度─包含→能力项);
|
|
|
|
|
+ // AbilityLevel 不进画布(层次降维为 Ability 颜色属性)
|
|
|
|
|
+ const cypher = includeAbility ? `
|
|
|
|
|
+ MATCH (n)
|
|
|
|
|
+ WHERE id(n) = ${nodeId}
|
|
|
|
|
+ OPTIONAL MATCH (n)-[r1]->(m1)
|
|
|
|
|
+ WHERE NOT type(r1) IN $hidden AND NOT m1:AbilityLevel
|
|
|
|
|
+ AND (NOT n:Entity OR m1:Entity)
|
|
|
|
|
+ OPTIONAL MATCH (m2)-[r2]->(n)
|
|
|
|
|
+ WHERE NOT type(r2) IN $hidden AND NOT m2:AbilityLevel
|
|
|
|
|
+ AND (NOT n:Entity OR m2:Entity)
|
|
|
|
|
+ WITH n,
|
|
|
|
|
+ CASE WHEN r1 IS NOT NULL AND m1 IS NOT NULL THEN {rel: r1, other: m1} ELSE null END AS outRel,
|
|
|
|
|
+ CASE WHEN r2 IS NOT NULL AND m2 IS NOT NULL THEN {rel: r2, other: m2} ELSE null END AS inRel
|
|
|
|
|
+ WITH n, [x IN [outRel, inRel] WHERE x IS NOT NULL] AS rels
|
|
|
|
|
+ UNWIND rels AS rel
|
|
|
|
|
+ WITH n, rel.rel AS r, rel.other AS m
|
|
|
|
|
+ RETURN
|
|
|
|
|
+ {id: id(n), elementId: elementId(n), labels: labels(n), props: properties(n)} AS n_info,
|
|
|
|
|
+ {id: id(r), elementId: elementId(r), type: type(r), props: properties(r)} AS r_info,
|
|
|
|
|
+ {id: id(m), elementId: elementId(m), labels: labels(m), props: properties(m)} AS m_info
|
|
|
|
|
+ ` : `
|
|
|
MATCH (n)-[r]->(m)
|
|
MATCH (n)-[r]->(m)
|
|
|
WHERE id(n) = ${nodeId}
|
|
WHERE id(n) = ${nodeId}
|
|
|
- AND type(r) <> 'HAS_QUESTION'
|
|
|
|
|
-
|
|
|
|
|
|
|
+ AND NOT type(r) IN $hidden
|
|
|
|
|
+ AND NOT m:AbilityDimension AND NOT m:Ability AND NOT m:AbilityLevel
|
|
|
|
|
+
|
|
|
RETURN
|
|
RETURN
|
|
|
{id: id(n), elementId: elementId(n), labels: labels(n), props: properties(n)} AS n_info,
|
|
{id: id(n), elementId: elementId(n), labels: labels(n), props: properties(n)} AS n_info,
|
|
|
{id: id(r), elementId: elementId(r), type: type(r), props: properties(r)} AS r_info,
|
|
{id: id(r), elementId: elementId(r), type: type(r), props: properties(r)} AS r_info,
|
|
|
{id: id(m), elementId: elementId(m), labels: labels(m), props: properties(m)} AS m_info
|
|
{id: id(m), elementId: elementId(m), labels: labels(m), props: properties(m)} AS m_info
|
|
|
`;
|
|
`;
|
|
|
- const records = await neo4jInstance.execute(cypher);
|
|
|
|
|
|
|
+ const records = await neo4jInstance.execute(cypher, {
|
|
|
|
|
+ hidden: includeAbility ? HIDDEN_REL_TYPES_ABILITY : HIDDEN_REL_TYPES
|
|
|
|
|
+ });
|
|
|
|
|
|
|
|
// 将 records 转换为 G6 需要的 {nodes:[], edges:[]} 格式
|
|
// 将 records 转换为 G6 需要的 {nodes:[], edges:[]} 格式
|
|
|
const nodeMap = new Map();
|
|
const nodeMap = new Map();
|
|
@@ -51,12 +116,7 @@ export const graphService = {
|
|
|
// console.log(n_info.props.sources)
|
|
// console.log(n_info.props.sources)
|
|
|
nodeMap.set(n_info.id, {
|
|
nodeMap.set(n_info.id, {
|
|
|
id: String(n_info.id),
|
|
id: String(n_info.id),
|
|
|
- data: {
|
|
|
|
|
- text: n_info.props.name || '',
|
|
|
|
|
- elementId: n_info.elementId,
|
|
|
|
|
- labels: n_info.labels,
|
|
|
|
|
- ...(n_info.props.star !== undefined && { star: n_info.props.star })
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ data: toNodeData(n_info)
|
|
|
});
|
|
});
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -64,12 +124,7 @@ export const graphService = {
|
|
|
if (m_info && !nodeMap.has(m_info.id)) {
|
|
if (m_info && !nodeMap.has(m_info.id)) {
|
|
|
nodeMap.set(m_info.id, {
|
|
nodeMap.set(m_info.id, {
|
|
|
id: String(m_info.id),
|
|
id: String(m_info.id),
|
|
|
- data: {
|
|
|
|
|
- text: m_info.props.name || '',
|
|
|
|
|
- elementId: m_info.elementId,
|
|
|
|
|
- labels: m_info.labels,
|
|
|
|
|
- ...(m_info.props.star !== undefined && { star: m_info.props.star })
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ data: toNodeData(m_info)
|
|
|
});
|
|
});
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -79,7 +134,7 @@ export const graphService = {
|
|
|
id: String(r_info.elementId),
|
|
id: String(r_info.elementId),
|
|
|
source: String(n_info.id),
|
|
source: String(n_info.id),
|
|
|
target: String(m_info.id),
|
|
target: String(m_info.id),
|
|
|
- label: r_info.type || r_info.props.label || '相关',
|
|
|
|
|
|
|
+ label: formatEdgeLabel(r_info),
|
|
|
// sources: r_info.props?.sources?r_info.props?.sources:"[]",
|
|
// sources: r_info.props?.sources?r_info.props?.sources:"[]",
|
|
|
});
|
|
});
|
|
|
}
|
|
}
|
|
@@ -98,16 +153,22 @@ export const graphService = {
|
|
|
* 根据节点ID查询节点的双向关系(网状结构)
|
|
* 根据节点ID查询节点的双向关系(网状结构)
|
|
|
* 包括入边和出边,展示节点的完整关联路径
|
|
* 包括入边和出边,展示节点的完整关联路径
|
|
|
*/
|
|
*/
|
|
|
- async findNodeRelations(neo4jInstance, nodeId) {
|
|
|
|
|
|
|
+ async findNodeRelations(neo4jInstance, nodeId, includeAbility = false) {
|
|
|
|
|
+ // 普通模式剔除能力画像三类节点;能力画像模式保留但排除 AbilityLevel(层次不进画布);
|
|
|
|
|
+ // 能力画像模式下展开实体时仅保留实体邻居(过滤 Mu/Section 等结构节点)
|
|
|
|
|
+ const filterM = includeAbility ? ' AND NOT m:AbilityLevel AND (NOT n:Entity OR m:Entity)'
|
|
|
|
|
+ : ' AND NOT m:AbilityDimension AND NOT m:Ability AND NOT m:AbilityLevel';
|
|
|
|
|
+ const filterP = includeAbility ? ' AND NOT p:AbilityLevel AND (NOT n:Entity OR p:Entity)'
|
|
|
|
|
+ : ' AND NOT p:AbilityDimension AND NOT p:Ability AND NOT p:AbilityLevel';
|
|
|
const cypher = `
|
|
const cypher = `
|
|
|
MATCH (n)
|
|
MATCH (n)
|
|
|
WHERE elementId(n) = $nodeId OR id(n) = toInteger($nodeId)
|
|
WHERE elementId(n) = $nodeId OR id(n) = toInteger($nodeId)
|
|
|
|
|
|
|
|
OPTIONAL MATCH (n)-[r1]->(m)
|
|
OPTIONAL MATCH (n)-[r1]->(m)
|
|
|
- WHERE type(r1) <> 'HAS_QUESTION'
|
|
|
|
|
|
|
+ WHERE NOT type(r1) IN $hidden${filterM}
|
|
|
|
|
|
|
|
OPTIONAL MATCH (p)-[r2]->(n)
|
|
OPTIONAL MATCH (p)-[r2]->(n)
|
|
|
- WHERE type(r2) <> 'HAS_QUESTION'
|
|
|
|
|
|
|
+ WHERE NOT type(r2) IN $hidden${filterP}
|
|
|
|
|
|
|
|
WITH n, m, r1, p, r2
|
|
WITH n, m, r1, p, r2
|
|
|
|
|
|
|
@@ -117,7 +178,7 @@ export const graphService = {
|
|
|
UNWIND allNodes AS n1
|
|
UNWIND allNodes AS n1
|
|
|
UNWIND allNodes AS n2
|
|
UNWIND allNodes AS n2
|
|
|
MATCH (n1)-[r]->(n2)
|
|
MATCH (n1)-[r]->(n2)
|
|
|
- WHERE type(r) <> 'HAS_QUESTION' AND r IS NOT NULL
|
|
|
|
|
|
|
+ WHERE NOT type(r) IN $hidden AND r IS NOT NULL
|
|
|
|
|
|
|
|
RETURN
|
|
RETURN
|
|
|
{id: id(n1), elementId: elementId(n1), labels: labels(n1), props: properties(n1)} AS n_info,
|
|
{id: id(n1), elementId: elementId(n1), labels: labels(n1), props: properties(n1)} AS n_info,
|
|
@@ -125,7 +186,10 @@ export const graphService = {
|
|
|
{id: id(n2), elementId: elementId(n2), labels: labels(n2), props: properties(n2)} AS m_info
|
|
{id: id(n2), elementId: elementId(n2), labels: labels(n2), props: properties(n2)} AS m_info
|
|
|
`;
|
|
`;
|
|
|
|
|
|
|
|
- const records = await neo4jInstance.execute(cypher, { nodeId });
|
|
|
|
|
|
|
+ const records = await neo4jInstance.execute(cypher, {
|
|
|
|
|
+ nodeId,
|
|
|
|
|
+ hidden: includeAbility ? HIDDEN_REL_TYPES_ABILITY : HIDDEN_REL_TYPES
|
|
|
|
|
+ });
|
|
|
|
|
|
|
|
const nodeMap = new Map();
|
|
const nodeMap = new Map();
|
|
|
const edgeMap = new Map();
|
|
const edgeMap = new Map();
|
|
@@ -136,24 +200,14 @@ export const graphService = {
|
|
|
if (n_info && !nodeMap.has(n_info.id)) {
|
|
if (n_info && !nodeMap.has(n_info.id)) {
|
|
|
nodeMap.set(n_info.id, {
|
|
nodeMap.set(n_info.id, {
|
|
|
id: String(n_info.id),
|
|
id: String(n_info.id),
|
|
|
- data: {
|
|
|
|
|
- text: n_info.props.name || '',
|
|
|
|
|
- elementId: n_info.elementId,
|
|
|
|
|
- labels: n_info.labels,
|
|
|
|
|
- ...(n_info.props.star !== undefined && { star: n_info.props.star })
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ data: toNodeData(n_info)
|
|
|
});
|
|
});
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
if (m_info && !nodeMap.has(m_info.id)) {
|
|
if (m_info && !nodeMap.has(m_info.id)) {
|
|
|
nodeMap.set(m_info.id, {
|
|
nodeMap.set(m_info.id, {
|
|
|
id: String(m_info.id),
|
|
id: String(m_info.id),
|
|
|
- data: {
|
|
|
|
|
- text: m_info.props.name || '',
|
|
|
|
|
- elementId: m_info.elementId,
|
|
|
|
|
- labels: m_info.labels,
|
|
|
|
|
- ...(m_info.props.star !== undefined && { star: m_info.props.star })
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ data: toNodeData(m_info)
|
|
|
});
|
|
});
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -162,7 +216,7 @@ export const graphService = {
|
|
|
id: String(r_info.elementId),
|
|
id: String(r_info.elementId),
|
|
|
source: String(n_info.id),
|
|
source: String(n_info.id),
|
|
|
target: String(m_info.id),
|
|
target: String(m_info.id),
|
|
|
- label: r_info.type || r_info.props.label || '相关',
|
|
|
|
|
|
|
+ label: formatEdgeLabel(r_info),
|
|
|
});
|
|
});
|
|
|
}
|
|
}
|
|
|
});
|
|
});
|
|
@@ -197,18 +251,22 @@ export const graphService = {
|
|
|
* 根据父节点ID,获取其下属的子图
|
|
* 根据父节点ID,获取其下属的子图
|
|
|
* (包含父节点、所有子节点,以及这个家族圈子内部的所有交叉关系)
|
|
* (包含父节点、所有子节点,以及这个家族圈子内部的所有交叉关系)
|
|
|
*/
|
|
*/
|
|
|
- async findSubgraphByParentId(neo4jInstance, parentNodeId) {
|
|
|
|
|
|
|
+ async findSubgraphByParentId(neo4jInstance, parentNodeId, includeAbility = false) {
|
|
|
// 1. MATCH 找到指定的父节点 n
|
|
// 1. MATCH 找到指定的父节点 n
|
|
|
// 2. OPTIONAL MATCH (n)-[r1]->(m) 顺着箭头找出所有的子节点 m
|
|
// 2. OPTIONAL MATCH (n)-[r1]->(m) 顺着箭头找出所有的子节点 m
|
|
|
// 3. 限制数量,并将父节点与所有子节点合并为 allNodes 节点池
|
|
// 3. 限制数量,并将父节点与所有子节点合并为 allNodes 节点池
|
|
|
// 4. 在节点池内部,寻找任意两点之间的所有连线 r2(从而把子节点之间的关系也查出来)
|
|
// 4. 在节点池内部,寻找任意两点之间的所有连线 r2(从而把子节点之间的关系也查出来)
|
|
|
|
|
+ // 能力画像模式排除 AbilityLevel(层次降维为颜色属性,不进画布);
|
|
|
|
|
+ // 展开实体时仅保留实体邻居(过滤 Mu/Section 等结构节点及其他类型)
|
|
|
|
|
+ const abilityFilter = includeAbility ? ' AND NOT m:AbilityLevel AND (NOT n:Entity OR m:Entity)'
|
|
|
|
|
+ : ' AND NOT m:AbilityDimension AND NOT m:Ability AND NOT m:AbilityLevel';
|
|
|
const cypher = `
|
|
const cypher = `
|
|
|
MATCH (n)
|
|
MATCH (n)
|
|
|
WHERE elementId(n) = $parentNodeId OR id(n) = toInteger($parentNodeId)
|
|
WHERE elementId(n) = $parentNodeId OR id(n) = toInteger($parentNodeId)
|
|
|
|
|
|
|
|
// 明确单向箭头,只找父节点指向的子节点
|
|
// 明确单向箭头,只找父节点指向的子节点
|
|
|
OPTIONAL MATCH (n)-[r1]->(m)
|
|
OPTIONAL MATCH (n)-[r1]->(m)
|
|
|
- WHERE type(r1) <> 'HAS_QUESTION'
|
|
|
|
|
|
|
+ WHERE NOT type(r1) IN $hidden${abilityFilter}
|
|
|
|
|
|
|
|
WITH n, m LIMIT 60
|
|
WITH n, m LIMIT 60
|
|
|
|
|
|
|
@@ -219,7 +277,7 @@ export const graphService = {
|
|
|
UNWIND allNodes AS n1
|
|
UNWIND allNodes AS n1
|
|
|
UNWIND allNodes AS n2
|
|
UNWIND allNodes AS n2
|
|
|
MATCH (n1)-[r2]->(n2)
|
|
MATCH (n1)-[r2]->(n2)
|
|
|
- WHERE type(r2) <> 'HAS_QUESTION'
|
|
|
|
|
|
|
+ WHERE NOT type(r2) IN $hidden
|
|
|
|
|
|
|
|
RETURN
|
|
RETURN
|
|
|
{id: id(n1), elementId: elementId(n1), labels: labels(n1), props: properties(n1)} AS n_info,
|
|
{id: id(n1), elementId: elementId(n1), labels: labels(n1), props: properties(n1)} AS n_info,
|
|
@@ -227,7 +285,10 @@ export const graphService = {
|
|
|
{id: id(n2), elementId: elementId(n2), labels: labels(n2), props: properties(n2)} AS m_info
|
|
{id: id(n2), elementId: elementId(n2), labels: labels(n2), props: properties(n2)} AS m_info
|
|
|
`;
|
|
`;
|
|
|
|
|
|
|
|
- const records = await neo4jInstance.execute(cypher, { parentNodeId: parentNodeId });
|
|
|
|
|
|
|
+ const records = await neo4jInstance.execute(cypher, {
|
|
|
|
|
+ parentNodeId: parentNodeId,
|
|
|
|
|
+ hidden: includeAbility ? HIDDEN_REL_TYPES_ABILITY : HIDDEN_REL_TYPES
|
|
|
|
|
+ });
|
|
|
|
|
|
|
|
const nodeMap = new Map();
|
|
const nodeMap = new Map();
|
|
|
const edgeMap = new Map();
|
|
const edgeMap = new Map();
|
|
@@ -265,7 +326,7 @@ export const graphService = {
|
|
|
id: String(r_info.elementId),
|
|
id: String(r_info.elementId),
|
|
|
source: String(n_info.elementId),
|
|
source: String(n_info.elementId),
|
|
|
target: String(m_info.elementId),
|
|
target: String(m_info.elementId),
|
|
|
- label: r_info.type || r_info.props.label || '相关',
|
|
|
|
|
|
|
+ label: formatEdgeLabel(r_info),
|
|
|
});
|
|
});
|
|
|
}
|
|
}
|
|
|
});
|
|
});
|
|
@@ -296,14 +357,273 @@ export const graphService = {
|
|
|
};
|
|
};
|
|
|
},
|
|
},
|
|
|
|
|
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 查询某个父节点的直接子节点总数
|
|
|
|
|
+ */
|
|
|
|
|
+ async countChildren(neo4jInstance, parentNodeId, includeAbility = false) {
|
|
|
|
|
+ // 能力画像模式:双向计数(出边邻居 + 入边邻居),排除 AbilityLevel;
|
|
|
|
|
+ // 支撑能力等边在黑名单内,此计数反映"画布可见邻居数"
|
|
|
|
|
+ const cypher = includeAbility ? `
|
|
|
|
|
+ MATCH (n)
|
|
|
|
|
+ WHERE elementId(n) = $parentNodeId OR id(n) = toInteger($parentNodeId)
|
|
|
|
|
+ OPTIONAL MATCH (n)-[r1]->(m1)
|
|
|
|
|
+ WHERE NOT type(r1) IN $hidden AND NOT m1:AbilityLevel
|
|
|
|
|
+ AND (NOT n:Entity OR m1:Entity)
|
|
|
|
|
+ OPTIONAL MATCH (m2)-[r2]->(n)
|
|
|
|
|
+ WHERE NOT type(r2) IN $hidden AND NOT m2:AbilityLevel
|
|
|
|
|
+ AND (NOT n:Entity OR m2:Entity)
|
|
|
|
|
+ WITH collect(DISTINCT m1) + collect(DISTINCT m2) AS allM
|
|
|
|
|
+ RETURN size([x IN allM WHERE x IS NOT NULL]) AS total
|
|
|
|
|
+ ` : `
|
|
|
|
|
+ MATCH (n)-[r]->(m)
|
|
|
|
|
+ WHERE (elementId(n) = $parentNodeId OR id(n) = toInteger($parentNodeId))
|
|
|
|
|
+ AND NOT type(r) IN $hidden
|
|
|
|
|
+ AND NOT m:AbilityDimension AND NOT m:Ability AND NOT m:AbilityLevel
|
|
|
|
|
+ RETURN count(DISTINCT m) AS total
|
|
|
|
|
+ `;
|
|
|
|
|
+
|
|
|
|
|
+ const records = await neo4jInstance.execute(cypher, {
|
|
|
|
|
+ parentNodeId,
|
|
|
|
|
+ hidden: includeAbility ? HIDDEN_REL_TYPES_ABILITY : HIDDEN_REL_TYPES
|
|
|
|
|
+ });
|
|
|
|
|
+ return records[0]?.total || 0;
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 能力画像:查询能力项的层次详情(唯一需要查 AbilityLevel 节点的场景)
|
|
|
|
|
+ * 取 bloom 区间映射与 definition,用于抽屉展示
|
|
|
|
|
+ */
|
|
|
|
|
+ async findAbilityLevelDetail(neo4jInstance, abilityId) {
|
|
|
|
|
+ const cypher = `
|
|
|
|
|
+ MATCH (a:Ability {ability_id: $abilityId})-[:属于层次]->(l:AbilityLevel)
|
|
|
|
|
+ RETURN properties(l) AS levelProps
|
|
|
|
|
+ `;
|
|
|
|
|
+ const records = await neo4jInstance.execute(cypher, { abilityId });
|
|
|
|
|
+ return records[0]?.levelProps || null;
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 能力画像:本维度的能力层次分布(聚合统计小字,如"会 3 · 通 2")
|
|
|
|
|
+ */
|
|
|
|
|
+ async findDimensionLevelDistribution(neo4jInstance, dimensionId) {
|
|
|
|
|
+ const cypher = `
|
|
|
|
|
+ MATCH (d:AbilityDimension {dimension_id: $dimensionId})-[:包含]->(a:Ability)
|
|
|
|
|
+ RETURN a.level AS level, count(a) AS cnt
|
|
|
|
|
+ ORDER BY cnt DESC
|
|
|
|
|
+ `;
|
|
|
|
|
+ const records = await neo4jInstance.execute(cypher, { dimensionId });
|
|
|
|
|
+ return records.map(r => ({ level: r.level, count: r.cnt }));
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * Q1 能力全景(首页态 + 一级展开)
|
|
|
|
|
+ * 每行一个能力项,带支撑统计(total / high_cnt);
|
|
|
|
|
+ * 首页态按维度聚合 supportCount(节点大小编码),展开态取该维度的能力项行
|
|
|
|
|
+ */
|
|
|
|
|
+ async findAbilityOverview(neo4jInstance) {
|
|
|
|
|
+ const cypher = `
|
|
|
|
|
+ MATCH (d:AbilityDimension)-[:包含]->(a:Ability)-[:属于层次]->(l:AbilityLevel)
|
|
|
|
|
+ OPTIONAL MATCH (e:Entity)-[s:支撑能力]->(a)
|
|
|
|
|
+ RETURN id(d) AS d_id, elementId(d) AS d_elementId,
|
|
|
|
|
+ d.dimension_id AS dimension_id, d.name AS dimension_name,
|
|
|
|
|
+ id(a) AS a_id, elementId(a) AS a_elementId,
|
|
|
|
|
+ a.ability_id AS ability_id, a.name AS ability_name,
|
|
|
|
|
+ a.level AS level, a.definition AS definition, a.knowledge_support AS knowledge_support,
|
|
|
|
|
+ l.code AS level_code, l.bloom AS level_bloom, l.definition AS level_definition,
|
|
|
|
|
+ count(s) AS total,
|
|
|
|
|
+ sum(CASE WHEN s.confidence='高' THEN 1 ELSE 0 END) AS high_cnt
|
|
|
|
|
+ ORDER BY d.dimension_id, a.ability_id
|
|
|
|
|
+ `;
|
|
|
|
|
+ return await neo4jInstance.execute(cypher);
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * Q2 能力详情:支撑知识点列表(置信度分组排序:高→中→低,star 优先)
|
|
|
|
|
+ * 供能力项抽屉展示(name/type/star/confidence/reasoning)
|
|
|
|
|
+ */
|
|
|
|
|
+ async findAbilitySupportList(neo4jInstance, abilityId) {
|
|
|
|
|
+ const cypher = `
|
|
|
|
|
+ MATCH (e:Entity)-[s:支撑能力]->(a:Ability {ability_id: $abilityId})
|
|
|
|
|
+ RETURN e.name AS name, e.type AS type, e.star AS star,
|
|
|
|
|
+ s.confidence AS confidence, s.reasoning AS reasoning
|
|
|
|
|
+ ORDER BY CASE s.confidence WHEN '高' THEN 1 WHEN '中' THEN 2 ELSE 3 END,
|
|
|
|
|
+ e.star DESC
|
|
|
|
|
+ `;
|
|
|
|
|
+ return await neo4jInstance.execute(cypher, { abilityId });
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * Q3a 支撑子图·节点:Entity─支撑能力→Ability,高置信优先 + star 优先,LIMIT 默认 200
|
|
|
|
|
+ */
|
|
|
|
|
+ async findAbilitySupportSubgraph(neo4jInstance, abilityId, limit = 200) {
|
|
|
|
|
+ const cypher = `
|
|
|
|
|
+ MATCH (e:Entity)-[r:支撑能力]->(a:Ability {ability_id: $abilityId})
|
|
|
|
|
+ WITH e, r, a
|
|
|
|
|
+ ORDER BY CASE r.confidence WHEN '高' THEN 1 WHEN '中' THEN 2 WHEN '低' THEN 3 ELSE 4 END, e.star DESC
|
|
|
|
|
+ LIMIT toInteger($limit)
|
|
|
|
|
+ RETURN
|
|
|
|
|
+ {id: id(a), elementId: elementId(a), labels: labels(a), props: properties(a)} AS n_info,
|
|
|
|
|
+ {id: id(r), elementId: elementId(r), type: type(r), props: properties(r)} AS r_info,
|
|
|
|
|
+ {id: id(e), elementId: elementId(e), labels: labels(e), props: properties(e)} AS m_info
|
|
|
|
|
+ `;
|
|
|
|
|
+ const records = await neo4jInstance.execute(cypher, { abilityId, limit });
|
|
|
|
|
+
|
|
|
|
|
+ const nodeMap = new Map();
|
|
|
|
|
+ const edgeMap = new Map();
|
|
|
|
|
+
|
|
|
|
|
+ records.forEach(record => {
|
|
|
|
|
+ const { n_info, r_info, m_info } = record;
|
|
|
|
|
+ if (n_info && !nodeMap.has(n_info.id)) {
|
|
|
|
|
+ nodeMap.set(n_info.id, {
|
|
|
|
|
+ id: String(n_info.id),
|
|
|
|
|
+ data: toNodeData(n_info)
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+ if (m_info && !nodeMap.has(m_info.id)) {
|
|
|
|
|
+ nodeMap.set(m_info.id, {
|
|
|
|
|
+ id: String(m_info.id),
|
|
|
|
|
+ data: toNodeData(m_info)
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+ if (r_info && !edgeMap.has(r_info.id)) {
|
|
|
|
|
+ edgeMap.set(r_info.id, {
|
|
|
|
|
+ id: String(r_info.elementId),
|
|
|
|
|
+ source: String(m_info.id),
|
|
|
|
|
+ target: String(n_info.id),
|
|
|
|
|
+ label: formatEdgeLabel(r_info),
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ return {
|
|
|
|
|
+ nodes: Array.from(nodeMap.values()),
|
|
|
|
|
+ edges: Array.from(edgeMap.values())
|
|
|
|
|
+ };
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * Q3b 支撑子图·实体间语义边:仅在 Q3a 返回的实体名列表内,
|
|
|
|
|
+ * 找 前序/关联/因果/对比 四类语义关系($names 为实体名列表)
|
|
|
|
|
+ */
|
|
|
|
|
+ async findEntitySemanticEdges(neo4jInstance, names) {
|
|
|
|
|
+ if (!names || names.length < 2) return [];
|
|
|
|
|
+ const cypher = `
|
|
|
|
|
+ UNWIND $names AS n1
|
|
|
|
|
+ UNWIND $names AS n2
|
|
|
|
|
+ MATCH (a:Entity {name: n1})-[r]->(b:Entity {name: n2})
|
|
|
|
|
+ WHERE type(r) IN ['前序', '关联', '因果', '对比']
|
|
|
|
|
+ RETURN n1, n2, type(r) AS rel
|
|
|
|
|
+ `;
|
|
|
|
|
+ return await neo4jInstance.execute(cypher, { names });
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * Q4 能力可选题量:考查该能力项的题目数(本阶段仅数字展示,不做跳转)
|
|
|
|
|
+ */
|
|
|
|
|
+ async findAbilityQuestionCount(neo4jInstance, abilityId) {
|
|
|
|
|
+ const cypher = `
|
|
|
|
|
+ MATCH (a:Ability {ability_id: $abilityId})<-[:考查能力]-(q:Question)
|
|
|
|
|
+ RETURN count(q) AS q_cnt
|
|
|
|
|
+ `;
|
|
|
|
|
+ const records = await neo4jInstance.execute(cypher, { abilityId });
|
|
|
|
|
+ return records[0]?.q_cnt || 0;
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 分页查询父节点的子图(用于子节点超过阈值时分批加载)
|
|
|
|
|
+ * 行为与 findNodeById 一致(单向父子边,数字id),仅加分页
|
|
|
|
|
+ */
|
|
|
|
|
+ async findSubgraphByParentIdPaged(neo4jInstance, parentNodeId, skip, limit, includeAbility = false) {
|
|
|
|
|
+ // 分页查询(普通模式单向父子边);能力画像模式为双向(入边邻居)
|
|
|
|
|
+ const cypher = includeAbility ? `
|
|
|
|
|
+ MATCH (n)
|
|
|
|
|
+ WHERE (elementId(n) = $parentNodeId OR id(n) = toInteger($parentNodeId))
|
|
|
|
|
+ OPTIONAL MATCH (n)-[r1]->(m1)
|
|
|
|
|
+ WHERE NOT type(r1) IN $hidden AND NOT m1:AbilityLevel
|
|
|
|
|
+ AND (NOT n:Entity OR m1:Entity)
|
|
|
|
|
+ OPTIONAL MATCH (m2)-[r2]->(n)
|
|
|
|
|
+ WHERE NOT type(r2) IN $hidden AND NOT m2:AbilityLevel
|
|
|
|
|
+ AND (NOT n:Entity OR m2:Entity)
|
|
|
|
|
+ WITH n,
|
|
|
|
|
+ CASE WHEN r1 IS NOT NULL AND m1 IS NOT NULL THEN {rel: r1, other: m1} ELSE null END AS outRel,
|
|
|
|
|
+ CASE WHEN r2 IS NOT NULL AND m2 IS NOT NULL THEN {rel: r2, other: m2} ELSE null END AS inRel
|
|
|
|
|
+ WITH n, [x IN [outRel, inRel] WHERE x IS NOT NULL] AS rels
|
|
|
|
|
+ UNWIND rels AS rel
|
|
|
|
|
+ WITH n, rel.rel AS r, rel.other AS m
|
|
|
|
|
+ ORDER BY elementId(m), elementId(r)
|
|
|
|
|
+ SKIP toInteger($skip) LIMIT toInteger($limit)
|
|
|
|
|
+ RETURN
|
|
|
|
|
+ {id: id(n), elementId: elementId(n), labels: labels(n), props: properties(n)} AS n_info,
|
|
|
|
|
+ {id: id(r), elementId: elementId(r), type: type(r), props: properties(r)} AS r_info,
|
|
|
|
|
+ {id: id(m), elementId: elementId(m), labels: labels(m), props: properties(m)} AS m_info
|
|
|
|
|
+ ` : `
|
|
|
|
|
+ MATCH (n)-[r]->(m)
|
|
|
|
|
+ WHERE (elementId(n) = $parentNodeId OR id(n) = toInteger($parentNodeId))
|
|
|
|
|
+ AND NOT type(r) IN $hidden
|
|
|
|
|
+ AND NOT m:AbilityDimension AND NOT m:Ability AND NOT m:AbilityLevel
|
|
|
|
|
+ WITH n, r, m
|
|
|
|
|
+ ORDER BY elementId(m)
|
|
|
|
|
+ SKIP toInteger($skip) LIMIT toInteger($limit)
|
|
|
|
|
+ RETURN
|
|
|
|
|
+ {id: id(n), elementId: elementId(n), labels: labels(n), props: properties(n)} AS n_info,
|
|
|
|
|
+ {id: id(r), elementId: elementId(r), type: type(r), props: properties(r)} AS r_info,
|
|
|
|
|
+ {id: id(m), elementId: elementId(m), labels: labels(m), props: properties(m)} AS m_info
|
|
|
|
|
+ `;
|
|
|
|
|
+ const records = await neo4jInstance.execute(cypher, {
|
|
|
|
|
+ parentNodeId, skip, limit,
|
|
|
|
|
+ hidden: includeAbility ? HIDDEN_REL_TYPES_ABILITY : HIDDEN_REL_TYPES
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ const nodeMap = new Map();
|
|
|
|
|
+ const edgeMap = new Map();
|
|
|
|
|
+
|
|
|
|
|
+ records.forEach(record => {
|
|
|
|
|
+ const { n_info, r_info, m_info } = record;
|
|
|
|
|
+ // 起始节点 n
|
|
|
|
|
+ if (n_info && !nodeMap.has(n_info.id)) {
|
|
|
|
|
+ nodeMap.set(n_info.id, {
|
|
|
|
|
+ id: String(n_info.id),
|
|
|
|
|
+ data: toNodeData(n_info)
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+ // 目标节点 m
|
|
|
|
|
+ if (m_info && !nodeMap.has(m_info.id)) {
|
|
|
|
|
+ nodeMap.set(m_info.id, {
|
|
|
|
|
+ id: String(m_info.id),
|
|
|
|
|
+ data: toNodeData(m_info)
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+ // 关系 r
|
|
|
|
|
+ if (r_info && !edgeMap.has(r_info.id)) {
|
|
|
|
|
+ edgeMap.set(r_info.id, {
|
|
|
|
|
+ id: String(r_info.elementId),
|
|
|
|
|
+ source: String(n_info.id),
|
|
|
|
|
+ target: String(m_info.id),
|
|
|
|
|
+ label: formatEdgeLabel(r_info),
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ return {
|
|
|
|
|
+ nodes: Array.from(nodeMap.values()),
|
|
|
|
|
+ edges: Array.from(edgeMap.values())
|
|
|
|
|
+ };
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
/**
|
|
/**
|
|
|
* 根据节点名称模糊查询节点信息
|
|
* 根据节点名称模糊查询节点信息
|
|
|
|
|
+ * 按模式区分检索范围:
|
|
|
|
|
+ * - 普通模式(智能分析/智能组卷):Book/Part/Chapter/Section/Mu 结构节点 + 全部实体节点
|
|
|
|
|
+ * - 能力画像模式:维度 + 能力项(懂/会/通/创)+ 全部实体节点
|
|
|
*/
|
|
*/
|
|
|
- async searchNodesByName(neo4jInstance, keyword) {
|
|
|
|
|
|
|
+ async searchNodesByName(neo4jInstance, keyword, includeAbility = false) {
|
|
|
|
|
+ const typeFilter = includeAbility
|
|
|
|
|
+ ? `(node:AbilityDimension OR node:Ability OR node:Entity)`
|
|
|
|
|
+ : `(node:Book OR node:Part OR node:Chapter OR node:Section OR node:Mu OR node:Entity)`;
|
|
|
// 1. 同款 Cypher 改造:在数据库直接把数据组装成干净的 JSON 对象
|
|
// 1. 同款 Cypher 改造:在数据库直接把数据组装成干净的 JSON 对象
|
|
|
const cypher = `
|
|
const cypher = `
|
|
|
MATCH (node)
|
|
MATCH (node)
|
|
|
- WHERE (node:Book OR node:Part OR node:Chapter OR node:EntityType OR node:Entity)
|
|
|
|
|
|
|
+ WHERE ${typeFilter}
|
|
|
AND node.name CONTAINS $keyword
|
|
AND node.name CONTAINS $keyword
|
|
|
WITH collect(node) AS items
|
|
WITH collect(node) AS items
|
|
|
WITH items[0..100] AS nodesList
|
|
WITH items[0..100] AS nodesList
|
|
@@ -311,7 +631,7 @@ export const graphService = {
|
|
|
|
|
|
|
|
// 寻找这些节点之间的内部上下级/横向关系
|
|
// 寻找这些节点之间的内部上下级/横向关系
|
|
|
OPTIONAL MATCH (n)-[r]->(m)
|
|
OPTIONAL MATCH (n)-[r]->(m)
|
|
|
- WHERE m IN nodesList
|
|
|
|
|
|
|
+ WHERE m IN nodesList AND NOT type(r) IN $hidden
|
|
|
|
|
|
|
|
// 同款 RETURN 打包语法糖,把所有需要的字段一口气全部暴露出来
|
|
// 同款 RETURN 打包语法糖,把所有需要的字段一口气全部暴露出来
|
|
|
RETURN
|
|
RETURN
|
|
@@ -328,7 +648,8 @@ export const graphService = {
|
|
|
|
|
|
|
|
// 2. 使用你的自定义注入插件执行查询
|
|
// 2. 使用你的自定义注入插件执行查询
|
|
|
const records = await neo4jInstance.execute(cypher, {
|
|
const records = await neo4jInstance.execute(cypher, {
|
|
|
- keyword
|
|
|
|
|
|
|
+ keyword,
|
|
|
|
|
+ hidden: HIDDEN_REL_TYPES
|
|
|
});
|
|
});
|
|
|
// console.log("模糊搜索原始 records:", records);
|
|
// console.log("模糊搜索原始 records:", records);
|
|
|
|
|
|
|
@@ -376,7 +697,7 @@ export const graphService = {
|
|
|
id: String(r_info.elementId),
|
|
id: String(r_info.elementId),
|
|
|
source: String(n_info.id), // 连线起点
|
|
source: String(n_info.id), // 连线起点
|
|
|
target: String(m_info.id), // 连线终点
|
|
target: String(m_info.id), // 连线终点
|
|
|
- label: r_info.type || r_info.props.label || '相关',
|
|
|
|
|
|
|
+ label: formatEdgeLabel(r_info),
|
|
|
// sources: r_info.props?.sources ? r_info.props.sources : "[]",
|
|
// sources: r_info.props?.sources ? r_info.props.sources : "[]",
|
|
|
});
|
|
});
|
|
|
}
|
|
}
|
|
@@ -401,16 +722,21 @@ export const graphService = {
|
|
|
`;
|
|
`;
|
|
|
const records = await neo4jInstance.execute(cypher);
|
|
const records = await neo4jInstance.execute(cypher);
|
|
|
const props = records[0]?.props || {};
|
|
const props = records[0]?.props || {};
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
|
|
+ // 能力画像节点(维度/能力项/层次)用 definition/knowledge_support 等属性展示
|
|
|
const result = [{
|
|
const result = [{
|
|
|
- mu_id: props.mu_id || '',
|
|
|
|
|
|
|
+ mu_id: props.mu_id || props.dimension_id || props.ability_id || props.code || '',
|
|
|
name: props.name || '',
|
|
name: props.name || '',
|
|
|
- path: props.path || '',
|
|
|
|
|
- text: props.description || props.text || '',
|
|
|
|
|
|
|
+ path: props.path
|
|
|
|
|
+ || [props.core_position, props.knowledge_module, props.domain_tag].filter(Boolean).join(' · ')
|
|
|
|
|
+ || '',
|
|
|
|
|
+ text: props.description || props.text || props.definition
|
|
|
|
|
+ || [props.level && `层次:${props.level}`, props.bloom && `Bloom:${props.bloom}`, props.knowledge_support].filter(Boolean).join('\n')
|
|
|
|
|
+ || '',
|
|
|
menuVisible: false,
|
|
menuVisible: false,
|
|
|
menuStyle: {}
|
|
menuStyle: {}
|
|
|
}];
|
|
}];
|
|
|
-
|
|
|
|
|
|
|
+
|
|
|
return JSON.stringify(result);
|
|
return JSON.stringify(result);
|
|
|
},
|
|
},
|
|
|
/**
|
|
/**
|
|
@@ -420,21 +746,41 @@ export const graphService = {
|
|
|
const cypher = `
|
|
const cypher = `
|
|
|
MATCH (n)-[r]->(m)
|
|
MATCH (n)-[r]->(m)
|
|
|
WHERE elementId(r) = $edgeId
|
|
WHERE elementId(r) = $edgeId
|
|
|
- RETURN r.source_sentence as source_sentence, r.slice_id as slice_id,r.chapter as chapter,r.slice_text as slice_text
|
|
|
|
|
|
|
+ RETURN r.source_sentence as source_sentence, r.slice_id as slice_id, r.chapter as chapter, r.slice_text as slice_text,
|
|
|
|
|
+ properties(r) AS props, type(r) AS rel_type
|
|
|
`;
|
|
`;
|
|
|
const sources = []
|
|
const sources = []
|
|
|
const records = await neo4jInstance.execute(cypher, {
|
|
const records = await neo4jInstance.execute(cypher, {
|
|
|
edgeId
|
|
edgeId
|
|
|
});
|
|
});
|
|
|
- // console.log(records,999)
|
|
|
|
|
- if (!records[0].source_sentence || !records[0].slice_id || !records[0].chapter || !records[0].slice_text) {
|
|
|
|
|
- return "[]";
|
|
|
|
|
- } else {
|
|
|
|
|
- sources.push(records[0]);
|
|
|
|
|
|
|
+ const rec = records[0];
|
|
|
|
|
+ if (!rec) return "[]";
|
|
|
|
|
+
|
|
|
|
|
+ // 知识图谱边:有原文切片属性,直接返回
|
|
|
|
|
+ if (rec.source_sentence || rec.slice_id || rec.chapter || rec.slice_text) {
|
|
|
|
|
+ sources.push(rec);
|
|
|
|
|
+ return JSON.stringify(sources);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 能力画像边(支撑能力/考查能力等):展示边属性
|
|
|
|
|
+ // 样例: {confidence:"高", level:"会", bloom_level:"L3", knowledge_domain:"质量", reasoning:"..."}
|
|
|
|
|
+ const p = rec.props || {};
|
|
|
|
|
+ if (Object.keys(p).length > 0) {
|
|
|
|
|
+ sources.push({
|
|
|
|
|
+ mu_id: rec.rel_type,
|
|
|
|
|
+ name: p.confidence ? `置信度:${p.confidence}` : '关系属性',
|
|
|
|
|
+ path: [
|
|
|
|
|
+ p.level && `层次:${p.level}`,
|
|
|
|
|
+ p.bloom_level && `Bloom:${p.bloom_level}`,
|
|
|
|
|
+ p.knowledge_domain && `领域:${p.knowledge_domain}`,
|
|
|
|
|
+ p.derived === true ? 'derived' : null,
|
|
|
|
|
+ p.via_entity && `经由实体:${p.via_entity}`
|
|
|
|
|
+ ].filter(Boolean).join(' · '),
|
|
|
|
|
+ text: p.reasoning || JSON.stringify(p)
|
|
|
|
|
+ });
|
|
|
return JSON.stringify(sources);
|
|
return JSON.stringify(sources);
|
|
|
}
|
|
}
|
|
|
- // if(records[0])
|
|
|
|
|
- // return records[0].props.sources|| "[]";
|
|
|
|
|
|
|
+ return "[]";
|
|
|
},
|
|
},
|
|
|
/**
|
|
/**
|
|
|
* 根据实体id查询MU中所有信息
|
|
* 根据实体id查询MU中所有信息
|
|
@@ -621,5 +967,26 @@ export const graphService = {
|
|
|
} else {
|
|
} else {
|
|
|
return []
|
|
return []
|
|
|
}
|
|
}
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 查询库内可展开节点总数
|
|
|
|
|
+ * 按模式区分统计口径:
|
|
|
|
|
+ * - 普通模式(智能分析/智能组卷):Book/Part/Chapter/Section/Mu 结构节点 + 全部实体节点
|
|
|
|
|
+ * - 能力画像模式:维度 + 能力项(懂/会/通/创)+ 全部实体节点
|
|
|
|
|
+ * @param {Object} neo4jInstance Neo4j 驱动实例
|
|
|
|
|
+ * @param {boolean} includeAbility 是否能力画像模式
|
|
|
|
|
+ * @returns {Promise<number>} 节点总数
|
|
|
|
|
+ */
|
|
|
|
|
+ async countAllNodes(neo4jInstance, includeAbility = false) {
|
|
|
|
|
+ const cypher = includeAbility
|
|
|
|
|
+ ? `MATCH (n)
|
|
|
|
|
+ WHERE (n:AbilityDimension OR n:Ability OR n:Entity)
|
|
|
|
|
+ RETURN count(n) AS total`
|
|
|
|
|
+ : `MATCH (n)
|
|
|
|
|
+ WHERE (n:Book OR n:Part OR n:Chapter OR n:Section OR n:Mu OR n:Entity)
|
|
|
|
|
+ RETURN count(n) AS total`;
|
|
|
|
|
+ const records = await neo4jInstance.execute(cypher);
|
|
|
|
|
+ return records[0]?.total || 0;
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|