feiyu-memory
给 DSH 的本地纯文件长期记忆插件:热层 / 事件层 / 事实层 / 摘要层,每步按 token 预算注入相关记忆。 零运行时依赖:只用 Node 内置模块(fs / path / os),不装 npm 包、不连向量库、不调外部 embedding,连抽取都是本地规则——没有一行数据离开这台机器。
A local-first, zero-dependency memory plugin for DSH. Everything is plain files under your home directory; the recall/scoring code is ~500 lines of readable JS you can audit in ten minutes.
热层 hot.md 你手写/自动维护的「每次都得知道」的事
事件层 events.jsonl 一行一条经历,带时间、重要度、情绪、存活系数
事实层 facts.json 槽位表:key → value,可失效、可钉住(换人换说法不打架)
摘要层 summary/ 按天压缩过去的事件,省 token
为什么是纯文件
- 可读可改:想改记忆直接用编辑器打开
hot.md,下次注入立即生效;想全忘光就删目录。 - 可审计:召回打分、衰减、去重全是明文规则,不藏在黑盒 embedding 里。
- 零成本:没有 API 调用,没有后台模型,闲置时零 token。
- 可迁移:一个目录拷走就是完整记忆,换机器、换版本都在。
安装
Host-only bundle,无构建步骤:
- 把本仓库放到任意目录(绝对路径,避免中文/空格更稳)。
- DSH → 设置 → 插件 → 安装 bundle,目标目录选本仓库根目录。
- 重启 DSH(插件代码变更需重启才会加载新模块)。
也可以在命令行登记进某个 profile:
node tools/install-profile.mjs
# 默认 ~/.dsh/profiles/desktop,可用 DSH_PROFILE_DIR / DSH_PROFILE 覆盖
脚本会做三件事(幂等、先备份 package.json):写 link: 依赖、把 feiyu-memory 加进 dsh.profile.bundles、在 node_modules 下建 junction。
注意:某些受限 profile / 会话预设不带
plugin_manager权限,安装这一步要在桌面 UI 或权限完整的会话里做。
数据目录
默认 ~/.dsh/feiyu-memory/:
~/.dsh/feiyu-memory/
├── hot.md # 热层:手改立即生效
├── facts.json # 事实层:槽位表
├── events.jsonl # 事件层:一行一条
├── summary/ # 按日摘要 YYYY-MM-DD.md
├── config.json # 配置,缺省即默认
└── feiyu.log # 出错日志(最多 200 行)
想放别的盘:cordis.patch.yml 里打开 dataDir,或在 config.json 里写 dataDir。
怎么全忘光:删掉整个目录即可,下次启动自动重建空库。
配置
优先级:行 config(cordis.patch.yml)> data/config.json > 默认值。
| 键 | 默认 | 含义 |
|---|---|---|
| enabled | true | 关掉后不注入 |
| injectTokenBudget | 1200 | 每步注入预算(按字符 ×4 估算) |
| recallLimit | 3 | 最多命中几条事件 |
| minScore | 0.35 | 证据分门槛 |
| eventHalfLifeDays | 30 | 普通事件半衰期 |
| emotionalHalfLifeDays | 7 | 情绪浓的事件半衰期 |
| emotionalThreshold | 0.6 | 多浓算「情绪浓」 |
| hotMaxChars | 800 | 热层截断上限 |
| summaryEnabled | true | 是否做按日摘要 |
| disableExtract | false | true 时只写不抽(隐私敏感场景) |
| extractModel | heuristic | 预留位,本插件不调远程模型 |
| dataDir | "" | 覆盖数据目录 |
| viewHttp | true | 是否起面板数据服务 |
| viewPort | 17654 | 数据服务起始端口 |
工具
| 工具 | 作用 |
|---|---|
feiyu_remember |
写一条记忆(event 经历 / fact 长期事实) |
feiyu_recall |
按关键词检索,返回热层/事实/事件片段 |
feiyu_forget |
标记遗忘(markSuperseded,不硬删,可追溯) |
feiyu_stats |
事件数 / 事实数 / 热层字数 / 数据目录 |
侧边栏面板
同一包里带客户端半边:client.js 注册一个全局面板(布局 root 作用域 main keyed slot,key = feiyu-memory),
并在 sidebar.panellist 里挂一个「记忆」图标。
面板用 Canvas 2D 手写(无 d3):
- 力导向布局:斥力 + 弹簧 + 中心引力 + 碰撞 + 垂直漂移(decay 高上浮、衰减下沉),alpha 收敛到 0.02 停
- 视觉编码:hot
#FFD166/ fact#9B7BFF/ event#4DD4E8/ summary#6B7A8F,半径6+weight*10,透明度0.25+0.75*decay,辉光8+decay*16,贝塞尔边 - 交互:悬停高亮 1 跳邻居、点击出详情卡、拖拽钉住 3 秒、滚轮缩放 0.3–3x、拖空白平移、搜索过滤、四层开关、时间轴双端滑块、重置视角
- 召回脉冲:pre-step 命中时从 hot 沿边放白色光点
- 降级:节点 > 600 或帧率连续低于 45fps → 关辉光/贝塞尔/标签
Host ↔ Client 通道(V1:回环 HTTP)
没有用 DSH 的 Typert Remote(那套要 TS 装饰器在编译期写原型上的描述符,纯 JS 手搓容易碎)。
V1 由 Host 起一个只绑 127.0.0.1 的小 HTTP 服务,端口从 viewPort 往后试 10 个:
| 路径 | 方法 | 作用 |
|---|---|---|
| /graph | GET | 全量 nodes / edges |
| /stats | GET | 事件数 / 事实数 / 热层字数 / 数据目录 |
| /hits?since= | GET | 上次之后的召回命中 id(脉冲用) |
| /forget | POST | 标 superseded |
| /pin | POST | 钉住 / 取消(事件与事实都支持) |
安全边界:只绑回环、CORS 只放行 DSH 页面来源;但本机任何进程都能读这个端口,介意的话把 viewHttp 关掉。
面板数据每 2 秒轮询,没有推送;规模上万节点再换 SSE。
工作原理
| 环节 | 接口 |
|---|---|
| 每步注入 | agent.ctx.systemPrompt.context({name, order, text}),宿主按步重算,内容不变自动不重复提交 |
| 取当前提问 | agent.ctx.on('agent/pre-step', (payload, next) => next()) 只读捕获,不改消息 |
| 轮次收尾固化 | agent.ctx.on('agent/turn-stopping') → 异步规则抽取 → 写 events / facts |
| 挂到 agent | ctx.on('agent/created', ({ agent }) => ...) |
| 工具注册 | ctx.tools.register(...) ×4 |
| 遗忘 | markSuperseded 不硬删 |
召回打分(src/recall.js):
sim = 提问 token 的 idf 加权覆盖率(不是 Jaccard)
decay = 0.5 ^ (天数 / 半衰期) # 情绪浓的事件换更短半衰期
s = sim × decay × importance
C = 1 - e^(-3s) # 指数饱和,比 s/(s+2) 够得着门槛
命中条件:C ≥ minScore
设计取舍(实测后改的,写在这里免得后人再踩)
C = s/(s+2)数学上够不到minScore = 0.35:s ∈ [0,1]时C上限只有1/3,已改为指数饱和C = 1 - e^(-3s)。- 相似度用加权覆盖度(intersection / 提问侧 idf 总量),不用 Jaccard:提问短、事件长时 Jaccard 天然偏低,会把真正相关的事件压死。
- 抽取默认不调模型:逐轮调 LLM 会持续花钱;默认本地规则抽取,
extractModel只留位。 - 注入走动态上下文,不往
decision.messages里塞 user 消息:后者会把记忆写进会话日志,越长越贵。 - 用户文本先洗信封:聊天桥接常把「【当前时间】…(时区)【角色】」拼在消息前面,抽取前剥掉,事件库才不被时间戳污染。
- 24 小时内相同文本的事件自动去重,否则同一句话会被反复记。
隐私
- 所有记忆都是你机器上的纯文本,插件不发起任何网络请求(面板服务只绑
127.0.0.1)。 - 仓库里不含任何个人记忆数据:
data/整个目录在.gitignore里,发布版从零开始建库。 - 想彻底不留痕:删
~/.dsh/feiyu-memory/;想只写不抽:disableExtract: true。
自检
npm test # 全部
node test/selftest.mjs # 内核 28 项,生成 SELFTEST.md
node test/polish-test.mjs # 精修 15 项:信封清洗、事实失效/钉住、去重、面板写接口
node test/view-selftest.mjs # 图模型 + 面板数据服务 19 项
node test/apply-sim.mjs # 假 ctx 演练钩子接线
测试全部用 os.tmpdir() 下的临时目录,不会碰你真实记忆库。
目录结构
index.js Host 入口:钩子接线 + 4 个工具
client.js 面板(Canvas 2D 力导向图)
src/store.js 文件读写、原子写、日志
src/config.js 配置合并与默认值
src/event.js 事件层:写入、去重、衰减、命中
src/fact.js 事实层:槽位、失效、钉住
src/hot.js 热层
src/recall.js 检索打分
src/inject.js 注入文本渲染(按 token 预算截断)
src/extract.js 本地规则抽取(记不记 / 抽成什么)
src/summary.js 按日摘要压缩
src/graph.js 面板图模型
src/viewhost.js 127.0.0.1 数据服务
test/ 自检脚本
tools/ 安装与数据清洗小工具
License
MIT
No comments yet. Be the first to write one.