dsh-user-memory
xinzeqing/dsh-user-memory
User-authored long-term memory in four tiers (must-follow rules, behavior settings, user profile, optional rules), injected as a system-prompt section, with agent tools to propose edits and a Chinese panel where the user reviews and applies them.
Installazione
dsh plugin --profile web add github:xinzeqing/dsh-user-memoryREADME
dsh-user-memory
自研的 DeepSeek Harness 插件:把用户本人编写的分级长期记忆注入到系统提示词里,并提供一个中文图形面板供直接查看和修改。
本文的称呼约定:AI 一律写「AI」、用户一律写「用户」,全文不拿「我/你」指代任何一方。引号里是界面或注入文本的原文,人称按原文自身语境——注入给模型的那几段里,「你」指模型、「用户」指用户。 文中的
<DSH_HOME>指 DSH 的数据目录(默认%USERPROFILE%\.dsh),<profile>指 profile 名(例如desktop);凡写进<...>的都是占位符,不是本机的真实路径。
- 宿主插件:profile 加载它,每一轮都在。
- 运行时只读(除用户操作外)。它从不自行写记忆:只有两条写入路径——用户在面板里点保存,或用户让 AI 用文件工具改。没有自动采集、自动总结、后台回写、网络访问。
- AI 只能提议,不能决定:
memory_suggest把一条建议放进「待确认」区,只有用户在面板里点「采纳」,它才变成一条通知,AI 下一轮才去写。 - 零依赖,只用 Node 内置模块。整个目录拷到哪都能跑。
- 两层记忆:长期记忆回答「用户是谁、AI 该怎么干」,临时记忆(默认关)回答「这件事还没做完、交给谁」。后者是第二段系统提示词、另一份预算,做完归档就立刻从上下文里消失。
图形面板(推荐用这个)
重启 DeepSeek Harness 之后,左侧栏会出现一个「长期记忆」入口,点开就是面板:
- 五个分页:必须遵守 / 行为设定 / 用户画像 / 可选遵守 / 变更日志(
tempMemory: true时多一个临时记忆分页);全部中文,不用看文件名。「待确认」分页固定在最后一个。 - 「待确认」与「临时记忆」两个分页各带一个数字气泡,表示各自还有多少条要处理。
- 直接改,点「保存这一节」。保存后立即生效,不需要重启;底部回显结果,例如「已生效:这一节会注入(约 320 字)。」或「目前这一节是空的(只剩标题或注释),不会注入。」
- 有未保存改动的分页显示一个小黄点;「放弃改动」可一次性还原。
- 只认那五个文件名,其它路径一律拒绝;单个文件上限 256 KB。
- 顶部有一个「自动采集记忆:已开启 / 已关闭」开关(见下面
autoCapture)。
待确认区(AI 与用户之间的闸门)
- AI 用
memory_suggest提议一条:目标文件 + 完整措辞 + 理由。这个工具不写任何记忆文件,只在<DSH_HOME>\memory-suggest.json里放一条pending。 - 用户打开面板 →「待确认」→ 看到卡片。卡片下方有一个**「补充说明」输入框**(可选):想额外加的要求、想改的措辞写在这里。
- 打字不落盘:输入框里的字只在点「采纳」的那一刻才随这条存下来(存进
userNote),所以犹豫期间磁盘上没有任何痕迹;自动刷新也不会擦掉正在打的草稿。 - 采纳后这一条带着补充一起交给 AI,与 AI 原文冲突时以补充为准。补充上限 1000 字符,自动去掉首尾空白。
- 打字不落盘:输入框里的字只在点「采纳」的那一刻才随这条存下来(存进
- 点「采纳」或「忽略」。只有采纳之后,用户下一次发消息时 AI 才会收到一条
<MEMORY_SUGGESTION_ACCEPTED>通知;AI 到那时才总结措辞、写进记忆文件,并调用memory_suggest_done记账。 - 反悔窗口:采纳之后、AI 下一轮还没开始之前,卡片状态是「已采纳 · 可撤销,等 AI 下一轮写入」,并多出一个「撤销采纳」按钮——点它这条回到
pending,补充原样留着。一旦通知已经交到 AI 手里(状态变成「通知已送达 · 处理中」),撤销按钮就没了,并说明原因;如果点撤销的那一刻 AI 刚好开跑,服务端会如实拒绝并告知。 - 两种例外 AI 会先停下把 diff 摆出来:措辞与用户采纳的原文有实质差异;或这条与文件里已有内容重复、冲突。
打开「待确认」或「临时记忆」分页时,面板每 5 秒自动刷新一次。沉默 = 放弃:提议完 AI 就回去干手里的活,不催促。(memory-suggest.json 里最多留 80 条:未决定的一条都不丢,已决定的从最旧的开始丢。)
| 工具 | 干什么 | 会不会写记忆 |
|---|---|---|
memory_suggest | 提议一条(文件限那五个,正文上限 4000 字符,单条可带一句理由) | 不会 |
memory_suggest_done | 处理完一条后记账:写进了哪个文件,或为什么不写 | 不会 |
记忆库在哪(也可以直接改文件)
<DSH_HOME>\memory\
├── rules-must.md 必须遵守(硬性) ← 注入
├── agent.md 行为设定 ← 注入
├── profile.md 用户画像 ← 注入
├── rules-should.md 可选遵守(酌情) ← 注入
└── log.md 变更日志 ← 不注入,只给用户自己记账
第一次启动时,缺失的文件会用内置模板创建——模板里是 <!-- --> 注释说明,不注入;已经存在的文件从不覆盖。
直接记事本改也行,效果与面板完全一样——文件是唯一真相。
注入规则(决定它花多少 token)
| 行为 | 说明 |
|---|---|
| 注入位置 | 系统提示词的一个段落,order = -500:Harness 身份之后、人格设定之前 |
| 顺序 | 必须遵守 → 行为设定 → 用户画像 → 可选遵守 |
<!-- --> 注释 | 永远不注入。所以模板里的说明文字不占上下文、不干扰 AI |
| 只有标题没有正文 | 整节不注入。空文件 = 零开销;文件都没内容则整块不注入 |
| token 成本 | 记忆不变时系统提示词逐字节不变 → 吃满 prompt cache;改了才会有一次新快照 |
| 预算 | maxChars 默认 6000 字符。按节累加,某节装不下就截断它并注明「…(本段超出预算已截断,完整内容见记忆目录)」——剩余空间不足 120 字符就整节不留,后面的节也不再试 |
autoCapture | 默认关。关着时一个字都不多注入;打开后末尾多一段固定提醒,且不算进 6000 预算 |
| 第二段(临时记忆) | order = -490,紧跟在这一段之后,有自己的 tempMaxChars 预算;只有没归档的条目进,归档即消失。默认整段不存在 |
临时记忆(跨对话的活跃工作集)
长期记忆是用户写给 AI 长期遵守的;临时记忆是这一阵子没做完的活——跨对话交接、阶段性目标、等条件满足才做的待办。
| kind | 用来干什么 | 什么时候清掉 |
|---|---|---|
handoff 交接 | 一件事交到别的对话手里(换会话执行)。正文可以写长、写全:它同时就是交付文档 | 接手方做完后归档 |
stage 阶段目标 | 眼下的阶段性目标,省掉在多个对话里重复说明 | 阶段结束 |
waiting 等待项 | 等条件满足再执行(CI 绿了、文件到了、用户确认了……) | 条件满足、执行完 |
- 只在用户要求时写。只有用户说出「写临时记忆 / 记到临时记忆 / 交付记忆 / 换会话执行 / 下次对话接着做」,AI 才调用它;其余一律不调——不自动总结、不顺手记、不后台回写。
- 完成即清理。调用
temp_memory_done之后它立刻不再注入;数据留在盘上做归档(保留 30 天内、最多 20 条;没归档的一条都不剪),要物理删除得在面板里点「删除」。 - 存在
<DSH_HOME>\temp-memory.json,不在记忆目录里——它要跨对话,就得让所有会话共用一份。只有第一次写入时才创建这个文件。 - 预算独立、超了只点名:
tempMaxChars(默认 1200)只管这一层,与四层正文的maxChars互不挤占。装不下整条的不会被截一半塞进去,而是只留一行#id 全文见 <路径>——半截的交付文档比一个指针更糟。最新优先:预算不够时最晚更新的一条先保住。 - 文本是常量:没条目时整段不注入;有
waiting时才追加一小段固定提醒。绝不把时间、条数之类每轮都在变的东西写进注入文本,否则 prompt cache 每轮失效。 - 不保证「到点自动执行」。DSH 没有常驻观察者:
waiting做到的是「AI 在对话里能看到它,条件满足就直接做、做完归档」。要真正的定时/条件触发用schedule_create,本插件不做,也不假装做了。
| 工具 | 干什么 | 会不会写记忆 |
|---|---|---|
temp_memory({kind,title,body}) | 写一条,返回编号;下一个模型步骤就出现在上下文里。标题上限 200 字、正文上限 8000 字 | 只写 temp-memory.json,不碰那五个记忆文件 |
temp_memory_done({id,note}) | 归档一条(note 写结果),立刻停止注入 | 同上 |
面板里的「临时记忆」分页(tempMemory: true 时才出现):看 / 改 / 新增 / 标记完成(归档)/ 恢复为活跃 / 删除 / 清空归档,一处点完,不用重启。「待确认」分页仍然固定在最后一个。
让 AI 写记忆(两条流程)
- 「记住…」 → AI 把内容整理成条目,先把 diff 摆出来,用户确认后才落盘。
- 「整理记忆」 → AI 把这段时间的沉淀批量整理出来,同样先给用户审。
配置
在 profile 的 bundle patch 里(cordis.patch.yml):
- insert:
- id: user-memory
name: 'dsh-user-memory'
config:
enabled: true # false = 停用注入,不卸载
dir: '' # 空 = <DSH_HOME>/memory
maxChars: 6000 # 注入总预算(含前言与截断提示)
autoCapture: false # true = 打开「记忆采集提醒」
tempMemory: false # true = 再挂一段「临时记忆」
tempMaxChars: 1200 # 只给上面那一段的预算,与 maxChars 互不占用
改完需要重启才对 composition 生效;只改记忆文件内容则不需要。
实测注意:profile 里的
config:是「整块替换」,不是逐键合并。 在<DSH_HOME>/profiles/<profile>/cordis.patch.yml里写- id: user-memory+config:时,列出的键会顶掉 bundle 里的整套 config。比如只写autoCapture: true,读回来的就是config: { autoCapture: true }——dir和maxChars都不见了(退回内置默认值)。 所以覆盖时把需要生效的键一次写全:enabled/dir/maxChars/autoCapture/tempMemory/tempMaxChars。 (桌面版的 profile 由 App 自己管,命令行未必读得到它的 composition;这种情况就靠重启后看面板——那个新分页出现、且不是「未启用」,就是生效了。)
autoCapture:要不要让 AI 主动提建议(默认关)
默认 false。关着的时候,注入的提示词与没有这个开关时逐字节相同——一个字符都不多。
开关在面板顶部(不用切分页,打开面板就在),点一下就切换,不需要重启、不需要改配置文件——它写的是 <DSH_HOME>\.autocapture 这一个文件(内容就是一个 on 或 off),宿主每轮读到它就把注入段落同步过去,下一次发消息时就能看到效果。
优先级顺序:.autocapture 文件 > config.autoCapture > 内置默认 false。文件里认 on / off(也接受 1 / true / 0 / false,不区分大小写、忽略首尾空白);写了别的内容(例如手改坏了)等于「没有意见」,自动退回看配置——不会因为一个字写错就静默关掉整个功能。反过来,一旦在面板上点过,.autocapture 就一直盖住配置文件;想交还给配置,删掉那个文件即可。
也可以走配置文件(老办法):把 config: 里的 autoCapture 写成 true,然后重启 App。适合一开始就想默认打开、或者要写进 dotfiles 的场景。
打开后,注入段落的末尾会多出这一小节固定文本:
## 记忆采集提醒(开关已打开)
- 本次对话中若发现值得长期记住的用户偏好或规则(明说的要求、反复出现的偏好、明确的禁止项),调用 `memory_suggest` 把它放进待确认区。
- 不要直接改记忆文件:用户点了「采纳」才会真正写入。
- 只提你确实观察到的,不要为了凑数而提。
要点:不占预算(这段提醒不算进 maxChars,打开它不会挤掉记忆内容);文本是常量(内容写死在插件里,绝不随对话变化,否则 prompt cache 每轮失效,比多这几十个字贵得多);只是提醒,不是强制(不保证每轮都调,也不会让 AI 越权直接写文件)。这个开关只动注入文本:它不写记忆文件、不碰待确认区。「AI 从不自行写入」照旧。
安装
profile 用 link: 依赖指向插件目录(不拷贝),装好后 <DSH_HOME>/profiles/<profile>/node_modules/dsh-user-memory 就是那个目录的 junction:
// <DSH_HOME>/profiles/<profile>/package.json
"dsh-user-memory": "link:<插件目录>"
每个 profile 各装一次:profile 之间互相独立,装到哪个 profile 就只在哪个里生效。改代码改的就是插件目录里的文件。
自测
node test/selftest.mjs # 52 项:注入与服务端(含面板开关、autoCapture)
node test/suggest-test.mjs # 48 项:待确认闸门 + 补充框 + 撤销
node test/temp-test.mjs # 72 项:临时记忆(第二段注入、工具、预算、归档、面板接口)
node test/panel-test.mjs # 66 项:面板 UI;未设 UMEM_UI_DEPS 时打印 SKIP 并以 0 退出
四个都是纯 Node、离线、零依赖。panel-test.mjs 需要的 react / react-dom / jsdom 故意不是本插件的依赖(本插件零依赖、无构建步骤),装到一个临时目录再指给它,然后重跑上面那条命令:
npm install --prefix <临时目录> react@19 react-dom@19 jsdom@26
# PowerShell: $env:UMEM_UI_DEPS = '<临时目录>\node_modules'
# bash: UMEM_UI_DEPS='<临时目录>/node_modules'
suggest-test.mjs 里有一项会去调 DSH 自带的 schema 校验器(它藏在 DSH 安装目录里,路径每台机器不同):脚本自己按 $DSH_HOME 与 ~/.dsh 找,找不到就打印 SKIP;也可以设 UMEM_DSH_TOOLS 直接指出 .../dsh-tools/lib/index.js。
改完代码先跑这四个。改了 index.js 或 client.js 还要重启 App 才生效。
完全卸载 / 回退
在插件图形管理器里移除 dsh-user-memory 这个 bundle(AI 侧对应的动作是 plugin_manager 的 remove_bundle)。
插件消失,记忆库 <DSH_HOME>\memory\ 不会被删(数据与代码分离)。想彻底清干净就再手删那个目录。
已知边界
- 「必须遵守」不是技术强制。插件只能把规则放进系统提示词的强位置,不能让模型永不失误。写得具体、可判定、够短,比写得多更有效。
- 改代码需要重启。加载器用裸
import(),Node 的 ESM 模块缓存按路径记账且不失效——改index.js或client.js后必须重启 App。改记忆文件不需要。 - profile 相互独立。每个 profile 有各自的
node_modules,插件要各装一次。 - 界面文案硬编码中文。不走 locale 服务——单语言自用插件的取舍(
locale/里有 en/zh 两份 JSON,但面板文案不读它)。 - 临时记忆不跨进程同步。
temp-memory.json由拥有它的那个进程在内存里维护。手改这个文件、或同时跑两个进程时:磁盘上的改动要下次启动才生效,而且下一次写入会覆盖手改的内容——要改它请用面板。 waiting不是调度器。没有常驻观察者,条件是否满足由 AI 在对话里判断;要真正的定时/条件触发请用schedule_create。