昨天我花了点时间使用 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); })();
欢迎来到这里!
我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。
注册 关于