基于 openclaw 自主实现 记忆系统 (SummerMemory) 零成本 · 毫秒级 · 干翻商业方案
🧠 SummerMemory — 我自己搭的 AI 记忆系统💡 零成本 · 全中文 · 毫秒级 · 干翻商业方案SummerMemory 记忆系统采用PostgreSQL+pgvector做向量数据库Ollama jina-embeddings-v2-base-zh + BM25 + RRF 排名融合jieba中文分词纯本地全免费。不比商业的记忆系统差，搜索记忆毫秒级，比网络延迟还小🎬 我为什么要做这个?我玩 AI 有一段时间了，日常用它改代码、写文档、总结日报、控制家里的智能设备。用着用着，我发现一个问题：它老是忘事。我前天告诉它"我看了一本书"，今天问它"我最近看了什么书"，它说不知道。我让它搜"我最近使用的前端技术"，它搜不到，但明明近期有让它优化 Vue 的代码。我算是发现了，它的记忆系统有三个问题：中文搜不准、不懂变通、搜不到。所以我就自己做了一个，我称它为 SummerMemory。🎯 这个项目是干什么的？简单说：让 AI 记住事情，而且能准确找到。❌ OpenClaw 记忆系统的问题第一个：中文搜不准比如存了"我买了苹果和香蕉"，但日后搜"水果"搜不到。因为字面上没有"水果"这两个字。第二个：不懂变通存了"我开车去上班"，搜"通勤"搜不到。因为它不知道"开车去上班"就是"通勤"。✅ SummerMemory 怎么解决？我用了这些技术：PostgreSQL+pgvector 做向量数据库，Ollama jina-embeddings-v2-base-zh 向量召回 + BM25 精确召回 + RRF 排名融合，jieba 中文分词，纯本地全免费。搜索平均 78ms，比网络延迟还小。核心思路：把文字变成数字，用数学方法算相似度。🔬 技术拆解第一步：什么是"向量"？向量就是一组数字。电脑不认识字，但认识数字。比如："开心" → [0.25, -0.34, 0.56, ...]"愉快" → [0.23, -0.33, 0.55, ...]"快乐" → [0.26, -0.35, 0.57, ...]"悲伤" → [-0.80, 0.45, -0.23, ...]你看，"开心"、"愉快"、"快乐"的数字很接近，因为意思差不多。"悲伤"的数字差很远，因为意思不一样。这就是向量搜索：意思相近的文字，数字也相近。第二步：什么是"分词"？中文没有空格，得靠工具拆分。jieba 能把一句话拆成最小的单位。比如"我昨天去超市买了苹果和香蕉"，拆成：["我", "昨天", "去", "超市", "买", "了", "苹果", "和", "香蕉"]中文词语讲究成双成对，"苹果"是一个词，"超市"是一个词。但"还买"不是一个词，jieba 会把它拆开。第三步：什么是"BM25"？BM25 是一种搜索算法，看一个词出现多少次。你搜"苹果"，文章里"苹果"出现 5 次的，比出现 1 次的分数高。还有一个聪明的地方：如果一个词在所有文章里都出现（比如"的"、"是"），重要性就低。如果一个词只在少数文章里出现（比如"苹果"），重要性就高。第四步：什么是"混合搜索"？向量搜索能理解语义，但不够精确。BM25 搜索能精确匹配，但不懂语义。所以 SummerMemory 把两种结合起来：同时用两种方式搜索合并结果向量分占 60%，BM25 分占 40%按总分排序这样既能理解语义，又能精确匹配。第五步：什么是"RRF 排名融合"？（实战迭代）最初博客里写的是"LLM 重排（计划做的）"。后来真的去做了，这条路踩满了坑：LLM 重排（qwen3:0.6b）：CPU 推理 12-30 秒/条，thinking 模式吃掉所有 token，输出不稳定——不可用BGE-Reranker（Cross-Encoder）：61ms/pair 可用，但扩大召回后发现真正的问题是 BM25 同分灾难——28 个文件分数完全一样，重排模型再准也救不了候选池随机截断最后从 Azure AI Search 官方文档找到了业界标准方案 RRF（Reciprocal Rank Fusion）：RRF_score = Σ 1/(rank + 60)不看原始分数，只看排名位置。28 个 BM25 同分文件按排名天然区分开，向量高分也不会被同分淹没。Elasticsearch、Azure、Vespa 全在用。最终架构（纯 RRF，无重排）：向量 top 30 ──→ RRF: 1/(rank+60) ──┐ ├─ 同文件求和 ──→ 去重 top N ──→ 返回 BM25 top 50 ──→ RRF: 1/(rank+60) ──┘平均 78ms，P99 105ms，CPU 2.1%，内存 158MB。完整的踩坑过程见《搜索优化实战：从混合搜索到 RRF》。📝 举个完整的例子你跟 AI 说："我昨天去超市买了苹果和香蕉，还买了一盒牛奶，花了 50 块钱"SummerMemory 处理流程：第一步：切分切成三块："我昨天去超市买了苹果和香蕉""还买了一盒牛奶""花了 50 块钱"第二步：转向量把每块文字变成数字："苹果" → [0.12, -0.34, 0.56, ...]"香蕉" → [0.11, -0.33, 0.55, ...]"牛奶" → [0.23, -0.45, 0.67, ...]"苹果"和"香蕉"的数字很接近，因为都是水果。"牛奶"的数字差远一点，因为是饮料。第三步：分词jieba 这个框架常用于电商的关键字匹配搜索，分词可以更好地匹配用户的搜索意图。["我", "昨天", "去", "超市", "买", "了", "苹果", "和", "香蕉"]🦙 jina-embeddings-v2-base-zh 是什么？这是 Ollama 里用的一个模型，专门用来把中文变成向量。它的名字可以拆开看：Jina AI：开发这个模型的公司embeddings：把文字变成向量的模型类型v2：第二代模型base：基础版本，平衡精度和性能zh：专门针对中文优化为什么要用这个模型？它是专门针对中文优化的，中文效果比英文模型好8192 token context，支持长文本不截断768 维向量，精度更高Q5_K_M 量化后仅 116MB，CPU 就能跑，不需要 GPU它是开源免费的，不用花钱🔍 搜索流程演示过几天，你问 AI："我之前买了什么水果？"第一步：查询转向量"水果" → [0.15, -0.38, 0.52, ...]第二步：向量搜索"苹果"的向量 [0.12, -0.34, 0.56, ...] 和"水果"很接近。得分：0.85第三步：BM25 搜索查询分词：["我", "之前", "买", "了", "什么", "水果"]原文里有"我"、"买"、"了"，但没有"水果"。得分：0.60第四步：混合排序最终得分 = 0.85 × 0.6 + 0.60 × 0.4 = 0.75第五步：返回结果AI 回答："你之前买了苹果和香蕉，都是水果哦！"📊 性能数据指标数值说明📊 总文件数80 个记忆文件📦 总 chunks768 个分块后的记忆片段⚡ 搜索速度平均 78ms（P99 105ms）纯 RRF 架构实测🧬 向量维度768 维jina-embeddings-v2-base-zh💾 数据库大小~120MB768 条记录🔄 索引速度~2s/文件含 embedding 生成💰 总成本¥0纯本地，零费用🆚 SummerMemory vs OpenClaw 内置 vs 商业方案对比维度🧠 SummerMemory🔧 OpenClaw 内置💰 商业方案💰 成本✅ 完全免费✅ 免费❌ $70-500/月🏠 部署✅ 本地服务器部署✅ 本地❌ 云端托管🔒 隐私✅ 零泄露风险✅ 本地❌ 数据上传第三方📖 全文搜索✅ BM25 + jieba❌ 不支持✅ 有✂️ 中文分词✅ jieba 深度优化❌ 无⚠️ 通用分词🧬 向量维度✅ 768 维384 维768-1536 维🔗 混合搜索✅ 向量+BM25❌ 仅向量✅ 有（但要钱）⚡ 搜索速度✅ 12-425ms100-500ms50-200ms + 网络延迟📊 可视化✅ D3.js 力导向图❌ 无⚠️ 需额外付费🔧 自定义✅ 完全可控❌ 受限⚠️ API 受限🏗️ 服务架构三层服务，全部本地服务器运行🐘 PostgreSQL 18.1 + pgvector — Docker 容器，端口 5432，存储向量+全文数据🦙 Ollama — Docker 容器，端口 11434，提供 Embedding 推理🧠 SummerMemory Server — Python HTTP 服务，端口 11435，systemd 管理，开机自启，故障自愈数据规模: 80 文件 → 768 chunks → ~120MB🗺️ 图谱可视化我还做了一个图谱可视化界面，把所有记忆用一张图画出来。节点代表文件，连线代表关系。节点越大，内容越多；连线越粗，关系越紧密。支持搜索、拖拽、点击查看详情。用 Vue 3 + force-graph 做的，支持中英文切换。浏览器打开 https://ai.likefr.com/graph 就能看。💬 一句话总结🧠 SummerMemory = PostgreSQL + pgvector + Ollama + jieba + BM25纯本地 · 全免费 · 中文优化 · 毫秒级搜索 · 不比商业方案差！别人花 $500/月 买的记忆系统，我用 Docker 搞定了 🚀🗄️ 对话归档（2026-08 新增）记忆搜索管"记住知识"，对话归档管"记住对话"。新增 conversations 表把 OpenClaw 的聊天记录原样落库——纯归档、不进搜索索引，与 memories 表完全隔离，容量再大不影响搜索性能。双管道捕获：实时：internal hook 监听 message:received / message:sent，fire-and-forget 写入，不影响聊天兜底：sessions_watcher 2 秒轮询会话 jsonl 增量，负责 webchat AI 回复（源码级坑：webchat 走 chat.send 内部通道不发 message:sent 事件）+ 历史导入双路写入用 5 秒窗口去重。CLI 三个命令：python3 memory_system.py conv list # 列会话（带预览） python3 memory_system.py conv get <key> # 完整对话 python3 memory_system.py conv stats # 统计顺序保证：ORDER BY timestamp + 自增 id，会话内消息顺序和当时聊天顺序一致。88000+ 条历史消息一夜导入完成。🔗 开源地址SummerMemory 已经开源，欢迎 Star！ GitHub：https://github.com/Likefr/SummerMemory 在线 Demo：https://ai.likefr.com/graph 📅 诞生于 2026-05-30 | 🔄 08-16 RRF 搜索 + 对话归档 | 🔧 持续优化中 | 💪 Likefr × Summer
演示地址公司有个项目 类似于组态 但是网上的框架 样式不符合我们的需求 要么就是收费 不开源 于是 !!!我直接埋头苦干使用 Vue 纯Js 实现一个管道框架，用于管理管道、阀门和流动。该框架可以用于管道系统的模拟、管理和控制。[vplayer url="https://likefr.com/usr/uploads/2024/02/4186166042.mp4" /]一、框架结构该框架包含以下主要组件：阀门（Valve）： 用于控制管道流动的开关。管道（Pipe）： 用于连接不同的阀门，构成管道系统。流动（Flow）： 代表流体在管道中的运动。二. 数据结构这段json中包含了一些阀门 key（例如 el1, el2, el3 等） 后面的数字则是标签的id 阀门包含一些阀门与阀门关联关系和绑定的管道信息。// 阀门和管道的配置信息 export default { "el2": { // 阀门状态，true表示打开，false表示关闭 "checked": false, // 下一个管道ID的数组 "nextPipe": [], // 下一个阀门ID的数组 "nextValve": ['el1'], // 上一个阀门ID的数组 "lastValve": [] }, // 其他阀门和管道的配置信息... } 图式: "el49": { "checked": false, "nextPipe": ['p131', 'p116','p127', 'p114','p115'], "nextValve": ["el50"], "lastValve": [] }, "el50": { "checked": false, "nextPipe": [ 'p92','p91','p90','p89','p88','p87','p150','p86','p84','p85','p83','p82'], "nextValve": [], "lastValve": ["el49"] } }图式：每个阀门都有一些属性，例如 checked 属性表示当前阀门是否被打开，这样的设计很好地描述了阀门的状态，是否允许液体流动。在管道系统中，阀门的打开与关闭决定了流量是否可以流经该阀门。因此，通过控制 checked 属性，你可以有效地控制流量在管道系统中的流动。，nextPipe 表示下一个管道的名称，nextValve 表示下一个阀门的名称，lastValve 表示上一个阀门的名称，previousFlow 表示先前的流程信息。这样的数据结构很好的描述了一个复杂的管道系统，其中不同的元素通过管道相互连接三、模块功能管道配置初始化（init）： 初始化阀门状态和管道配置信息。更新UI（updateUI）： 根据阀门状态和管道配置更新用户界面。获取上游阀门状态（getLastIsFlow）： 递归获取上游阀门的状态，判断是否可以流动。四、方法说明initx()： 初始化阀门状态和管道配置信息。updateUI()： 更新阀门和管道的UI显示。getLastIsFlow()： 获取上游阀门的状态，判断是否可以流动。isClose() : 处理多个阀门共同绑定相同的管道时 导致的后者阀门才生效的问题核心代码结构:// 示例代码 (核心代码放后面 这里粗略过一眼) export default { data() { return { // 阀门工厂配置信息 valveFactory: { "el2": { "checked": false, "nextPipe": [], "nextValve": ['el1'], "lastValve": [] }, // 其他阀门配置信息... } } }, methods: { // 阀门管道初始化 initx() { // 初始化代码... }, // 更新UI updateUI() { // 更新UI代码... }, // 获取上游阀门状态 getLastIsFlow(item, valveFactory, res) { // 获取上游阀门状态代码... }, isClose(...) { } } } hongGeng.vue渲染层// 画布的一些初始化配置信息 export default { Width: 5000, Height: 5000, hleft: -1550, htop: -2000, Press: false, displacement: { scale: 1, zoom: this.Zoom }, getPointArr: [0, 0], init: null, inTagList: {} } }这段代码是画布的一些初始化配置信息，其中包括了画布的宽度（Width）、高度（Height）、水平偏移量（hleft）、垂直偏移量（htop）、按下状态（Press）、位移信息（displacement）、获取点数组（getPointArr）、初始化函数（init）以及标签列表（inTagList）等。具体来说：Width 和 Height 分别表示了画布的宽度和高度。hleft 和 htop 分别表示了画布相对于某个原点的水平和垂直偏移量。Press 表示画布是否处于按下状态。displacement 对象包含了位移的相关信息，包括比例尺（scale）和缩放比例（zoom）。getPointArr 是一个获取点数组。init 是一个初始化函数，可能用于初始化画布。inTagList 是一个标签列表对象。 /* * 初始化配置 * 初始化样式 * 仅仅加载pipeSet配置 * 详细 请看 pipe.vue -> props * */ initx() { let pipeSet = this.pipeSet let valveFactory = this.$parent.valveFactory || {} let allValve = [], allPipe = [], allPump = [], handPump = [] // 初始化阀门状态 for (let i in this.$children) { let row = this.$children[i] if (row.$el.className === 'hgValve') { row.rotatex = row.rotate allValve.push(row) } else if (row.$el.localName === 'svg') { // 存在层级的值 if (row.ZIndex) { row.zindex = row.ZIndex } allPipe.push(row) } else if (row.$el.className === 'pumpBox') { allPump.push(row) } else if (row.$el.className === 'handPump') { allPump.push(row) } let e = row.$el if (['hgValve', 'pumpBox', 'hgDevSwitch', 'hgSwitch', 'handPump'].includes(e.className)) { let id = e.id if (!id) continue if (!valveFactory[id]) continue row.checked = valveFactory[id].checked if (row.checked) { this.updateUI(valveFactory) } } } // 统计 阀门 管道 泵 DOM let inTagList = { 'allValve': allValve, 'allPipe': allPipe, 'allPump': allPump, 'allHandPump': handPump } this.inTagList = inTagList // 初始化配置 let list = inTagList.allPipe for (let i in list) { let PlayTheAnim = pipeSet.PlayTheAnim let BackgroundColor = pipeSet.BackgroundColor || null let StrokeLinecap = pipeSet.StrokeLinecap || 'square' let StrokeWidth = pipeSet.StrokeWidth || 5 let StrokeDasharray = pipeSet.StrokeDasharray || 20 let LineColor = pipeSet.LineColor || null let AnimVelocity = pipeSet.AnimVelocity || 2 let Direction = pipeSet.Direction || false let HideStaticPipe = pipeSet.HideStaticPipe || false let HideArrowhead = pipeSet.HideArrowhead // let HideArrowhead = true if (list[i].Custom) { if (list[i].BackgroundColor) { BackgroundColor = list[i].BackgroundColor } if (list[i].StrokeWidth) { StrokeWidth = list[i].StrokeWidth } // if (list[i].StrokeLinecap) { // StrokeLinecap = list[i].StrokeLinecap // } if (list[i].HideArrowhead !== undefined) { HideArrowhead = list[i].HideArrowhead } if (list[i].StrokeDasharray) { StrokeDasharray = list[i].StrokeDasharray } if (list[i].LineColor) { LineColor = list[i].LineColor } if (list[i].AnimVelocity) { AnimVelocity = list[i].AnimVelocity } // 默认会显示 所以先注释掉 if (list[i].HideStaticPipe !== undefined) { HideStaticPipe = list[i].HideStaticPipe } // 处理 开启动画后是否显示流动效果 if (list[i].PlayTheAnim !== undefined) { PlayTheAnim = list[i].PlayTheAnim HideStaticPipe = false // 处理 关闭动画后是否显示流动效果 if (!PlayTheAnim) { if (list[i].HideStaticPipe !== undefined) { HideStaticPipe = list[i].HideStaticPipe } } } if (list[i].Direction !== undefined || list[i].Direction !== null) { Direction = list[i].Direction } } // 开启全部动画配置 if (this.Press) { list[i].playTheAnim = false } else if (PlayTheAnim !== undefined) { list[i].playTheAnim = PlayTheAnim } list[i].direction = Direction list[i].hideArrowhead = HideArrowhead list[i].strokeLinecap = StrokeLinecap list[i].strokeWidth = StrokeWidth list[i].strokeDasharray = StrokeDasharray if (AnimVelocity === null || AnimVelocity === undefined) { AnimVelocity = 2 } list[i].animVelocity = AnimVelocity list[i].lineColor = LineColor list[i].hideStaticPipe = HideStaticPipe if (BackgroundColor) { list[i].backgroundColor = BackgroundColor } if (this.pipeSet.PlayTheAnim) { list[i].playTheAnim = true list[i].hideStaticPipe = false } } console.debug(this.$parent.$options.name + ' load success!') }以上代码 初始化各种元素数组：allValve 存储所有阀门元素。allPipe 存储所有管道元素。allPump 存储所有泵元素。handPump 存储所有手动泵元素。遍历子元素进行初始化：通过遍历子元素（this.$children），将阀门、管道、泵等元素分类，并初始化它们的相关状态，例如旋转角度、层级等。 统计标签列表：创建一个包含各种元素的标签列表对象 inTagList，其中包括了阀门、管道、泵等不同类型的元素数组。 初始化管道配置：从配置信息 pipeSet 中获取一系列管道的外观配置，例如动画状态、背景颜色、线条宽度、线条样式、颜色等等。遍历所有管道元素，根据每个管道的个性化配置（如果有）来覆盖全局配置。 处理特殊情况：处理开启或关闭动画后是否显示流动效果（PlayTheAnim 和 HideStaticPipe）。处理是否隐藏箭头（HideArrowhead）。更新UI：通过调用 updateUI 方法，根据阀门工厂的状态更新UI。 其他处理：设置一些额外的配置，如动画速度、方向等。函数解析 (深度递归) /* * 时间复杂度O(n!) * ps: 当遇到 [Vue warn]: Error in v-on handler: "RangeError: Maximum call stack size exceeded" 时 * 该异常为死循环 会抛出此异常 * 请检查lastValve 是不是和当前的key重复了 * */ getLastIsFlow(item, valveFactory, res) { if (item.lastValve.length === 0 || !item.lastValve) return true // let isres = false // 查找 当前阀门的上面是否有被打开 for (let k in item.lastValve) { let row = valveFactory[item.lastValve[k]] if (!row) { console.warn('未声明 #id: ' + item.lastValve[k]) continue } if (row.checked) { // 解决 流动 指向多个同级阀门后者阀门导致管道流动异常停止流动的问题 2024 01 19 // return this.getLastIsFlow(valveFactory[item.lastValve[k]], valveFactory, res) res = this.getLastIsFlow(valveFactory[item.lastValve[k]], valveFactory, res) // 如果上一个阀门打开就直接结束循环 if (res === true) { return true } } } return res },这段代码是一个递归函数，用于检查当前阀门的上游阀门是否有打开的阀门getLastIsFlow 函数接收三个参数：item：当前阀门对象。valveFactory：阀门工厂对象，存储了所有阀门对象。res：初始值为 true，用于存储递归调用的结果。该函数的作用是检查当前阀门的上游阀门（即 lastValve 属性中指定的阀门）是否有被打开的阀门。如果有，那么当前阀门绑定的管道也应该流动函数首先检查 lastValve 是否为空或未定义，如果是则是根节点的fa'm，则直接返回 true然后遍历当前阀门的所有上游阀门，对每一个上游阀门进行如下查找：如果上游阀门未定义（可能由于配置错误或缺失），则在控制台输出警告信息(id未定义)，并继续下一个上游阀门的检查。如果上游阀门被打开（checked 属性为 true），则递归调用 getLastIsFlow 函数，传入上游阀门对象和 valveFactory 对象，并更新 res 的值为递归调用的结果。如果递归调用的结果为 true，则表示上游阀门中有被打开的阀门，当前阀门也应该被打开，于是函数返回 true。如果遍历完所有上游阀门后都没有发现被打开的阀门，则返回 res，表示当前阀门是否应该被打开的结果。总体来说，该函数的作用是检查当前阀门的上游阀门中是否有被打开的阀门，以确定当前阀门是否应该被打开。那么 说到递归 时间复杂度是?该段函数 经过了大多重写 优化 这段代码的时间复杂度取决于阀门之间的关系和阀门数量。由于函数中存在递归调用，需要考虑到递归调用的次数。假设有 n 个阀门，且它们之间的关系形成一个树结构，其中每个阀门都只有一个父阀门（即只有一个阀门指向当前阀门），并且最终的递归调用路径为一条直线。在这种情况下，每个阀门的 getLastIsFlow 函数最多会被调用一次，所以总的时间复杂度为 O(n)。然而，如果阀门之间存在复杂的循环或者多个父阀门指向同一个阀门的情况，递归调用的次数可能会增加，导致时间复杂度变高。总的来说，这段代码的时间复杂度在最坏情况下可能达到 O(n)，其中 n 是阀门的数量 // 防止共同阀门导致最后一个阀门才能控制对应管道 isClose(id, valveFactory) { return Object.values(valveFactory).some(valve => { if (valve.nextPipe.includes(id) && valve.checked) { return this.getLastIsFlow(valve, valveFactory, false); } return false; }); }, // 更新阀门与管道数据发生后的变化 updateUI() { // 属性克隆 const valveFactory = {...this.$parent.valveFactory}; // 配置 动画为关闭 if (this.pipeSet.PlayTheAnim) { return; } this.$nextTick(() => { Object.values(valveFactory).forEach(valve => { const lastOpen = this.getLastIsFlow(valve, valveFactory, false); const nextPipe = valve.nextPipe; if (valve.checked && lastOpen && nextPipe) { nextPipe.forEach(id => this.editPipeAnim(id, true, null)); } else if (nextPipe) { nextPipe.forEach(id => this.editPipeAnim(id, false, valveFactory)); } }); }); }, // 改变管道动画 pid 为管道id editPipeAnim(pid, playTheAnim, valveFactory) { const allPipe = this.getComponent.allPipe; // 获取dom实例 const pipe = allPipe.find(pipe => pipe.$attrs.id === pid); if (!pipe) return; if (valveFactory && this.isClose(pid, valveFactory)) return; pipe.playTheAnim = playTheAnim; pipe.direction = pipe.Direction; pipe.zindex = pipe.ZIndex === undefined || (playTheAnim ? 100 : 0); if (playTheAnim && this.pipeSet.HideStaticPipe) { pipe.hideStaticPipe = false; } else if (!playTheAnim && pipe.custom) { pipe.hideStaticPipe = pipe.HideStaticPipe; } else { pipe.hideStaticPipe = this.pipeSet.HideStaticPipe; } }, 这段代码包含了两个方法：updateUI 和 editPipeAnim，它们都涉及到了更新阀门与管道数据后的变化。updateUI 方法：首先对阀门工厂进行属性克隆，以确保不直接修改原始数据。如果 Teacher 属性或 PlayTheAnim 属性为真，表示不需要更新UI，则直接返回。使用 $nextTick 方法，在下一次 DOM 更新周期执行以下操作：遍历阀门工厂中的所有阀门。对于每个阀门，检查其是否应该被打开，并获取其下一级管道的 ID。如果阀门被打开且其上游阀门也被打开，并且存在下一级管道，则将这些管道设置为播放动画状态。否则，将这些管道设置为停止播放动画状态。editPipeAnim 方法：接收参数 pid（管道ID）、playTheAnim（播放动画状态）、valveFactory（阀门工厂对象）。通过 this.getComponent.allPipe 获取所有管道的实例。根据 pid 查找对应的管道实例。如果未找到管道实例，则返回。如果 valveFactory 存在，并且指定的阀门关闭，则返回。根据 playTheAnim 设置管道的 playTheAnim 属性，用于控制播放动画状态。根据管道的 custom 属性和 HideStaticPipe 属性设置管道的 hideStaticPipe 属性。根据管道的 ZIndex 属性设置管道的 zindex 属性。这段代码主要用于在更新阀门与管道数据后，根据新的状态更新UI，包括管道的动画播放状态、层级、静态管道显示状态等。最后该组件会被挂载到全局使用 <hongGeng ref="hongGeng" :Zoom="1" :pipeSet="pipeSet" :Factory="valveFactory" @change="handlerClick"> </hongGeng> tips 为什么要进行属性克隆，不直接修改原始数据？属性克隆是一种常见的做法，特别是在涉及到原始数据的修改时。主要原因有以下几点：避免直接修改原始数据： 在编程中，直接修改原始数据可能导致意外的副作用，特别是在多处引用同一数据时。通过属性克隆，可以确保对克隆对象的修改不会影响到原始数据，从而降低出错的可能性。保留原始数据的完整性： 属性克隆会生成一个新的对象，这样可以保留原始数据的完整性，即使在后续的操作中对克隆对象进行了修改，也不会影响到原始数据的结构和内容。可控制性： 克隆后的对象可以根据需求进行修改和处理，而不会影响到其他地方对同一数据的使用。这样就可以更灵活地控制数据的操作和状态。更易于调试和维护： 克隆后的对象与原始数据相互独立，使得在调试和维护过程中更容易定位和理解代码的行为，降低了代码的复杂度。顺便说一嘴 属性克隆是一种良好的编程实践，可以提高代码的健壮性、可维护性和可读性，避免潜在的错误和不良影响。演示hgpipeDemo2.mp4[vplayer url="https://likefr.com/usr/uploads/2024/02/2830783580.mp4" /]
SummerMemory Graph 优化:性能提升15倍 + 交互式相似度阈值
背景问题SummerMemory Graph（知识图谱可视化）的 /graph-data 接口响应耗时 5.3 秒，随着记忆文件不断增长，这个问题会指数级恶化。问题分析1. O(n²) SQL 查询瓶颈原始代码对每对文件执行一次独立的 PostgreSQL 查询，计算 embedding 向量余弦距离： for i, path1 in enumerate(path_list): for path2 in path_list[i+1:]: cur2.execute(""" SELECT MAX(1 - (m1.embedding <=> m2.embedding)) AS similarity FROM memories m1, memories m2 WHERE m1.path = %s AND m2.path = %s """, (path1, path2)) 100 个文件 = 4950 次 SQL 查询，每次都做向量距离运算。2. 无意义的噪音链接链接由两部分组成： 目录关系链接（同目录即关联，weight=0.3）：4950 条 embedding 语义链接（weight>0.8）：2086 条 大部分记忆文件都在 memory/ 目录下，导致每个节点和所有节点相连，图谱变成毛线团。3. Cosmograph WASM 内存泄漏动态更新 links 时，Cosmograph 的 WASM DuckDB 表生命周期管理有 bug，反复 setConfig 会导致 Array buffer allocation failed。优化方案性能优化：单次查询 + numpy 质心计算 # 一次查出所有 embedding cur2.execute("SELECT path, embedding FROM memories WHERE embedding IS NOT NULL") # Python 内存中计算每个文件的 embedding 质心 for path, embs in path_embs.items(): arr = np.array(embs) centroid = normalize(arr).mean(axis=0) centroids[path] = normalize(centroid) # 内存中 O(n²) 算余弦相似度（向量点积） sim = float(np.dot(c1, centroids[path2])) 结果| 指标 | 优化前 | 优化后 | |------|--------|--------| | 接口耗时 | 5.3s | 0.35s | | SQL 查询次数 | 4950 次 | 1 次 | | 链接数 | 7036（70%噪音） | 891（纯语义） |交互式相似度阈值前端右侧边栏新增滑块控件，拖动即可实时调整链接过滤阈值： 拖动滑块 → debounce 300ms 请求后端 /graph-data?threshold=0.90 后端按阈值过滤返回 links 前端淡出→更新→淡入 + 重新启动模拟动画 这样避免了 Cosmograph WASM 内部表冲突的内存泄漏问题，阈值过滤逻辑全部在后端完成。其他改进 左侧文件列表按 last_updated 降序排列（最新更新的文件在最上面） 去掉了目录关系的噪音链接，只保留 embedding 语义链接 节点数据新增 last_updated 时间字段 技术栈 后端：Python + PostgreSQL（pgvector）+ numpy 前端：Vue 3 + Cosmograph 部署：阿里云 + 宝塔面板 源码 GitHub：[Likefr/SummerMemory](https://github.com/Likefr/SummerMemory) 在线体验：[ai.likefr.com/graph](https://ai.likefr.com/graph)
🎨 Design Prompt Library — AI 设计提示词模板库，117 种顶级风格一键复制
这是什么？Design Prompt Library 是一个精心整理的 AI 设计风格提示词模板库，收录了 117 种顶级设计风格，覆盖国际设计趋势、国内热门 App、Dribbble 灵感、行业场景、趋势美学等多个方向。简单来说，当你想让 AI（Midjourney、DALL·E、Stable Diffusion 等）生成某个特定风格的界面或设计时，直接找到对应模板，一键复制提示词，粘贴使用即可。🎯 解决什么问题？用 AI 生成设计图时，提示词写不好，出来的效果总是差强人意——不是配色太普通，就是风格不够明确，或者根本不知道该怎么描述。Design Prompt Library 把每种风格的提示词都标准化了： ✅ 色彩系统 — 精确到 hex 色值、主色/强调色/辅助色 ✅ 字体排版 — 字体选择、字号层级、行高字距 ✅ 边框与阴影 — 卡片阴影、边框样式、层级关系 ✅ 组件规范 — 圆角、间距、按钮、表单样式 ✅ 布局特点 — 网格、留白、信息架构 📦 收录哪些风格？目前共 117 种设计风格，分为几个大类：国际经典苹果极简、Google Material、微软 Fluent、IBM Carbon、Uber、Airbnb、Stripe、Figma、Linear、Vercel、Notion、Slack……国内热门微信深色、支付宝、淘宝、小红书、抖音、网易云音乐、哔哩哔哩、高德地图、百度……行业场景Web3/区块链、房产平台、物流追踪、医疗健康、法律科技、工业 IoT、餐饮外卖、保险金融、汽车、游戏、投资理财……趋势美学Neubrutalism 新野兽派、Brutalist 粗野主义、Gradient Mesh 渐变网格、Maximalism 极繁主义、Bento Grid 便当盒……原创风格极光、深海、火山岩浆、全息投影、赛博故障、大理石、武士道、沙漠落日、折纸艺术……✨ 功能特性🔍 搜索 — 按名称快速查找🏷️ 筛选 — dark / light / minimal / enterprise / consumer / luxury📋 一键复制 — 点击按钮直接复制完整提示词🌙 亮暗主题 — 自由切换，偏好自动保存📱 响应式 — 桌面 4 列、平板 3 列、手机 2 列、小屏 1 列💎 精致 UI — Glassmorphism 风格，GSAP 动画🛠 技术实现纯前端：HTML + CSS + JavaScript，无框架依赖动画：GSAP + ScrollTrigger字体：Space Grotesk + DM Sans（Google Fonts）部署：Nginx 静态托管，零依赖📌 适合谁用？UI/UX 设计师 — 快速获取风格提示词前端开发者 — 参考设计规范AI 绘图爱好者 — 提升 Midjourney / DALL·E 出图质量产品经理 — 快速生成原型参考设计学生 — 学习不同设计风格的特征🔮 后续计划🌐 公网部署上线 — tool.likefr.com➕ 持续新增风格模板🎭 主题预览功能（代码已就绪，待开放）一个设计师朋友问我：「你为什么要把提示词整理成模板库？」我说：「因为好的提示词不应该每次都从零开始写。」
问题描述在使用 OpenClaw Node 执行远程命令时，遇到 SYSTEM_RUN_DENIED: approval required 错误，明明已设置 security: full 和 ask: off，配置显示正确但仍被拒绝。环境信息OpenClaw 版本: 2026.5.28+架构: Gateway (multi-server) + Node (Work-PC / Home-PC)操作系统: Gateway (Ubuntu), Node (Windows + WSL2)目标: 在 Node 上执行远程命令错误现象# 在 Gateway 执行 exec(host="node", node="Work", command="hostname") # 返回 UNAVAILABLE: SYSTEM_RUN_DENIED: approval required排查过程1. 检查 Gateway 配置openclaw config get配置正确：{ "tools": { "exec": { "security": "full", "ask": "off", "host": "auto" } }, "agents": { "list": [{ "id": "main", "tools": { "exec": { "node": "6009e1d3f068e1cab0dcf9c99060b292739a19c2076198cec06ff1922a3bd555" } } }] } }2. 检查 Node approvals 配置cat ~/.openclaw/exec-approvals.json初始配置（有问题的版本）：{ "version": 1, "socket": { "path": "/home/administrator/.openclaw/exec-approvals.sock", "token": "Uh_SGs…xr1n" }, "defaults": { "security": "full", "ask": "off", "askFallback": "full", "autoAllowSkills": true }, "agents": {} }3. 验证 effective policyopenclaw approvals get --node Work输出显示配置正确读取：Effective Policy ┌────────────┬────────────────────────┬───────────────────────┬────────────────────┐ │ Scope │ Requested │ Effective │ Notes │ ├────────────┼────────────────────────┼───────────────────────┼────────────────────┤ │ tools.exec │ security=full, ask=off │ security=full, ask=off │ requested applies │ └────────────┴────────────────────────┴───────────────────────┴────────────────────┘4. 尝试的解决方案（均无效）✅ 修改 Gateway openclaw.json：tools.exec.ask: off✅ 修改 Node exec-approvals.json：移除 permissions 字段✅ 重启 Node 服务：systemctl --user restart openclaw-node✅ 重启 Gateway：openclaw gateway stop && openclaw gateway start✅ 检查 Node 日志：无错误信息✅ 查阅官方文档和 GitHub issue根本原因exec-approvals.json 中的 socket 字段阻止了配置生效。当 socket 字段存在时，OpenClaw 会尝试通过 Unix socket (exec-approvals.sock) 通信来获取 approvals 配置。如果：Socket 文件不存在Socket 文件权限不正确Socket 通信失败则回退到某个默认行为（可能是 deny），导致配置文件中的 security: full 和 ask: off 被忽略。新版推荐方案：exec-policy preset yolo在新版 OpenClaw 中，官方提供了更简洁的方式来一键配置 YOLO 模式（无需手动编辑 JSON）：# 在 Node 端执行（推荐方式） openclaw exec-policy preset yolo openclaw node restart这会自动：设置 tools.exec 的 security: full 和 ask: off同步更新 exec-approvals.json（含 askFallback: "full"）移除/覆盖 socket 字段（避免 IPC 干扰）重启节点使配置生效旧版手动方案（备选）如果 exec-policy 命令不可用，手动方案如下：方案一：删除 socket 字段编辑 ~/.openclaw/exec-approvals.json，删除 socket 字段：{ "version": 1, "defaults": { "security": "full", "ask": "off", "askFallback": "full", "autoAllowSkills": true }, "agents": {} }方案二：清空文件如果还是不行，可以直接清空该文件（节点重启后会重建）：# 备份 cp ~/.openclaw/exec-approvals.json ~/.openclaw/exec-approvals.json.bak # 清空 echo '{}' > ~/.openclaw/exec-approvals.json # 重启节点 systemctl --user restart openclaw-node验证修复# Gateway 端测试 openclaw execPolicy get --node Work # 或直接执行测试 exec(host="node", node="Work", command="hostname") # 应返回目标主机名，如 DESKTOP-46MKU59 ✅完整配置示例（新版推荐写法）新版 OpenClaw 的配置更简洁，核心只需设置：Gateway 端openclaw config set tools.exec.security full openclaw config set tools.exec.ask offNode 端openclaw exec-policy preset yolo openclaw node restart无需手动编辑 openclaw.json 中的 agents.list[].tools.exec.node 字段，节点会自动识别。经验总结新版优先用 exec-policy preset yolo：官方推荐的自动化方式，比手动改 JSON 更可靠Socket 配置陷阱：exec-approvals.json 中的 socket 字段是 macOS App 的本地 IPC 通信用途，在 headless Linux node 环境中会导致 approvals 系统异常调试方法：新版：openclaw exec-policy get --node <name> 查看 effective policy旧版：openclaw approvals get --node <name>检查 Node 日志：journalctl --user -u openclaw-node确认配置文件格式正确（JSON 语法）文档参考：Exec approvals - OpenClawNode troubleshooting - OpenClaw附录：常见问题Q: openclaw exec-policy 命令不存在？A: 你的 OpenClaw 版本较旧，请升级到最新版。使用 openclaw update check 查看更新。Q: 节点已审批通过但 exec 仍无法路由？A: 确认 exec 命令中 host="node" 和 node="节点名" 是作为工具参数传递，而不是写在命令字符串里：# ✅ 正确写法（作为参数） exec(command="hostname", host="node", node="Work") # ❌ 错误写法（写在命令字符串里） exec(command='hostname --host node --node Work')Q: Home 节点审批后仍无法连接？A: 在 Gateway Web UI 中完成配对审批后，可能需要重启 Gateway 才能使节点路由生效。折腾时间: 约 4 小时（初次）+ 5 分钟（新版方案） 最终解决: 新版一行命令，旧版删除 socket 字段 记录时间: 2026-05-31 更新: 2026-07-01（补充新版 exec-policy 方案）
OpenClaw WSL 节点文件读写踩坑全记录：三次血泪教训，终于找到正确姿势
前提：OpenClaw Gateway 部署在 Linux 服务器上，Home 节点跑在 Windows WSL2 Ubuntu 里。背景我的 OpenClaw 架构：Gateway：Linux 服务器（192.168.31.113）Home 节点：Windows 11 + WSL2 Ubuntu（桌面在 F:\Desktop）Work 节点：公司电脑 WSL2需要通过 OpenClaw 远程操作 Home WSL 节点上的文件，读写 Windows 分区的项目代码。踩坑1：exec 看不到 Windows 盘符现象# WSL 终端里一切正常 likefr@DESKTOP-4F1J92R:~$ ls /mnt/f/Desktop/ 01-项目代码 02-压缩包 ... # 但 OpenClaw exec 命令看不到 $ ls /mnt/f/Desktop/ ls: cannot access '/mnt/f/Desktop/': No such file or directory原因OpenClaw 的 exec 命令跑在节点进程的 mount namespace 里。WSL 的 Windows 盘符（C/D/E/F 盘）是通过 drvfs（9p 文件系统）动态挂载的，如果挂载发生在节点进程启动之后，exec 就看不到。验证方法# 在 exec 里检查 mount | grep drvfs # 空！ cat /proc/self/mountinfo | grep 9p # 空！ ls /mnt/f/ # 空 # 在 WSL 终端里检查 mount | grep drvfs # 能看到所有盘符解决方案方案A：/etc/fstab 预挂载（推荐）在 WSL 的 /etc/fstab 里预先声明所有盘符，这样 WSL 启动时就挂好：sudo tee -a /etc/fstab << 'EOF' C: /mnt/c drvfs defaults 0 0 D: /mnt/d drvfs defaults 0 0 E: /mnt/e drvfs defaults 0 0 F: /mnt/f drvfs defaults 0 0 G: /mnt/g drvfs defaults 0 0 H: /mnt/h drvfs defaults 0 0 EOF然后 wsl --shutdown 重启 WSL。方案B：system service + ExecStartPre 挂载把 OpenClaw 节点改成 system 级 service（root 权限），在启动前挂载：# /etc/systemd/system/openclaw-node.service [Service] ExecStartPre=/bin/bash -c 'until [ -d /mnt/c/Users ]; do sleep 1; done' ExecStartPre=/bin/bash -c 'mkdir -p /mnt/f && mount -t drvfs F: /mnt/f 2>/dev/null; true' ExecStartPre=/bin/bash -c 'mkdir -p /mnt/d && mount -t drvfs D: /mnt/d 2>/dev/null; true' ExecStart=/usr/bin/openclaw node run --host YOUR_HOST --port 443 --tls --display-name "Home"⚠️ 注意：普通用户的 user service 没有权限 mount，必须用 system service（root）。踩坑2：WSL cat > 写 Windows 文件为空现象# exec 里 cat > 写文件，看似成功 echo "hello" > /mnt/f/Desktop/test.txt # 但 Windows 侧看到文件 size=0！ # PowerShell 确认 PS> (Get-Item F:\Desktop\test.txt).Length 0原因WSL 的 sh/bash 通过 cat > 写到 /mnt/f/ 或 /mnt/c/ 的文件，Windows 侧看到 size=0。这是 9p 文件系统的缓存问题，在某些 mount namespace 配置下会出现。验证# 用 PowerShell 单引号 here-string 可以写（不解析 ${}） # 但需要 .ps1 文件方式调用解决方案不要用 exec 的 cat/echo 写 Windows 文件！ 用 OpenClaw 的 file_write 工具。踩坑3：exec 路由到了 Gateway 而不是 WSL 节点现象所有 exec 命令都在 Gateway（Linux 服务器）上执行，而不是 WSL 节点：hostname # 输出：multi-server（这是 Gateway 服务器，不是 WSL）原因OpenClaw 的 agent 配置里 tools.exec.node 绑定了节点 ID。如果绑错或者节点不在线，exec 会 fallback 到 Gateway 本机。// openclaw.json { "agents": { "list": [ { "id": "devops", "tools": { "exec": { "node": "正确的节点ID" } } } ] } }解决方案确认节点 ID：在 Control UI 的 Nodes 页面查看绑定正确节点：配置 agents.list[].tools.exec.node 为目标节点的完整 ID确保节点在线：如果节点离线，exec 会 fallback 到 Gatewayexec 命令加 host=node：host="node", node="Home" 确保路由到节点最终正确姿势✅ 读文件：file_fetchfile_fetch(node="Home", path="/mnt/f/Desktop/project/config.json")走节点插件的文件通道，不受 mount namespace 限制。✅ 写文件：file_writefile_write(node="Home", path="/mnt/f/Desktop/project/newfile.java", contentBase64="...")同样走节点插件的文件通道，Windows 侧能看到完整内容。✅ 路径格式必须用：/mnt/f/Desktop/... 不能用：F:\Desktop\... 或 F:/Desktop/...✅ 执行命令（需要 fstab 或 service 预挂载）# 确保节点能看到盘符后 exec(host="node", node="Home", command="ls /mnt/f/Desktop/")systemd service 完整配置user service（普通用户，推荐）# ~/.config/systemd/user/openclaw-node.service [Unit] Description=OpenClaw Node After=network-online.target Wants=network-online.target [Service] ExecStartPre=/bin/bash -c 'until [ -d /mnt/c/Users ]; do sleep 1; done' ExecStart=/usr/bin/openclaw node run --host YOUR_HOST --port 443 --tls --display-name "Home" Environment=OPENCLAW_GATEWAY_TOKEN=YOUR_TOKEN Restart=always RestartSec=5 [Install] WantedBy=default.target配合 fstab 预挂载# /etc/fstab 追加 C: /mnt/c drvfs defaults 0 0 D: /mnt/d drvfs defaults 0 0 E: /mnt/e drvfs defaults 0 0 F: /mnt/f drvfs defaults 0 0 G: /mnt/g drvfs defaults 0 0 H: /mnt/h drvfs defaults 0 0总结操作工具说明读写 Windows 文件file_write / file_fetch走插件通道，不受 namespace 限制路径格式/mnt/x/...不能用 X:\...exec 看盘符fstab 预挂载确保 WSL 启动时就挂好exec 路由host="node" + 正确 node ID避免 fallback 到 GatewayWSL cat 写文件❌ 别用Windows 侧看到 size=0一句话总结：读写 Windows 文件一律用 file_write / file_fetch，路径格式 /mnt/x/...本文由 Summer（OpenClaw AI 助手）协助整理，基于 2026-05-22 的真实踩坑过程。
前置条件Windows 10/11，已启用 WSL2有 OpenClaw Gateway 运行中Gateway token 已获取第一步：卸载旧 Ubuntu（如需重装）在 PowerShell 中执行：wsl --unregister Ubuntu第二步：安装 Ubuntu在 PowerShell 中执行：wsl --install Ubuntu安装完成后设置用户名和密码。第三步：配置镜像网络模式解决 WSL2 无法使用 Windows localhost 代理的问题。在 PowerShell 中执行：Set-Content -Path "$env:USERPROFILE\.wslconfig" -Value "[wsl2]`nnetworkingMode=mirrored`nvmIdleTimeout=-1"networkingMode=mirrored：WSL 与 Windows 共享网络栈，代理直接能用vmIdleTimeout=-1：WSL 不会因空闲自动关闭然后重启 WSL 使配置生效：wsl --shutdown第四步：安装 Node.js在 WSL 中执行：curl -fsSL https://deb.nodesource.com/setup_22.x | sudo -E bash - sudo apt install nodejs -y验证：node -v && npm -v⚠️ 包名是 nodejs 不是 node，apt install node 会报错。第五步：安装 OpenClaw在 WSL 中执行：sudo npm install -g openclaw第六步：手动运行并配对在 WSL 中执行：OPENCLAW_GATEWAY_TOKEN=<你的gateway_token> openclaw node run --host <gateway地址> --port 443 --tls --display-name "Work"参数说明：--host：Gateway 地址--port 443：端口--tls：443 端口必须加--display-name：节点显示名称OPENCLAW_GATEWAY_TOKEN：Gateway 认证 token（必须传）确认连接成功后 Ctrl+C 停掉，继续配置自启。第七步：创建 systemd 自启服务7.1 配置 wsl.conf在 WSL 中执行：sudo tee /etc/wsl.conf << 'EOF' [boot] systemd=true [automount] enabled=true options="metadata,umask=22,fmask=11" mountFsTab=true EOF然后在 PowerShell 重启 WSL：wsl --shutdown7.2 创建 service 文件在 WSL 中执行：mkdir -p ~/.config/systemd/user cat > ~/.config/systemd/user/openclaw-node.service << EOF [Unit] Description=OpenClaw Node After=network-online.target Wants=network-online.target [Service] ExecStartPre=/bin/bash -c 'until [ -d /mnt/c/Users ]; do sleep 1; done' ExecStart=/usr/bin/openclaw node run --host <gateway地址> --port 443 --tls --display-name "Work" Environment=OPENCLAW_GATEWAY_TOKEN=<你的gateway_token> Restart=always RestartSec=5 [Install] WantedBy=default.target EOF关键点：ExecStartPre：等待 Windows 盘符挂载完成后再启动，否则 /mnt/c /mnt/d 等路径不可用Restart=always：进程异常退出自动重启7.3 启用并启动服务systemctl --user daemon-reload systemctl --user enable openclaw-node.service systemctl --user start openclaw-node.service7.4 检查状态systemctl --user status openclaw-node.service应该显示 active (running)。第八步：设置 WSL 开机自启（Windows 计划任务）WSL 关掉终端后可能退出，需要用 VBS 脚本后台常驻。8.1 创建 VBS 脚本在 PowerShell 中执行：Set-Content -Path "$env:USERPROFILE\start-wsl.vbs" -Value 'CreateObject("Wscript.Shell").Run "wsl -d Ubuntu", 0, False'VBS 脚本会静默启动 WSL，不会弹出终端窗口。8.2 创建计划任务在 PowerShell 中执行：schtasks /create /tn "WSL Ubuntu AutoStart" /tr "wscript.exe \`"$env:USERPROFILE\start-wsl.vbs\`"" /sc onlogon /rl highest /f用户登录后自动运行 VBS 脚本，静默启动 WSL + systemd 服务。8.3 验证计划任务schtasks /query /tn "WSL Ubuntu AutoStart"状态应显示 Ready。常见问题排查问题原因解决方案apt install node 报错包名不对用 apt install nodejsWSL 下载慢/代理不生效NAT 模式不支持 localhost 代理配置 networkingMode=mirroredgateway token missing没传 token加 OPENCLAW_GATEWAY_TOKEN=xxxgateway token mismatchtoken 写错了检查 service 文件里的 tokensystemd 服务里 /mnt/c 不存在服务启动时 Windows 盘符还没挂载加 ExecStartPre 等待挂载WSL 关终端就退出没有活跃会话用 .vbs 脚本后台常驻 + 计划任务服务频繁重启 start-limit-hit启动失败太多次systemctl --user reset-failed 再启动最终效果✅ Windows 开机 → 计划任务自动运行 VBS → 静默启动 WSL✅ WSL 启动 → systemd 自动拉起 openclaw-node 服务✅ 服务等待 Windows 盘符挂载 → 连接 Gateway → 节点上线✅ 关闭终端不影响 WSL 和节点运行✅ 文件读写权限正常，/mnt/c /mnt/d 等均可访问
scp传输当两台LINUX主机之间要互传文件时可使用SCP命令来实现scp传输速度较慢,但使用ssh通道保证了传输的安全性将本地目录拷贝到远程scp -r 目录名 用户名@计算机IP或者计算机名称:远程路径从远程将目录拷回本地scp -r 用户名@计算机IP或者计算机名称:目录名 本地路径如果出现This key is not known by any other names. Are you sure you want to continue connecting (yes/no/[fingerprint])?则说明该连接钥匙没有别的名称了输入yes 在输入密码即可 输入密钥别名rsync差异化传输(支持断点续传,数据同步)将本地文件推送到目标服务器rsync -av /backup/ -e ssh root@192.168.1.110:/bak将远程服务器的文件 拉取下来 rsync -av -e ssh root@36.212.236.144:/opt/service/nextEcdis /nextEcdis-a: archive归档模式，表示以递归方式传输文件，并保持所有文件属性，链接等,等于-rlptgoDrsync——remot- e sync。rsync是Linux系统下的文件同步和数据传输工具，它采用“rsync”算法，可以将一个客户机和远程文件服务器之间的文件同步，也可以在本地系统中将数据从一个分区备份到另一个分区上。如果rsync在备份过程中出现了数据传输中断，恢复后可以继续传输不一致的部分。rsync可以执行完整备份或增量备份。它的主要特点有：可以镜像保存整个目录树和文件系统；可以很容易做到保持原来文件的权限、时间、软硬链接；无须特殊权限即可安装；可以增量同步数据，文件传输效率高，因而同步时间短；可以使用rcp、ssh等方式来传输文件，当然也可以通过直接的socket连接；支持匿名传输，以方便进行网站镜象等；加密传输数据，保证了数据的安全性；
详细：Failing row contains (null, xxx,xx..)大意是 你将id字段设置了不允许为空 但在插入数据时id 却是空 因为没有自增以下是我的创建表sql 没有创建id序列 类似 mysql的 AUTO INCREMENTCREATE TABLE "public"."ship_faults" ( "id" int4 NOT NULL, "is_ack" bool DEFAULT false, CONSTRAINT "ship_faults_pkey" PRIMARY KEY ("id"), CONSTRAINT "ship_faults_tag_key" UNIQUE ("tag") )解决:SELECT setval('ship_fault_alarm_log_id_seq', (SELECT MAX(id) FROM ship_fault_alarm_log)); -- 或者创建新的序列（如果序列不存在） CREATE SEQUENCE ship_fault_alarm_log_id_seq; ALTER TABLE ship_fault_alarm_log ALTER COLUMN id SET DEFAULT nextval('ship_fault_alarm_log_id_seq'); SELECT setval('ship_fault_alarm_log_id_seq', COALESCE((SELECT MAX(id) FROM ship_fault_alarm_log), 1), false);也可以在创建表的时候 使用 SERIAL 自动创建序列 -- 使用 SERIAL 自动创建序列 CREATE TABLE "public"."ship_fault_alarm_log" ( "id" SERIAL NOT NULL, -- SERIAL 会自动创建并关联序列 "description" varchar(255) COLLATE "pg_catalog"."default" NOT NULL, -- 其他字段... CONSTRAINT "ship_fault_alarm_log_pkey" PRIMARY KEY ("id") );
version: '3.8' services: redis: image: redis:7.2.4 container_name: redis-server restart: unless-stopped ports: - "6381:6379" volumes: - ./data:/data - ./conf/redis.conf:/usr/local/etc/redis/redis.conf command: redis-server /usr/local/etc/redis/redis.conf environment: - TZ=Asia/Shanghai networks: - redis-net networks: redis-net: driver: bridge config: # Redis配置文件示例 port 6381 requirepass X # 基本配置 daemonize no protected-mode yes bind 127.0.0.1 timeout 0 tcp-keepalive 300 # 持久化配置 save 900 1 save 300 10 save 60 10000 # 指定 Docker Compose 文件格式版本 version: '3.8' # 定义服务列表 services: # 定义 PostgreSQL 服务 postgres: # 使用 PostgreSQL 官方镜像，版本13 image: postgres:latest # 指定容器名称（如果不指定，Docker 会自动生成） container_name: postgres # 环境变量配置 environment: # 设置 PostgreSQL 超级用户（默认为 postgres） POSTGRES_USER: postgres # 设置 PostgreSQL 超级用户密码（必须设置） POSTGRES_PASSWORD: HongGeng1107 # 设置默认创建的数据库名称（默认为 POSTGRES_USER 的值） # POSTGRES_DB: db-default # 端口映射配置（主机端口:容器端口） ports: - "5432:5432" # 将容器内部的5432端口映射到主机的5432端口 # 数据卷配置（持久化数据） volumes: # 使用命名卷 postgres_data 挂载到容器中的数据库数据目录 - postgres_data:/var/lib/postgresql/data - ./backup:/backup # 容器重启策略 restart: always # 总是自动重启容器，无论退出状态如何 # 定义卷列表（用于数据持久化） volumes: # 定义一个名为 postgres_data 的命名卷 # Docker 会自动管理这个卷，确保数据库数据持久化 # 即使容器被删除，数据也不会丢失 postgres_data:
SQL Studio — Android 数据库管理工具在移动设备上轻松管理 PostgreSQL 与 MySQL 数据库(开源地址)[http://likefr.com:3000/Likefr/SQLStudio.git]简介SQL Studio 是一款专为 Android 设备打造的本地数据库管理工具。它支持连接远程 PostgreSQL 和 MySQL 数据库，提供 SQL 查询执行、数据结构管理、数据导入导出以及点对点同步功能。无论你是后端开发人员、数据库管理员，还是学生或 IT 工程师，SQL Studio 都能让你在离线或局域网环境下仅通过手机完成数据库调试与管理任务。使用场景在没有电脑时快速调试远程数据库移动端查看和修改数据库表结构与数据导出、导入 SQL 文件用于备份与恢复跨数据库同步结构与数据技术栈层级技术选型描述操作系统Android宿主平台前端框架Vue3可选前端 UI 开发框架Web 引擎腾讯 X5 WebView提供更强大的网页渲染能力数据通信evaluateJavascriptJS 与 Java 交互桥梁数据库驱动PostgreSQL / MySQL JDBC嵌入式数据库连接驱动核心功能数据库连接管理支持 PostgreSQL / MySQL连接配置保存与状态检测失败提示基于 SQLState 状态码首页连接列表显示已保存连接快速进入编辑器或管理连接（编辑/删除/重连）SQL 编辑器模块支持 SQL 高亮与校验执行选中部分或全部 SQL查看查询结果表格（二维展示）支持撤销/重做操作数据导入导出导出生成 .sql 文件支持大容量插入模式支持复制到剪贴板或分享文件导入解析并执行 .sql 文件显示执行进度与结果数据同步功能（重点）点对点同步源与目标数据库结构差异对比 + 数据比对同步执行后反馈成功/失败原因安全性设计所有数据不存储在本地不自动连接数据库断开连接即销毁资源所有操作均在本地处理，无网络上传UI 设计风格遵循 Material Design (MD) 规范简洁直观的操作界面支持夜间模式PostgreSQL 错误状态码参考SQLState分类描述建议提示文案28P01认证失败invalid_password密码错误，请重新输入28000认证失败invalid_authorization_specification用户名错误或未授权08001连接失败sqlclient_unable_to_establish_sqlconnection无法连接，请检查 IP、端口设置08004拒绝连接sqlserver_rejected_establishment_of_sqlconnection数据库拒绝连接，检查配置与权限3D000数据库错误invalid_catalog_name数据库不存在57P01服务关闭admin_shutdown数据库服务已关闭，请稍后再试53300资源限制too_many_connections数据库连接过多，请稍后重试42601SQL 错误syntax_errorSQL 语法错误23505唯一冲突unique_violation唯一键冲突，数据重复42P01表不存在undefined_table表不存在，请检查 SQL 语句完整错误码参考开发计划数据库连接管理模块SQL 编辑器模块数据导入导出模块数据同步模块UI 界面美化与交互优化贡献指南欢迎贡献代码！请阅读我们的 CONTRIBUTING.md 获取更多信息。LicenseMIT © SQL Studio 开发团队
Neo4j 是一个高性能的、开源的图数据库。它将数据存储为图结构，其中节点表示实体，边表示实体之间的关系。这种图数据模型非常适合处理复杂的关系型数据，能够高效地进行关系查询和遍历。Neo4j 的主要特性包括：强大的图查询语言 Cypher：Cypher 是一种专门为 Neo4j 设计的声明式查询语言，使得查询和操作图数据变得直观和高效。创建两个节点CREATE (p:Person {name: 'Likefr', age: 40}), (m:Person {name: 'Neo', age: 20})查询节点查询name为Likefr的节点MATCH(p:Person{name: 'Likefr'}) return p查询指定标签 Person的所有节点MATCH(p:Person) return p查询全部节点 (部分标签)MATCH (n) RETURN n 关联节点关系1.创建节点时直接关联CREATE (p1:Person {name: 'MseDa'}), (p2:Person {name: 'Bob'}) CREATE (p1)-[:KNOWS]->(p2) RETURN p1, p22.将已有节点关联起来 (任何操作都是先把节点查出来在对其进行操作)MATCH(p1:Person {name: 'MseDa'}), (p2:Person {name: 'Bob'}) create(p1)-[:KNOWS]->(p2)新增或更新节点属性MATCH (p:Person {name: 'MseDa'}) SET p.age = 31更新关系属性MATCH (p1:Person {name: 'MseDa'})-[r:KNOWS]->(p2:Person) SET r.since = 2022 RETURN r 基本上操作都是一样 先使用MATCH 定义一个别名 把数据捞出来 在进行操作删除节点MATCH (p:Person {name: 'MseDa'}) DELETE p删除多个节点MATCH (p:Person {name: 'MseDa'}), (p2:Person {name: 'Alice2'}) DELETE p，p2需要注意的是 如果删除的节点与其他节点有关联将无法删除并会抛出异常 意思就是: 该节点它仍然和别的节点有关系。要删除该节点，必须先删除其关系删除关系MATCH (p1:Person {name: 'MseDa'})-[r:KNOWS]->(p2:Person) DELETE r 同时删除节点 和 关系 MATCH (p1:Person {name: 'MseDa'})-[r:KNOWS]->(p2:Person) DELETE p1, r, p2 查询节点的属性值create(el:lur{name: 'el',state: false}) match(el:lur{name: 'el'}) return el.state