把长期记忆写进本地 Markdown,跨会话记住你
28.5/100
综合分
0
GitHub 星标
—
npm 周下载
—/5
用户评分
数据截至 2026/10/3 · 周下载取自 npm 全网(含 CI / 镜像),非本站安装数;综合分由星标、活跃度与兼容性检查合成。
安装命令 · 需先装 dsh CLI 引擎
dsh plugin --profile web add s867968286/dsh-memory-md该包不可直接 npm 安装,改从 GitHub 源安装(明细见下方安装检查)
状态速览
判定依据:实装验证 · 静态安装检查 · 自动扫描 · 仓库维护状态信任档位:待复验
是什么
dsh 原生插件 · chat
安全吗
本站尚未对该插件做风险分级
暂未覆盖,不等同于无风险
还在维护吗
更新放缓:最近一次提交在 43 天前
档位由哪些信号合成
本站实装验证(真实安装 L4 覆盖全站;L5 为运行时冒烟或实机截图,覆盖范围小于 L4) · 验证所用 dsh 版本 · 静态安装检查 · 风险分级 · 仓库维护状态。 下方各区块是它的证据明细。
社区实装还没有人打卡 · 装过的第一个就是你
安全扫描自动 · 静态分析源码v2.7.0🔵 扫描:含敏感能力尚无人工评估展开查看扫描说明与证据 ↓
安全扫描自动 · 静态分析源码
v2.7.0静态扫描发现插件具备触达本地系统的能力:执行外部命令、读写/删除本地文件、读取剪贴板或密钥类环境变量(至少其一)。这是能力清单,不是危险判定——具体能力项与证据见下方,安装前请自行判断是否需要。
读写/删除本地文件发起外部网络请求
查看扫描证据(共 17 处,展示前 17 条)
- 发起网络请求client/client.js:95
const response = await fetch(`${API}${path}`, options) - 读取本地文件src/settings.mjs:139
const parsed = JSON.parse(readFileSync(paths.settingsFile, 'utf8') || '{}') - 读取本地文件src/inject.mjs:324
return Math.max(0, Math.floor((now - statSync(path).mtimeMs) / 86_400_000)) - 读取本地文件src/store.mjs:104
return existsSync(path) ? readFileSync(path, 'utf8') : undefined - 读取本地文件src/store.mjs:104
return existsSync(path) ? readFileSync(path, 'utf8') : undefined - 写入本地文件src/store.mjs:118
mkdirSync(dirname(path), { recursive: true }) - 写入本地文件src/store.mjs:121
writeFileSync(tmp, content, 'utf8') - 写入本地文件src/store.mjs:122
renameSync(tmp, path) - 删除本地文件src/store.mjs:126
rmSync(tmp, { force: true }) - 读取本地文件src/store.mjs:218
if (!existsSync(memoryDir)) return [] - 读取本地文件src/store.mjs:219
return readdirSync(memoryDir, { withFileTypes: true }) - 读取本地文件src/store.mjs:293
stat = statSync(path) - 读取本地文件src/store.mjs:457
const hadBody = existsSync(bodyPath) - 删除本地文件src/store.mjs:461
rmSync(bodyPath, { force: true }) - 写入本地文件src/store.mjs:640
mkdirSync(dirname(errorLogFile), { recursive: true }) - 写入本地文件src/store.mjs:641
appendFileSync(errorLogFile, `${stamp} ${oneLine}\n`, 'utf8') - 写入本地文件src/store.mjs:724
mkdirSync(diagnosticsDir, { recursive: true })
扫描于 2026/11/1(0 天前) · 范围 入口闭包 + 发布文件清单 · 来源 GitHub 仓库
静态扫描只反映源码中出现的调用,不构成安全背书;人工评估与自动扫描相互独立、互不覆盖。
给这个插件打分还没有人投票↓
用户评分
还没有人投票,来当第一个
功能亮点
- 跨会话记忆
简单的纯依赖MD文件的记忆插件,实现思路参考CodeBuddy和WorkBuddy的实现
README
由 DeepSeek 最新模型翻译生成dsh-memory-md
纯本地 Markdown 记忆插件(DSH)。助手通过 memory_md_ 工具把值得长期保留的内容写进 ~/.dsh/memory-md/,跨会话记住你。
记忆索引会在回合开始时作为上下文快照注入,所以助手不必先调工具就知道有什么记忆。
一、设计要点
1. 注入分两段:常量进提示词,读盘的进快照
记忆文本按是否读盘分两类,走不同通道:
| | 内容 | 通道 | 为什么 |
| 协议 + 行为纪律 | 索引是什么、怎么写记忆(先查再写、删过期、按主题组织、四类型判据……) | systemPrompt.section()(系统提示词) | 纯常量,逐字节恒定 → 前缀 KV Cache 始终命中,不产生新消息 |
| 索引 | 用户级 + 项目级 MEMORY.md | systemPrompt.context()(上下文快照) | 读盘、随记忆变化 → 进 section 会让整个前缀缓存失效 |
两个方向的红线都成立:
- 读盘的内容绝不允许进 section() —— DSH 每个 step 都重新装配系统提示词(dsh-agent-loop 的 systemPrompt.assemble())。任何随文件变化的 section 都会让整个前缀的 KV Cache 失效(含全部历史)。
展开完整文档(README 剩余 1 节)
dsh-memory-md
纯本地 Markdown 记忆插件(DSH)。助手通过 memory_md_ 工具把值得长期保留的内容写进 ~/.dsh/memory-md/,跨会话记住你。
记忆索引会在回合开始时作为上下文快照注入,所以助手不必先调工具就知道有什么记忆。
一、设计要点
1. 注入分两段:常量进提示词,读盘的进快照
记忆文本按是否读盘分两类,走不同通道:
| | 内容 | 通道 | 为什么 |
| 协议 + 行为纪律 | 索引是什么、怎么写记忆(先查再写、删过期、按主题组织、四类型判据……) | systemPrompt.section()(系统提示词) | 纯常量,逐字节恒定 → 前缀 KV Cache 始终命中,不产生新消息 |
| 索引 | 用户级 + 项目级 MEMORY.md | systemPrompt.context()(上下文快照) | 读盘、随记忆变化 → 进 section 会让整个前缀缓存失效 |
两个方向的红线都成立:
- 读盘的内容绝不允许进 section() —— DSH 每个 step 都重新装配系统提示词(dsh-agent-loop 的 systemPrompt.assemble())。任何随文件变化的 section 都会让整个前缀的 KV Cache 失效(含全部历史)。
- 常量不该混进 context() —— 快照是追加而非替换。常量一旦和读盘内容捆在同一条快照里,读盘内容一变就把常量整段重发,而重发的那份永久留在会话历史里。常量放 section() 每步都在,但逐字节恒定 → 缓存命中,不产生新消息,严格更优。
// 协议 + 纪律:静态常量,写死在提示词里
ctx.systemPrompt.section({ name: 'memory-md:protocol', order: 950, text: () => memoryPromptText() })
// 索引:每次装配重读;空串则不贡献
ctx.systemPrompt.context({
name: 'memory-md:index',
order: 10_000,
text: () => renderMemoryIndex(...),
})
实测体量:常量段 2638 字符(协议 230 + 纪律 2406),索引快照约 1478 字符。
一段走过的弯路(已修正):行为纪律曾经被拼进 renderMemoryIndex() 的产物,与索引同走 context()。结果是规则一字未改,却因为和读盘的索引捆在同一条快照里,索引一变就带着 2400 多字整段重发 —— 等于把常量的缺点(重发)和动态内容的缺点(耦合)都占了。当时给的理由是「让模型读到『有哪些记忆』的同时读到『该怎么对待』」,但那是个读者便利的考虑,代价却由每一轮请求承担。CodeBuddy 的原版犯同一个错(把规则与 ## Current MEMORY.md contents 拼成一整块 <memory> 每次全量注入),不该照搬。
早先把两段都放进 context() 时问题更严重:协议文本跟着每次记忆变化重发。
行为纪律的内容与来源
规则文本(MEMORY_RULES)吸收自 CodeBuddy 的记忆提示词,只吸收适用于 DSH 的部分:
- 吸收:按主题而非时间组织、更新或删除过期/错误记忆、写前先查避免重复、用户明确要求就立刻办、用户纠正了从记忆里说出的说法必须改掉、写前先核实。附正面清单(什么值得存)与负面清单(什么不该存)—— 两者成对,只给约束不给目标等于没给规则。
- 吸收(typed 路径):四类记忆各自的「何时写 / 怎么写 / 为什么」(## Types of memory 的 <when_to_save> / <how_to_use> / <body_structure>)、「记忆不等于当前事实」(## Before recommending from memory)、「用户说别用记忆时当作空」(## When to access memories)。
- 排除:依赖 CodeBuddy 具体工具特性的说法(write to it directly with the Write tool、Use the Write and Edit tools —— 我们的模型不能传路径,写盘由插件走 Host 侧 fs);grep .jsonl 会话转录(DSH 的会话日志是 session.jsonl.zstd 压缩格式,直接 grep 不可行);<examples> 逐条照搬(那些是英文通用编码场景,与中文 DSH 语境错配)。
为什么不能只放工具 description:这是事前纪律,不是工具用法。模型得在决定要不要写的时刻就知道「先查再写」,等它已经调 memory_md_save 时才看到就晚了。工具 description 只适合放「这个工具怎么调」。
没有索引时快照为空,但纪律照常在。 纪律是常量、走 section(),它不可能依赖"磁盘上有没有索引"——所以一条记忆都没有时,提示词段里仍有纪律,只是
context() 那份快照返回空串(不注入任何东西)。这是刻意的:纪律讲的正是
"什么时候该去写记忆",恰恰在空库时最需要它。
后台总结也要防重复。 后台总结是独立 LLM 调用,它看不到注入快照(快照进的是主对话)。所以:
1. SUMMARY_SYSTEM 里有一份防重复规则:告诉它看不到记忆库、只写本段对话里新出现的明确事实、宁可漏记也不要重复记;并明确 <memory-index> 块是已有记忆的索引、不是用户说过的话 —— 否则它会据此再写一条重复记忆(自我喂养)。
2. 消息末尾附上「已有的记忆」清单(memoryManifestFor())—— 这是机械保障,不是叮嘱。清单里每条带 [scope] 前缀、类型、文件名、年龄与描述,模型据此判断"是不是已经有同一件事的条目,该覆盖哪个文件"。
只叮嘱不喂清单是没用的:模型无从知道已经有什么。这一招吸收自 CodeBuddy 的 buildExtractPrompt() —— 它把 formatMemoryManifest(scanMemoryFiles(dir)) 作为 "## Existing memory files" 一节喂给抽取子代理。
索引超过 1 天会带年龄属性。 索引是一份快照,记的是写下那一刻的记忆库状态。所以旧索引的标签会多一个 updated 属性:
<memory-index scope="global" updated="10 天前">
当天/昨天的不附(不存在「过期」问题,挂了只是噪音)。机制吸收自 CodeBuddy 的 memoryFreshnessText() —— 那边是注入时附一句 "This memory is N days old … Verify against current code before asserting as fact.";这里改成标签属性,因为它天然属于这份索引,模型不必再读一句话去对应它是说给谁听的。规则段里有配套的行为要求(见下)。
去重是白拿的。 dsh-agent-loop 的 RuntimeContextProjection.project() 会比对上一次保留的快照文本,内容未变则不产生任何消息 —— 所以「索引变了才注入、没变就不重复注入」由 loop 负责,插件不需要自己做跳过机制。
索引带确定性标签。 每份索引用 <memory-index scope="global|project" cwd="…"> 裹住,模型不必靠上下文猜这段文本的边界和作用域:
<memory-index scope="global">
- 用户偏好 — 用中文回复 </memory-index>
注入的只有索引,不含分类文件全文;模型命中描述后自行读原文。
⚠️ 注入内容会被官方二次处理,{{...}} 必须先中和。 官方 interpolate() 把注入文本里的
{{name}} 当作提示词变量严格校验(变量名须匹配 /^[a-z][a-z0-9_]$/),且对
context() 和 section() 都生效。所以记忆描述里只要出现 {{挖空}}、{{.Server.Version}}
这类合法模板语法,就会在 systemPrompt.assemble() 里抛错 —— 整个回合失败,而且每轮都失败、永久锁死该工作区(agent 无法自救,只能手工改 MEMORY.md)。
修法是在注入边界做相邻花括号转义({{ → } 中和 —— 官方 interpolate() 把注入文本里的 {{...}} 当提示词变量*
严格校验,且对 context() 与 section() 都生效。记忆里含 {{ 会抛错炸整轮,并永久锁死那个工作区(每轮都炸、agent 无法自救,只能手工改 MEMORY.md)。
故在注入边界把相邻花括号转义({{ → ,
"dsh": { "profile": { "bundles": [ "...", "dsh-memory-md" ] } }
}
3. 安装并重启 bash
cd ~/.dsh/profiles/web && pnpm install
然后重启 dsh(Host 半是 bundle 行插件,进程启动时装载)。
4. 验证 bash
node D:/workspaces/ai/dsh-memory-md/test/run.mjs
四、目录结构
记忆根目录固定,不在任何项目目录内:
~/.dsh/memory-md/ # Windows: C:\Users\<你>\.dsh\memory-md\
├── settings.json # 插件设置 ├── error.log # 错误日志(与设置同级,排查看这层) ├── global/ # 用户级(跨项目) │ ├── MEMORY.md # 索引(插件维护,唯一入口) │ ├── memory/ # 记忆正文 │ │ └── user_xxx.md │ └── .journal/ # 留痕(默认关) │ └── YYYY-MM-DD.md
└── {slug}/ # 项目级(结构同上)
├── MEMORY.md
├── memory/
│ ├── feedback_aaa.md
│ └── project_bbb.md
└── .journal/
索引在作用域根,正文在 memory/ 子目录,日志在 .journal/ —— 三者同级。这样作用域根一眼看清结构,memory/ 与 .journal/ 也彼此对称。索引里的链接相应带子目录:- 数据库端口 — …。
从旧版升级(正文曾平铺在作用域根):跑一次迁移脚本 —— 这是一次性运维动作,不是插件运行时行为(新写入本来就会落在正确位置)。
bash
node scripts/migrate-memory-layout.mjs --dry-run # 先看会做什么
node scripts/migrate-memory-layout.mjs # 实际执行
改了目录结构但还没重启 DSH 时,旧进程仍会往旧路径写新文件 —— 所以重启后要再跑一次,把这段时间散在作用域根的文件收进去。脚本幂等:已搬过的不会再动,没得搬时直接报「无事可做」。
安全保证:只搬带已知 type: frontmatter 的真记忆(别的文件一律不动),目标已存在则跳过并报告,绝不覆盖,只 rename 不改内容。跑前建议先备份。
{slug} 由工作区绝对路径压缩而来(盘符与分隔符转 -、全小写):
D:\workspaces\ai\dsh-memory-md → d-workspaces-ai-dsh-memory-md
五、记忆文件格式 markdown
name: 数据库端口 description: 本地数据库跑在 5433 type: reference
正文。feedback / project 类型建议写成:规则或事实,然后 Why: 与 How to apply: 两行。
索引 MEMORY.md 由插件维护,一行一条,超过 200 行会被截断(只影响读取时的截断,文件本身不动): markdown
- 数据库端口 — 本地数据库跑在 5433
六、设置
设置 → 记忆设置:
| 项 | 默认 | 说明 |
| 启用记忆 | 开 | 关掉后不注入上下文快照、拒绝保存、不做后台总结;读与搜索仍可用 |
| 工作留痕 | 关 | 是否允许写日志(只写不读)。后台总结与 memory_md_journal 都受它控制 |
| 触发轮数 | 2 | 累积够这么多轮就跑一次后台总结。与「触发字符数」任一达标即触发 |
| 触发字符数 | 2000 | 这一段新增内容够长也触发一次。单条很长(如一次长分析)时不必等够轮数 |
| 在这些预设下停用 | 空 | 一行一个 preset id,支持 通配(如 presetmd-)。这些预设里的对话不读也不写全局记忆 |
两个阈值清空即用默认值(服务端归一化兜底);填非正数也回落默认,不会静默夹到下限。改动点「保存」后生效。
预设级停用
本插件挂在 host 平面,默认对所有 agent 生效 —— 包括官方四个预设。
但某个预设可能自带独立记忆(跟着预设走的人格记忆),那时全局记忆必须整体让位,否则两套记忆同时生效、互相干扰。
disabledPresets 就是为此。命中后三条入口全停:
| 入口 | 行为 |
| 五个 memory_md_ 工具 | 直接拒绝并说明原因 |
| 后台总结 | 不跑(不写记忆、不写日志) |
| 记忆注入 | 不注入协议段与索引快照 |
为什么连读也停:只拦写会让 agent 读到一个它不该依赖的记忆库,于是回答里混进别的项目的上下文 —— 比完全不生效更难排查。
判定依据是 session.header.agentPreset(durable,随会话保存)。
没有 preset id 时永不匹配,即默认全局生效 —— 官方预设不受影响。
为什么不做成「只在某预设下生效」的白名单:那会让新装的预设默认没有记忆。
「默认有、特定预设退出」更符合直觉,漏配时也只影响那一个预设。
设置页有开关 + 两个触发阈值 + 四条固定路径(记忆目录、用户级目录、设置文件、错误日志),外加预设停用名单。改动点「保存」才生效(攒草稿,不即时提交)。
设置存 <dshHome>/memory-md/settings.json。
刻意不显示任何与会话有关的东西 —— 不列条目、不显示记忆卡片与计数、不显示项目级目录或留痕目录。原因:设置页是全局页面,拿不到「当前会话」, Host 半只能退回 sessions.list() 去猜工作区,而官方文档明确该列表是创建顺序 —— 于是永远返回最老的会话,切换会话后界面仍显示上一个会话的路径。猜错的路径比不显示更糟,所以整块去掉。
七、开发 bash
node test/run.mjs # 跑全部 15 个测试套件
| 套件 | 覆盖 |
| differential | 截断逻辑与 CodeBuddy 2.150.0 真实 bundle 逐字节比对 |
| load | 按包名装载插件(与真实 loader 一致),确认工具与两段式注入注册 |
| paths | 路径解析到 DSH 用户目录,不落工作区 |
| routes | HTTP 接口:只给与会话无关的路径、内容接口已移除 |
| client | client 半依赖白名单与注册契约 |
| empty-state | 设置页不显示条目、卡片、计数与会话相关路径 |
| inject | 两段式注入:索引标签、只注入索引、截断、协议不进快照 |
| summarize | 后台总结:解析、写记忆与日志、开关与异常隔离 |
| preset-exclude | 预设停用名单:读写日志全拦 |
| regression-kind | 从真实会话日志取证:缺 source 会崩回合 |
| schema | 工具 schema 过 DSH 真实的 assertSupportedJsonSchema |
| tools | 工具行为:写入位置、索引维护、越界拒绝 |
代码结构
| 文件 | 作用 |
| src/index.js | Host 半:注册工具 + 两段式注入 + HTTP 接口 |
| src/tools.mjs | 五个记忆工具的实现与说明书 |
| src/inject.mjs | 注入:MEMORY_PROTOCOL(提示词段)+ renderMemoryIndex(快照) |
| src/summarize.mjs | 轮末后台总结:独立 LLM 调用,写记忆与日志 |
| src/schema.mjs | schema 转换(不用 defineTool,见下) |
| src/routes.mjs | HTTP 接口 /memory-md/api/ |
| src/settings.mjs | 设置读写 |
| src/store.mjs | 共享写入原语:原子写、记忆正文、日志追加、索引行、错误日志 |
| src/context.mjs | 路径解析 |
| src/codebuddy-port.mjs | 截断与 frontmatter(CodeBuddy 移植) |
| client/client.js | 设置页(settings.section) |
⚠️ 为什么顶层不声明 inject: ['webServer']
cordis 的 inject 是必要依赖:依赖未就绪时 fiber 停在 INACTIVE,apply() 根本不会执行(实测:缺 webServer 时 apply() 不跑,没有 inject 声明的照常跑)。
而 webServer 只由 dsh-web-app bundle 提供(它的 cordis.patch.yml:136 插入了 dsh-host-webserver)——dsh-base、dsh-headless、dsh-acp-app 都不含它。
所以把 webServer 写进顶层 inject 的后果是:在 headless / acp profile 下,插件整体静默失效 —— 不只是设置页没有,而是五个记忆工具与两段式注入全部不注册,且不打任何警告,排查时完全看不出原因。
因此本插件分层:
| 能力 | 依赖 | 缺 webServer 时 |
| 五个记忆工具 | ctx.get('tools') | ✅ 照常注册(取不到只 warn) |
| 两段式注入 | ctx.inject(['systemPrompt']) | ✅ 照常生效 |
| 后台总结 | ctx.inject(['llm']) | ✅ 照常运行 |
| 设置页路由 | ctx.inject(['webServer']) | ⛔ 只有它不注册 |
load 套件有一条防回归断言:mod.inject 必须为空数组 —— 谁再把 webServer 提升成整个插件的准入门槛,测试立刻变红。
⚠️ 为什么不用 defineTool
@deepseek-ai/dsh-tools 不在 profile 的可解析范围内(profile 只暴露 cosmokit 与 schemastery),插件的 import 必然失败。
好在 ctx.tools.register() 要的本来就是纯 JSON Schema(内部调 assertSupportedJsonSchema,不是 spec 转换器),所以 src/schema.mjs 自己做转换。两个硬性要求:
- 可选字段必须完全不写 required;写 required: false 会被拒绝
- required 只能出现在对象根,属性节点上带着它会被拒绝
schema 套件用 DSH 真实的校验器验证这两点,而不是靠约定。
与同机其它插件的关系
同机还装着 dsh-preset-md(伙伴设置),它也占用 settings.section。两者是并列的两页,互不替换:slot id 不同、各插各的 (前缀 .mmd- vs .pmd-)、不共享模块。
样式令牌与参数行布局沿用 dsh-preset-md 的约定 —— 两个页面并排时不该长得像两个产品。这是刻意的视觉对齐,不是代码复用。
⚠️ client 半不能引用已消失的包
真事故:两个设置页同时变成「裸 HTML」。
根因是两者的 client 都 require('@deepseek-ai/dsh-client-ui-primitives'),而该包在当前 DSH 版本里已不存在(find 全盘无结果,dsh/package.json 也未提及):
FAIL @deepseek-ai/dsh-client-ui-primitives MODULE_NOT_FOUND
OK @deepseek-ai/dsh-client-ui-settings
dsh-preset-md 在 factory 顶层解构它,所以一抛错 apply 就从不执行 →
ensureStyles() 从不调用 → 整页裸样式。
教训:client 半的任何外部 require 都必须先确认包真的存在,不能靠「别的插件这么写」来推断。
client 套件把这件事写成显式白名单 —— 新增依赖必须同时改测试,并且点名禁止 primitives 再被引入。
八、卸载 bash
1. 从 profile 的 package.json 移除 dsh-memory-md(dependencies 与 bundles 两处)
2. 删链接并重装
cd ~/.dsh/profiles/web/node_modules && cmd //c "rmdir dsh-memory-md"
cd ~/.dsh/profiles/web && pnpm install
记忆文件不会被删(在 ~/.dsh/memory-md/),需要时手动清理。预览分享卡片长什么样 ↓
分享 · DeepSeek Harness Hub
s867968286/dsh-memory-md
把长期记忆写进本地 Markdown,跨会话记住你
相关插件 · 对话 / 记忆
上下文中枢oceanbase/powercontext
上下文跟着工作走 —— 跨会话、跨智能体、跨人。
★ 1,462已汉化DSH
73
智能体记忆库plastic-labs/honcho
Honcho 是记忆基础设施,用来构建有状态的智能体。这些智能体能理解不断变化的人、智能体、群组、项目和想法。
★ 7,848已汉化DSH
71.7
长程智能体控制面loopx-project/loopx
给 Agent 一个目标,让工作持续向前。
★ 6,358已汉化DSH
71.3
智能体上下文数据库volcengine/OpenViking
给 AI 智能体用的上下文数据库
★ 41,246已汉化DSH
71.3
长效机器人团队Fei2411/DS-Bot
DS Bot 是一个基于 DeepSeek Harness简称 dsh的 bot 插件,插件将 dsh 改造为 bot…
★ 4,717已汉化DSH
71.1
可携带记忆层EverMind-AI/EverOS
为什么选择 EverOS
★ 13,711已汉化DSH
71.1