昨天我花了点时间使用 cursor 写了一个简单的增强本地图谱的代码片段,也给大家分享一下
主要实现了两个功能:
- 本地图谱支持深度
- 出链也会计算权重
效果
代码片段关闭:
代码片段开启:
这个片段可以用来干什么
- 一个用于快速跳转的路径地图(这个用处最大了,文档树、搜索、反链面板都去一边吧)
- 打开一个文档,就可以通过关系图谱快速记起相关联项目的结构
本来还编了很多的,想了想还是删了。
代码
(function() {
// 预设配置 - 可根据需要调整
const 预设配置 = {
默认: {
最大深度: 4, // 建议3-4层深度对大多数情况已足够
每层最大节点数: 25, // 每层处理的最大节点数
并发请求数: 8, // 同时发送的最大请求数
请求间隔: 0, // 请求间隔(毫秒),0表示不延迟
最大处理总数: 120 // 处理节点上限
},
高速: {
最大深度: 4, // 限制深度提高响应速度
每层最大节点数: 15, // 减少每层节点数加快处理
并发请求数: 15, // 大幅提高并发
请求间隔: 0, // 无延迟
最大处理总数: 100 // 减少处理总数提高速度
},
全面: {
最大深度: 5, // 增加深度获取更多关联
每层最大节点数: 30, // 增加每层节点数获取更全面信息
并发请求数: 6, // 降低并发减轻服务器压力
请求间隔: 50, // 添加少量延迟
最大处理总数: 150 // 处理更多节点
}
};
// 当前使用的配置(默认使用"高速"配置)
const 当前模式 = "高速";
// 运行配置
const 配置 = {
...预设配置[当前模式],
已处理节点: new Set(), // 已处理节点集合
已请求节点: new Set(), // 已请求节点集合
正在处理: false, // 处理标记
显示调试信息: true, // 是否显示调试信息
节点缓存: new Map(), // 节点请求结果缓存
处理计数: 0, // 处理计数器
双向调整权重: true, // 是否为引用和被引用节点都调整大小
默认节点大小: 15, // 默认节点大小
引用权重系数: 1.0, // 引用其他节点时的权重系数
被引用权重系数: 1.2, // 被引用节点时的权重系数(略高于引用权重)
使用线性计算: true, // 使用线性计算而非对数计算
大小增长系数: 0.7, // 节点大小增长系数(值越大,差异越明显)
尺寸上限倍数: 5.0, // 节点最大尺寸为默认尺寸的倍数
强化对比度: true, // 进一步强化节点大小差异
基础增长倍数: 1.5 // 即使只有1个引用也会有的基础增长
};
// 保存原始fetch函数
const 原始Fetch = window.fetch;
// 拦截fetch请求
window.fetch = async function(...args) {
const url = args[0];
// 只拦截关系图请求
if (typeof url === 'string' && url.includes('/api/graph/getLocalGraph')) {
// 避免递归处理
if (配置.正在处理) {
return 原始Fetch.apply(this, args);
}
try {
配置.正在处理 = true;
// 获取基础数据
const 响应 = await 原始Fetch.apply(this, args);
const 响应克隆 = 响应.clone();
const 原始数据 = await 响应克隆.json();
// 获取当前文档ID
const 请求体 = JSON.parse(args[1].body);
const 根节点ID = 请求体.id;
// 重置状态
配置.已处理节点.clear();
配置.已请求节点.clear();
配置.节点缓存.clear();
配置.处理计数 = 0;
if (配置.显示调试信息) {
console.log(`[图谱增强${当前模式}] 开始处理根节点`);
console.time('图谱处理用时');
}
// 初始化根节点
配置.已处理节点.add(根节点ID);
配置.已请求节点.add(根节点ID);
配置.处理计数++;
// 准备数据结构 - 直接使用Map提高查找效率
const 节点映射 = new Map(原始数据.data.nodes.map(节点 => [节点.id, 节点]));
const 连线映射 = new Map();
// 引用计数器 - 记录每个节点的引用和被引用次数
const 引用计数 = new Map();
// 初始化引用计数
for (const 节点 of 原始数据.data.nodes) {
引用计数.set(节点.id, { 引用数: 0, 被引用数: 0 });
}
// 记录连线 - 使用Set加速
const 连线集合 = new Set();
原始数据.data.links.forEach(连线 => {
const 连线键 = `${连线.from}-${连线.to}`;
连线映射.set(连线键, 连线);
连线集合.add(连线键);
// 更新引用计数
if (引用计数.has(连线.from)) {
const 计数 = 引用计数.get(连线.from);
计数.引用数 += 1;
引用计数.set(连线.from, 计数);
}
if (引用计数.has(连线.to)) {
const 计数 = 引用计数.get(连线.to);
计数.被引用数 += 1;
引用计数.set(连线.to, 计数);
}
});
// 快速找出初始关联节点
const 初始关联节点 = [];
for (const 连线 of 原始数据.data.links) {
const 相关节点 = 连线.from === 根节点ID ? 连线.to :
连线.to === 根节点ID ? 连线.from : null;
if (相关节点 && !配置.已请求节点.has(相关节点)) {
初始关联节点.push(相关节点);
配置.已请求节点.add(相关节点);
}
}
// 高性能处理所有层级
await 高速处理(初始关联节点, 节点映射, 连线映射, 连线集合, 引用计数, 请求体.conf);
// 计算最终节点大小
if (配置.双向调整权重) {
重新计算节点大小(节点映射, 引用计数);
}
if (配置.显示调试信息) {
console.timeEnd('图谱处理用时');
console.log(`[图谱增强${当前模式}] 完成: ${节点映射.size}节点, ${连线映射.size}连接, 处理${配置.处理计数}节点`);
}
// 构建新响应
const 新数据 = {
...原始数据,
data: {
...原始数据.data,
nodes: Array.from(节点映射.values()),
links: Array.from(连线映射.values())
}
};
return new Response(JSON.stringify(新数据), {
status: 响应.status,
statusText: 响应.statusText,
headers: 响应.headers
});
} finally {
配置.正在处理 = false;
}
}
return 原始Fetch.apply(this, args);
};
/**
* 从连线中提取相关节点
*/
function 获取关联节点(节点ID, 连线数组, 已访问集合) {
const 关联节点 = [];
for (const 连线 of 连线数组) {
if (连线.from === 节点ID && !已访问集合.has(连线.to)) {
关联节点.push(连线.to);
} else if (连线.to === 节点ID && !已访问集合.has(连线.from)) {
关联节点.push(连线.from);
}
}
return 关联节点;
}
/**
* 高性能处理所有层级节点
*/
async function 高速处理(初始节点集, 节点映射, 连线映射, 连线集合, 引用计数, 图谱配置) {
let 当前深度 = 1;
let 当前层节点 = [...初始节点集];
// 非递归批量处理多层节点
while (
当前深度 < 配置.最大深度 &&
当前层节点.length > 0 &&
配置.处理计数 < 配置.最大处理总数
) {
if (配置.显示调试信息) {
console.log(`[图谱增强${当前模式}] 处理第${当前深度}层: ${当前层节点.length}个节点, 已处理${配置.处理计数}个`);
}
// 限制本层处理量
const 本层节点 = 当前层节点.slice(0, 配置.每层最大节点数);
const 下一层节点集 = new Set();
// 高并发批处理
for (let i = 0; i < 本层节点.length; i += 配置.并发请求数) {
// 分批处理,每批并发请求
const 批次节点 = 本层节点.slice(i, i + 配置.并发请求数);
// 并行处理当前批次
const 批处理结果 = await Promise.all(
批次节点.map(async (节点ID) => {
// 跳过已处理
if (配置.已处理节点.has(节点ID) || 配置.处理计数 >= 配置.最大处理总数) {
return null;
}
try {
// 标记处理
配置.已处理节点.add(节点ID);
配置.处理计数++;
// 优先使用缓存
if (配置.节点缓存.has(节点ID)) {
return 配置.节点缓存.get(节点ID);
}
// 发送请求
const 请求数据 = {
type: "local",
k: "",
id: 节点ID,
conf: 图谱配置
};
const 响应 = await fetch('/api/graph/getLocalGraph', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(请求数据)
});
const 响应数据 = await 响应.json();
// 处理数据
if (响应数据?.code === 0 && 响应数据.data) {
// 提取下一级关联节点
const 相关节点 = [];
// 快速合并节点
for (const 节点 of 响应数据.data.nodes) {
if (!节点映射.has(节点.id)) {
节点映射.set(节点.id, 节点);
// 初始化引用计数
if (!引用计数.has(节点.id)) {
引用计数.set(节点.id, { 引用数: 0, 被引用数: 0 });
}
}
}
// 快速合并连线并收集关联节点
for (const 连线 of 响应数据.data.links) {
const 连线键 = `${连线.from}-${连线.to}`;
if (!连线集合.has(连线键)) {
连线集合.add(连线键);
连线映射.set(连线键, 连线);
// 更新引用计数
if (引用计数.has(连线.from)) {
const 计数 = 引用计数.get(连线.from);
计数.引用数 += 1;
引用计数.set(连线.from, 计数);
} else {
引用计数.set(连线.from, { 引用数: 1, 被引用数: 0 });
}
if (引用计数.has(连线.to)) {
const 计数 = 引用计数.get(连线.to);
计数.被引用数 += 1;
引用计数.set(连线.to, 计数);
} else {
引用计数.set(连线.to, { 引用数: 0, 被引用数: 1 });
}
// 提取关联节点
const 目标节点 = 连线.from === 节点ID ? 连线.to :
连线.to === 节点ID ? 连线.from : null;
if (目标节点 && !配置.已请求节点.has(目标节点)) {
相关节点.push(目标节点);
配置.已请求节点.add(目标节点);
}
}
}
// 缓存结果
配置.节点缓存.set(节点ID, 相关节点);
return 相关节点;
}
return null;
} catch (错误) {
return null;
}
})
);
// 合并到下一层
for (const 结果 of 批处理结果) {
if (结果 && Array.isArray(结果)) {
结果.forEach(id => 下一层节点集.add(id));
}
}
// 批次间可选延迟
if (配置.请求间隔 > 0 && i + 配置.并发请求数 < 本层节点.length) {
await new Promise(resolve => setTimeout(resolve, 配置.请求间隔));
}
}
// 准备下一层
当前层节点 = Array.from(下一层节点集);
当前深度++;
}
}
/**
* 重新计算节点大小,同时考虑引用和被引用因素,使用更合理的计算方式
* 增强节点大小差异,使重要节点更加突出
*/
function 重新计算节点大小(节点映射, 引用计数) {
// 找出最大的引用数和被引用数,用于归一化
let 最大引用数 = 1;
let 最大被引用数 = 1;
for (const 计数 of 引用计数.values()) {
最大引用数 = Math.max(最大引用数, 计数.引用数);
最大被引用数 = Math.max(最大被引用数, 计数.被引用数);
}
// 防止除零
最大引用数 = Math.max(1, 最大引用数);
最大被引用数 = Math.max(1, 最大被引用数);
for (const [节点ID, 节点对象] of 节点映射.entries()) {
if (引用计数.has(节点ID)) {
const 计数 = 引用计数.get(节点ID);
let 引用得分, 被引用得分;
if (配置.使用线性计算) {
// 线性计算得分 - 更公平地表示引用数量
引用得分 = (计数.引用数 / 最大引用数) * 配置.引用权重系数;
被引用得分 = (计数.被引用数 / 最大被引用数) * 配置.被引用权重系数;
// 强化对比度 - 让即使很少引用的节点也有明显变化
if (配置.强化对比度 && (计数.引用数 > 0 || 计数.被引用数 > 0)) {
引用得分 = 配置.基础增长倍数 * 引用得分 / (引用得分 + 0.3);
被引用得分 = 配置.基础增长倍数 * 被引用得分 / (被引用得分 + 0.3);
}
} else {
// 对数计算得分 - 平滑处理引用数较多的情况
引用得分 = Math.log2(计数.引用数 + 1) / Math.log2(最大引用数 + 1) * 配置.引用权重系数;
被引用得分 = Math.log2(计数.被引用数 + 1) / Math.log2(最大被引用数 + 1) * 配置.被引用权重系数;
}
// 合并得分,计算最终尺寸
const 总得分 = 引用得分 + 被引用得分;
// 使用更激进的计算方式增大差异
let 尺寸系数;
if (配置.强化对比度) {
// 指数增长模式,让差异更明显
尺寸系数 = Math.pow(1 + 总得分, 配置.大小增长系数 * 2);
} else {
// 缩放到合理范围内
尺寸系数 = 1 + (总得分 * 配置.大小增长系数 * 配置.尺寸上限倍数);
}
// 限制最大尺寸
const 最终系数 = Math.min(尺寸系数, 配置.尺寸上限倍数);
// 应用新尺寸
节点对象.size = 配置.默认节点大小 * 最终系数;
// 添加引用和被引用数据便于调试
节点对象.refs = 计数.引用数;
节点对象.defs = 计数.被引用数;
// 如果是调试模式,为节点添加权重信息
if (配置.显示调试信息 && (计数.引用数 > 0 || 计数.被引用数 > 0)) {
const 原标签 = 节点对象.label;
节点对象.title = `${原标签}\n引用: ${计数.引用数}, 被引用: ${计数.被引用数}`;
}
}
}
}
// 显示已启用通知
console.log(`[图谱深度增强] 已启用${当前模式}模式: 深度${配置.最大深度}, 节点上限${配置.最大处理总数}, 强化对比${配置.强化对比度}`);
// 添加样式
const 样式 = document.createElement('style');
样式.textContent = `
.graph-depth-notification {
position: fixed;
bottom: 20px;
right: 20px;
background-color: rgba(0, 0, 0, 0.75);
color: white;
padding: 12px 16px;
border-radius: 6px;
z-index: 9999;
font-size: 14px;
transition: opacity 0.5s;
opacity: 1;
}
.graph-depth-notification.fade {
opacity: 0;
}
`;
document.head.appendChild(样式);
// 创建通知
const 通知 = document.createElement('div');
通知.className = 'graph-depth-notification';
通知.textContent = `思源笔记关系图谱深度增强已启用 (${当前模式}模式, 强化节点差异)`;
document.body.appendChild(通知);
setTimeout(() => {
通知.classList.add('fade');
setTimeout(() => 通知.remove(), 500);
}, 3000);
})();
再放个不会修改节点权重的旧版本
(function() {
// 预设配置 - 可根据需要调整
const 预设配置 = {
默认: {
最大深度: 4, // 建议3-4层深度对大多数情况已足够
每层最大节点数: 25, // 每层处理的最大节点数
并发请求数: 8, // 同时发送的最大请求数
请求间隔: 0, // 请求间隔(毫秒),0表示不延迟
最大处理总数: 120 // 处理节点上限
},
高速: {
最大深度: 4, // 限制深度提高响应速度
每层最大节点数: 15, // 减少每层节点数加快处理
并发请求数: 15, // 大幅提高并发
请求间隔: 0, // 无延迟
最大处理总数: 100 // 减少处理总数提高速度
},
全面: {
最大深度: 5, // 增加深度获取更多关联
每层最大节点数: 30, // 增加每层节点数获取更全面信息
并发请求数: 6, // 降低并发减轻服务器压力
请求间隔: 50, // 添加少量延迟
最大处理总数: 150 // 处理更多节点
}
};
// 当前使用的配置(默认使用"高速"配置)
const 当前模式 = "高速";
// 运行配置
const 配置 = {
...预设配置[当前模式],
已处理节点: new Set(), // 已处理节点集合
已请求节点: new Set(), // 已请求节点集合
正在处理: false, // 处理标记
显示调试信息: true, // 是否显示调试信息
节点缓存: new Map(), // 节点请求结果缓存
处理计数: 0 // 处理计数器
};
// 保存原始fetch函数
const 原始Fetch = window.fetch;
// 拦截fetch请求
window.fetch = async function(...args) {
const url = args[0];
// 只拦截关系图请求
if (typeof url === 'string' && url.includes('/api/graph/getLocalGraph')) {
// 避免递归处理
if (配置.正在处理) {
return 原始Fetch.apply(this, args);
}
try {
配置.正在处理 = true;
// 获取基础数据
const 响应 = await 原始Fetch.apply(this, args);
const 响应克隆 = 响应.clone();
const 原始数据 = await 响应克隆.json();
// 获取当前文档ID
const 请求体 = JSON.parse(args[1].body);
const 根节点ID = 请求体.id;
// 重置状态
配置.已处理节点.clear();
配置.已请求节点.clear();
配置.节点缓存.clear();
配置.处理计数 = 0;
if (配置.显示调试信息) {
console.log(`[图谱增强${当前模式}] 开始处理根节点`);
console.time('图谱处理用时');
}
// 初始化根节点
配置.已处理节点.add(根节点ID);
配置.已请求节点.add(根节点ID);
配置.处理计数++;
// 准备数据结构 - 直接使用Map提高查找效率
const 节点映射 = new Map(原始数据.data.nodes.map(节点 => [节点.id, 节点]));
const 连线映射 = new Map();
// 记录连线 - 使用Set加速
const 连线集合 = new Set();
原始数据.data.links.forEach(连线 => {
const 连线键 = `${连线.from}-${连线.to}`;
连线映射.set(连线键, 连线);
连线集合.add(连线键);
});
// 快速找出初始关联节点
const 初始关联节点 = [];
for (const 连线 of 原始数据.data.links) {
const 相关节点 = 连线.from === 根节点ID ? 连线.to :
连线.to === 根节点ID ? 连线.from : null;
if (相关节点 && !配置.已请求节点.has(相关节点)) {
初始关联节点.push(相关节点);
配置.已请求节点.add(相关节点);
}
}
// 高性能处理所有层级
await 高速处理(初始关联节点, 节点映射, 连线映射, 连线集合, 请求体.conf);
if (配置.显示调试信息) {
console.timeEnd('图谱处理用时');
console.log(`[图谱增强${当前模式}] 完成: ${节点映射.size}节点, ${连线映射.size}连接, 处理${配置.处理计数}节点`);
}
// 构建新响应
const 新数据 = {
...原始数据,
data: {
...原始数据.data,
nodes: Array.from(节点映射.values()),
links: Array.from(连线映射.values())
}
};
return new Response(JSON.stringify(新数据), {
status: 响应.status,
statusText: 响应.statusText,
headers: 响应.headers
});
} finally {
配置.正在处理 = false;
}
}
return 原始Fetch.apply(this, args);
};
/**
* 从连线中提取相关节点
*/
function 获取关联节点(节点ID, 连线数组, 已访问集合) {
const 关联节点 = [];
for (const 连线 of 连线数组) {
if (连线.from === 节点ID && !已访问集合.has(连线.to)) {
关联节点.push(连线.to);
} else if (连线.to === 节点ID && !已访问集合.has(连线.from)) {
关联节点.push(连线.from);
}
}
return 关联节点;
}
/**
* 高性能处理所有层级节点
*/
async function 高速处理(初始节点集, 节点映射, 连线映射, 连线集合, 图谱配置) {
let 当前深度 = 1;
let 当前层节点 = [...初始节点集];
// 非递归批量处理多层节点
while (
当前深度 < 配置.最大深度 &&
当前层节点.length > 0 &&
配置.处理计数 < 配置.最大处理总数
) {
if (配置.显示调试信息) {
console.log(`[图谱增强${当前模式}] 处理第${当前深度}层: ${当前层节点.length}个节点, 已处理${配置.处理计数}个`);
}
// 限制本层处理量
const 本层节点 = 当前层节点.slice(0, 配置.每层最大节点数);
const 下一层节点集 = new Set();
// 高并发批处理
for (let i = 0; i < 本层节点.length; i += 配置.并发请求数) {
// 分批处理,每批并发请求
const 批次节点 = 本层节点.slice(i, i + 配置.并发请求数);
// 并行处理当前批次
const 批处理结果 = await Promise.all(
批次节点.map(async (节点ID) => {
// 跳过已处理
if (配置.已处理节点.has(节点ID) || 配置.处理计数 >= 配置.最大处理总数) {
return null;
}
try {
// 标记处理
配置.已处理节点.add(节点ID);
配置.处理计数++;
// 优先使用缓存
if (配置.节点缓存.has(节点ID)) {
return 配置.节点缓存.get(节点ID);
}
// 发送请求
const 请求数据 = {
type: "local",
k: "",
id: 节点ID,
conf: 图谱配置
};
const 响应 = await fetch('/api/graph/getLocalGraph', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(请求数据)
});
const 响应数据 = await 响应.json();
// 处理数据
if (响应数据?.code === 0 && 响应数据.data) {
// 提取下一级关联节点
const 相关节点 = [];
// 快速合并节点
for (const 节点 of 响应数据.data.nodes) {
if (!节点映射.has(节点.id)) {
节点映射.set(节点.id, 节点);
}
}
// 快速合并连线并收集关联节点
for (const 连线 of 响应数据.data.links) {
const 连线键 = `${连线.from}-${连线.to}`;
if (!连线集合.has(连线键)) {
连线集合.add(连线键);
连线映射.set(连线键, 连线);
// 提取关联节点
const 目标节点 = 连线.from === 节点ID ? 连线.to :
连线.to === 节点ID ? 连线.from : null;
if (目标节点 && !配置.已请求节点.has(目标节点)) {
相关节点.push(目标节点);
配置.已请求节点.add(目标节点);
}
}
}
// 缓存结果
配置.节点缓存.set(节点ID, 相关节点);
return 相关节点;
}
return null;
} catch (错误) {
return null;
}
})
);
// 合并到下一层
for (const 结果 of 批处理结果) {
if (结果 && Array.isArray(结果)) {
结果.forEach(id => 下一层节点集.add(id));
}
}
// 批次间可选延迟
if (配置.请求间隔 > 0 && i + 配置.并发请求数 < 本层节点.length) {
await new Promise(resolve => setTimeout(resolve, 配置.请求间隔));
}
}
// 准备下一层
当前层节点 = Array.from(下一层节点集);
当前深度++;
}
}
// 显示已启用通知
console.log(`[图谱深度增强] 已启用${当前模式}模式: 深度${配置.最大深度}, 并发${配置.并发请求数}, 节点上限${配置.最大处理总数}`);
// 添加样式
const 样式 = document.createElement('style');
样式.textContent = `
.graph-depth-notification {
position: fixed;
bottom: 20px;
right: 20px;
background-color: rgba(0, 0, 0, 0.75);
color: white;
padding: 12px 16px;
border-radius: 6px;
z-index: 9999;
font-size: 14px;
transition: opacity 0.5s;
opacity: 1;
}
.graph-depth-notification.fade {
opacity: 0;
}
`;
document.head.appendChild(样式);
// 创建通知
const 通知 = document.createElement('div');
通知.className = 'graph-depth-notification';
通知.textContent = `思源笔记关系图谱深度增强已启用 (${当前模式}模式)`;
document.body.appendChild(通知);
setTimeout(() => {
通知.classList.add('fade');
setTimeout(() => 通知.remove(), 500);
}, 3000);
})();
欢迎来到这里!
我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。
注册 关于