🎁 福利专区全网大模型免费应用 + 新用户福利 + 注册活动入口,低成本玩转 AI

标签记忆库 marsma-101/dsh-tag-memory

DeepSeek Harness
marsma-10112 天前有提交已提供中文文档扫描:含敏感能力

把智能体记忆存成可查可导的 CSV 表格

31.5/100
综合分
2
GitHub 星标
—
npm 周下载
—/5
用户评分

数据截至 2026/10/22 · 周下载取自 npm 全网(含 CI / 镜像),非本站安装数;综合分由星标、活跃度与兼容性检查合成。

安装命令 · 需先装 dsh CLI 引擎
dsh plugin --profile web add marsma-101/dsh-tag-memory
该包不可直接 npm 安装,改从 GitHub 源安装(明细见下方安装检查)

状态速览

判定依据:实装验证 · 静态安装检查 · 自动扫描 · 仓库维护状态
信任档位:已验证
是什么
dsh 原生插件 · chat
装得上吗
本站已在 dsh 0.2.1-rc.2 下真实安装成功
非静态推断
L4 · 真实安装等级说明 →
安全吗
本站尚未对该插件做风险分级
暂未覆盖,不等同于无风险
还在维护吗
活跃:最近一次提交在 12 天前
档位由哪些信号合成

本站实装验证(真实安装 L4 覆盖全站;L5 为运行时冒烟或实机截图,覆盖范围小于 L4) · 验证所用 dsh 版本 · 静态安装检查 · 风险分级 · 仓库维护状态。 下方各区块是它的证据明细。

验证判据与等级说明 →
社区实装还没有人打卡 · 装过的第一个就是你
安全扫描自动 · 静态分析源码
v2.7.0
🔵 扫描:含敏感能力尚无人工评估
展开查看扫描说明与证据 ↓

静态扫描发现插件具备触达本地系统的能力:执行外部命令、读写/删除本地文件、读取剪贴板或密钥类环境变量(至少其一)。这是能力清单,不是危险判定——具体能力项与证据见下方,安装前请自行判断是否需要。

读写/删除本地文件写入剪贴板(不计风险)发起外部网络请求
查看扫描证据(共 157 处,展示前 79 条)
  • 读取本地文件
    index.js:398
    try { hasNotice = readFileSync(f.path, 'utf8').includes(NOTICE_BEGIN) } catch { }
  • 读取本地文件
    index.js:400
    try { backups = readdirSync(f.dir).filter((n) => n.startsWith(`${basename(f.file)}.bak-`)).length } catch { }
  • 读取本地文件
    index.js:745
    if (!existsSync(SCHEMA_PATH)) {
  • 读取本地文件
    index.js:750
    try { raw = JSON.parse(readFileSync(SCHEMA_PATH, 'utf8')) } catch (e) {
  • 读取本地文件
    index.js:754
    try { out.mtime = statSync(SCHEMA_PATH).mtime.toISOString() } catch { }
  • 读取本地文件
    index.js:857
    if (!existsSync(BACKUP_CFG_PATH)) return {}
  • 写入本地文件
    index.js:870
    writeFileSync(tmp, JSON.stringify(obj, null, 2) + '\n', 'utf8')
  • 写入本地文件
    index.js:871
    renameSync(tmp, path)
  • 删除本地文件
    index.js:873
    try { unlinkSync(tmp) } catch { }
  • 写入本地文件
    index.js:915
    if (!existsSync(raw)) { mkdirSync(raw, { recursive: true }); created = true }
  • 写入本地文件
    index.js:952
    writeFileSync(tmpPath, bytes)
  • 写入本地文件
    index.js:953
    renameSync(tmpPath, targetPath)
  • 删除本地文件
    index.js:955
    try { unlinkSync(tmpPath) } catch { }
  • 写入本地文件
    index.js:1120
    try { appendFileSync(join(st.dir, 'last-backup.log'),
  • 读取本地文件
    preset.js:1012
    const { header, rows } = core.readTable(readFileSync(CSV_PATH, 'utf8'), core.legacyWidthOf(s))
  • 读取本地文件
    preset.js:1062
    const { header, rows } = core.readTable(readFileSync(CSV_PATH, 'utf8'), core.legacyWidthOf(schema))
  • 发起网络请求
    client.js:326
    const response = await fetch(url, {
  • 发起网络请求
    client.js:341
    const response = await fetch(url, {
  • 写入剪贴板
    client.js:919
    await navigator.clipboard.writeText(text)
  • 读取本地文件
    lib/store.mjs:119
    if (existsSync(modern)) return modern
  • 读取本地文件
    lib/store.mjs:120
    if (existsSync(legacy)) return legacy
  • 读取本地文件
    lib/store.mjs:170
    if (existsSync(CSV_PATH)) return false
  • 读取本地文件
    lib/store.mjs:171
    if (existsSync(STORE_DIR)) {
  • 读取本地文件
    lib/store.mjs:172
    try { if (readdirSync(STORE_DIR).length > 0) return false } catch { return false }
  • 读取本地文件
    lib/store.mjs:243
    if (!existsSync(_dir) && _dir !== defaultDataDir()) {
  • 写入本地文件
    lib/store.mjs:257
    mkdirSync(STORE_DIR, { recursive: true })
  • 写入本地文件
    lib/store.mjs:258
    mkdirSync(REFERENCES_DIR, { recursive: true })
  • 写入本地文件
    lib/store.mjs:265
    writeFileSync(dst, readFileSync(src))
  • 写入本地文件
    lib/store.mjs:269
    writeFileSync(SCHEMA_PATH, readFileSync(ENGINE_SCHEMA_PATH))
  • 读取本地文件
    lib/notice.mjs:58
    try { names = readdirSync(dir).filter((n) => n.startsWith(prefix)) } catch { return }
  • 删除本地文件
    lib/notice.mjs:63
    try { unlinkSync(join(dir, n)) } catch { }
  • 读取本地文件
    lib/notice.mjs:77
    if (!existsSync(presetRoot)) return out
  • 读取本地文件
    lib/notice.mjs:80
    try { entries = readdirSync(presetRoot, { withFileTypes: true }) } catch { return out }
  • 读取本地文件
    lib/notice.mjs:86
    try { files = readdirSync(dir) } catch { continue }
  • 读取本地文件
    lib/notice.mjs:90
    try { if (!statSync(path).isFile()) continue } catch { continue }
  • 读取本地文件
    lib/notice.mjs:168
    if (!existsSync(filePath)) return none
  • 写入本地文件
    lib/notice.mjs:223
    copyFileSync(filePath, backupPath)
  • 写入本地文件
    lib/notice.mjs:226
    writeFileSync(filePath, next, 'utf8')
  • 写入本地文件
    lib/notice.mjs:262
    copyFileSync(filePath, backupPath)
  • 写入本地文件
    lib/notice.mjs:265
    writeFileSync(filePath, next, 'utf8')
  • 读取本地文件
    lib/engine/snapshot.mjs:65
    if (!existsSync(dir)) return []
  • 读取本地文件
    lib/engine/snapshot.mjs:67
    try { names = readdirSync(dir) } catch { return [] }
  • 读取本地文件
    lib/engine/snapshot.mjs:101
    if (!existsSync(join(dir, name)) && !taken.has(name)) return { stamp, name }
  • 删除本地文件
    lib/engine/snapshot.mjs:118
    try { unlinkSync(s.path); removed.push(s.name) } catch { }
  • 读取本地文件
    lib/engine/snapshot.mjs:125
    if (!existsSync(dir)) return []
  • 读取本地文件
    lib/engine/snapshot.mjs:127
    try { names = readdirSync(dir) } catch { return [] }
  • 写入本地文件
    lib/engine/snapshot.mjs:138
    writeFileSync(tmpPath, bytes)
  • 写入本地文件
    lib/engine/snapshot.mjs:139
    renameSync(tmpPath, targetPath)
  • 删除本地文件
    lib/engine/snapshot.mjs:141
    try { unlinkSync(tmpPath) } catch { }
  • 读取本地文件
    lib/engine/snapshot.mjs:161
    if (!existsSync(csv)) {
  • 写入本地文件
    lib/engine/snapshot.mjs:167
    try { mkdirSync(dir, { recursive: true }) } catch (e) {
  • 读取本地文件
    importers/memory-md.mjs:180
    try { text = readFileSync(src, 'utf8') } catch (e) {
  • 写入本地文件
    importers/memory-md.mjs:187
    writeFileSync(outPath, res.csv, 'utf8')
  • 读取本地文件
    lib/engine/backup.mjs:84
    const sha = (p) => createHash('sha256').update(readFileSync(p)).digest('hex')
  • 读取本地文件
    lib/engine/backup.mjs:86
    const b = readFileSync(p)
  • 读取本地文件
    lib/engine/backup.mjs:93
    const v = JSON.parse(readFileSync(path, 'utf8'))
  • 读取本地文件
    lib/engine/backup.mjs:131
    if (!existsSync(path)) return { path, exists: false, config: {}, broken: false }
  • 读取本地文件
    lib/engine/backup.mjs:133
    return { path, exists: true, config: JSON.parse(readFileSync(path, 'utf8')), broken: false }
  • 读取本地文件
    lib/engine/backup.mjs:159
    const currentCount = existsSync(csvPath) ? countEntries(readFileSync(csvPath, 'utf8')) : 0
  • 写入本地文件
    lib/engine/backup.mjs:298
    if (!existsSync(dir)) mkdirSync(dir, { recursive: true })
  • 写入本地文件
    lib/engine/backup.mjs:342
    copyFileSync(p.s, p.t)
  • 写入本地文件
    lib/engine/backup.mjs:385
    writeFileSync(configPathOf(storeDir), JSON.stringify(next, null, 2) + '\n', 'utf8')
  • 删除本地文件
    lib/engine/backup.mjs:395
    try { if (existsSync(join(dir, name))) unlinkSync(join(dir, name)) } catch { }
  • 写入本地文件
    lib/engine/backup.mjs:403
    try { appendFileSync(join(dir, 'last-backup.log'), line + '\n', 'utf8') } catch { }
  • 读取本地文件
    lib/engine/core.mjs:200
    if (!existsSync(csvPath)) { out.error = '找不到 ' + csvPath; return out }
  • 读取本地文件
    lib/engine/core.mjs:205
    const text = readFileSync(csvPath, 'utf8')
  • 写入本地文件
    lib/engine/core.mjs:221
    writeFileSync(tmp, newHead + rest, 'utf8')
  • 写入本地文件
    lib/engine/core.mjs:222
    renameSync(tmp, csvPath)
  • 删除本地文件
    lib/engine/core.mjs:693
    try { unlinkSync(lockPath) } catch { }
  • 删除本地文件
    lib/engine/core.mjs:698
    if (Date.now() - statSync(lockPath).mtimeMs > staleMs) { unlinkSync(lockPath); continue }
  • 读取本地文件
    lib/engine/core.mjs:698
    if (Date.now() - statSync(lockPath).mtimeMs > staleMs) { unlinkSync(lockPath); continue }
  • 读取本地文件
    lib/engine/core.mjs:885
    if (!logPath || !existsSync(logPath)) return out
  • 读取本地文件
    lib/engine/core.mjs:887
    try { text = readFileSync(logPath, 'utf8') } catch { return out }
  • 写入本地文件
    lib/engine/core.mjs:958
    appendFileSync(logPath, buf, 'utf8')
  • 读取本地文件
    lib/engine/core.mjs:1064
    if (!existsSync(csvPath)) { out.code = 'NO_FILE'; out.error = '找不到 ' + csvPath; return out }
  • 写入本地文件
    lib/engine/core.mjs:1137
    writeFileSync(tmp, built, 'utf8')
  • 写入本地文件
    lib/engine/core.mjs:1138
    renameSync(tmp, csvPath)
  • 删除本地文件
    lib/engine/core.mjs:1140
    try { unlinkSync(tmp) } catch { }
  • 写入本地文件
    lib/engine/core.mjs:1421
    if (!existsSync(missesPath)) writeFileSync(missesPath, head, 'utf8')
扫描于 2026/11/3(0 天前) · 范围 入口闭包 + 发布文件清单 · 来源 GitHub 仓库

静态扫描只反映源码中出现的调用,不构成安全背书;人工评估与自动扫描相互独立、互不覆盖。

依赖的 DSH / Cordis 模块
@deepseek-ai/dsh-tools@deepseek-ai/schemastery
给这个插件打分还没有人投票↓
用户评分
还没有人投票,来当第一个

dsh插件,将记忆保存到csv文件并稳定读写,闲时可整理的记忆库系统

README

由 DeepSeek 最新模型翻译生成
dsh-tag-memory DSH 插件:让智能体有一份自己的记忆库——存成 CSV,稳定读写,闲时还能整理。 每轮自动交给模型两样东西:一份标签字典(怎么判——这条该不该记、每个维度取什么值)和一份记忆快照(现在能用上的条目直接摆在眼前)。写和查交给两个工具;条目在界面上看得见、搜得到、导得出;备份 / 恢复 / 合并都在那一页上。 解决什么问题 - 换个会话就失忆:上一轮学到的东西,这一轮不在了。 - 记忆看不见:谁记了什么、记到哪一步,没有一处能翻。 - 不敢让它自己记:模型自由发挥写进去的东西格式五花八门,回头查不动也改不动。 答案:记忆只有一种形态——一张只追加的 CSV。模型只管判断,程序只管落笔。 于是它同时是一张随时能翻的表格、一份随时能导出的 CSV、一个随时能备份和回滚的文件。
展开完整文档(README 剩余 1 节)
dsh-tag-memory

DSH 插件:让智能体有一份自己的记忆库——存成 CSV,稳定读写,闲时还能整理。

每轮自动交给模型两样东西:一份标签字典(怎么判——这条该不该记、每个维度取什么值)和一份记忆快照(现在能用上的条目直接摆在眼前)。写和查交给两个工具;条目在界面上看得见、搜得到、导得出;备份 / 恢复 / 合并都在那一页上。

解决什么问题

- 换个会话就失忆:上一轮学到的东西,这一轮不在了。
- 记忆看不见:谁记了什么、记到哪一步,没有一处能翻。
- 不敢让它自己记:模型自由发挥写进去的东西格式五花八门,回头查不动也改不动。

答案:记忆只有一种形态——一张只追加的 CSV。模型只管判断,程序只管落笔。
于是它同时是一张随时能翻的表格、一份随时能导出的 CSV、一个随时能备份和回滚的文件。

是什么形式

一个插件,三块:

| 文件 | 挂在哪 | 干什么 |
| preset.js | 每个智能体 | systemPrompt.section 每轮注入标签字典 + 记忆快照;注册 memory_write / memory_recall 两个工具 |
| index.js | DSH 宿主 | 记忆文件托管:探测各智能体的 MEMORY.md,写一段受控的说明块;挂 /tag-memory-api 给界面取数 |
| client.js | 浏览器 | 「记忆」页:条目表格 + 维度筛选 + 搜索;标题行上带 备份 / 恢复 / 合并 |

零依赖:不引入向量库、外部 API 或服务,全部落在本地文件上。

两段式:模型判断,程序执行

字典里写着每个维度能取什么值、有什么规则(稳定性 只有 稳定 / 易变;深度 只在 类型=记忆 时才有值……)。模型读字典、填字段、调工具;编号、校验、追加、去重、筛选、返回,全是程序干的。

好处是可回溯:库里每条都带着当时「为什么记、凭什么这么判」。
判定规则以后改了,历史条目也不会被悄悄改写。

写不进去的时候,话得说清。 校验不通过不是甩一句「不能为空」——缺 环境 / 依据,说的是「这条在什么条件下成立、凭什么」,缺了换一个项目就会被当成通用做法误用;取值不在字典里,说的是「按这一维筛的时候永远筛不到它」。拒绝要能教会人怎么改,不然写的人只会随手填一个值糊过去。

同一句话写第二遍会被拦住。 库里已经有一条「内容 + 项目 + 类型」都一样的生效条目时,再写一条会被拒,并指名道姓说清库里那条是谁、正确做法是取代(把旧的那条改成 状态=被取代 退场)。
理由很实在:两条一模一样的条目,读的时候不知道该信哪条,而且它们会各自演进,越往后越说不清。
项目 不同不算重复(同一个做法在两个项目下各自成立);已经退场(被取代 / 已归档)的条目 不挡新的一条。

一行错位,整库跟着串。 CSV 的列是按位置读的:哪一行少写一格,它后面每一格都会往前串一位——「时间」那格里显示的其实是「环境」,界面照样有值,只是值是错的。这种错不报错、不丢行,最难发现,所以两头都盯:

- 看得见:GET /tag-memory-api/overview 的 health.ragged 点名「第几条、差几格」,记忆页顶上直接把这条挑明——表里这些行显示的值不可信。
- 写不进:库里有错位的行时,memory_write 停下并说明(而不是照写不误,让它继续长),因为判重也是按下标比字段的,拿错位行去比「内容」比的根本不是内容。
- 有退路:把那一行补齐到与表头同样多的格子就能接着写;拿不准就用「恢复」退回上一份备份——备份是整份替换,不看列数,所以带着错位行的旧备份也一定救得回来。

注入:字典 + 记忆快照

只给字典是不够的——模型知道有本手册,但它得先想起去查;忘了查就等于没记忆。
所以每轮除了字典,还按「能不能直接用」把条目分三档摆出来:

| 档 | 谁进这一档 | 给什么 |
| 常驻 | 距离=常驻 且生效 | 全文(内容 + 环境 + 原因)——标了常驻就是「每轮必读」 |
| 本项目 / 通用 | 项目=通用,或 项目 正是当前工作区文件夹名 | 全文,受字数预算约束 |
| 其他项目 / 深藏 | 其余生效条目(含 距离=深藏,原文 条一律是深藏) | 只有一行索引:正文不进上下文 |
| — | 状态=被取代 / 已归档 | 不出现 |

第三档是结构性闸门,不是提示词劝阻:看得到的东西一定会被顺手用,所以别的项目的做法
只留一行线索(- [M-…] 在「那个项目」:…),要用就得先 memory_recall 把正文取来。
原文 条平时不进常规召回,也是同一个道理。

三档都是每轮要付的 token 账,所以字数与条数上限全部可配(见「配置」)。超了按 id 倒序截断,并在段里明说截了多少——宁可让模型知道「还有没放进来的」,也不要让它以为库里就这么多。
库为空或读不到时这一段整段不出现,退回「只注入字典」。

记忆库

一条 17 列的 CSV:

id 内容 时间 环境 原因 备注 稳定性 成熟度 距离 角色 项目 状态 关键词 工具 版本 依据 类型

加一份维度字典(schema)。默认放在 <DSH_HOME>/skills/marsma101-tag-memory;要放别处,用配置里的 libraryDir(或环境变量 DSH_TAG_MEMORY_SKILL_DIR)指过去。

只追加:新记录追加到末尾,旧条目靠 状态 退场(被取代 / 已归档),不删行。
所以库只有一个方向长——任何一版都能原样备份、原样回滚。

「记忆」页

条目页挂两处(sidebar.panellist 的按钮 + main 的主区页面),设置栏另挂一处只读字典 (见下节)。数据全走宿主的只读接口。

| 槽 | 作用 | 位置 |
| sidebar.panellist | 侧边栏按钮(图标 + 「记忆」) | order: 5,紧排在官方插件按钮(order: 0)下面 |
| main | 侧边栏按钮切过去的主区页面(条目:浏览 / 备份 / 恢复 / 合并都在这) | key: 'tag-memory';shell 的 .centerCol 是 overflow:hidden 且高度确定,面板得自己当滚动容器(.tm-panel = height:100% + overflow:auto),顶部再让出 --dsh-frame-top-clearance |
| settings.section | 设置里的「记忆」一栏:标签字典(只读) | order: 60 |

浏览能力:

- 表格视图(默认):库就是 CSV,所以一行一条、一列一个字段。列名与列序都照库自己的表头走;默认只摆十列(id 内容 时间 稳定性 成熟度 距离 角色 项目 状态 关键词),环境/原因/备注/工具/版本/依据这些长文本或常空的留在「详情」里。表头与勾选列常驻,列多时横向滚动,长文本截断(悬浮看全文)。
设置页窄,默认给卡片视图;右上角随时切换,选择记在 localStorage。
- 维度下拉:每个维度一个下拉(选项带计数、已选打 ✓),横向排一行;选一个取值叠加一条条件,同一个再选一次即取消。
- 排序 sort:id_desc(默认——最新写进去的排最上面,一眼看到新加的是什么) / file_desc(按文件顺序,后加的在前)/ time_desc / time_asc / id_asc,按自然序比较(M-20260101-002 这类也排得对)。
并列时(库里同一天写进去的条目 时间 全一样)用「在文件里的先后」当第二把尺——没有它,时间 ↓ 看着像没排。
界面只摆宿主 overview.sorts 报出来的档位,旧宿主上不会出现点了没反应的选项。
- 分页 limit + offset,页尾「加载更多」;hasMore 指示是否还有。
- 含已归档:默认排除 被取代 / 已归档;开关打开时把 状态 全集 + 留白 显式写进条件。
- 详情展开:每条可就地展开全部非空字段;正文超过 120 字先折叠。
- 多维选择:同一维度可多选(维度内 = 或,维度间 = 与);当前条件以面包屑显示,可逐个撤掉。
- 勾选带走:勾选条目(含全选本页 / 清空)→「复制 Markdown」或「导出 CSV」;每条也有单独「复制」。
- 本地记忆:条件 / 排序 / 每页数存 localStorage(带存档版本号,改了默认值才顶得动老存档),重开设置页仍在。
- 键盘:/ 聚焦搜索框,Esc 清空全部条件与关键词。

条目本身只读:写入仍由智能体经 memory_write 完成。

标签字典(设置 →「记忆」)

字典是三份合同:注入给模型的说明文本、写入时的校验判据、筛选语义的来源。它在哪、写了什么,以前只有翻开文件才知道。设置里那一栏把这本手册原样摆出来:

- 四个维度(写入时必判)各一张卡:这个维度是选值还是写句子、每个取值什么意思、落入哪一列(深度 → 成熟度)、有没有上限(距离 最多 10 条)、必填与否、什么时候判 / 怎么判 / 什么能升降它 / 受谁管。
- 辅助列、通用规则(留白怎么填、必填哪几列、筛选支持哪些算子)、 写入路由(什么情形写成原文、什么时候算「被取代」)、召回口径(排序、先报total 再取前 N 条)。
- 顶上写明字典文件的路径、改动的时辰、version。盘上的字典和进程里正在用的那份对不上时,这一栏会当场点出来并提示「重启 DSH 才会生效」——不是等注入用着旧字典、你还以为改好了。

这一栏是只读的。 改字典=改规则,落到库里每一条的判法上,所以不在这页做编辑器:要改就直接编辑那个文件(改前自己留一份),改完重启 DSH。页尾写明了这一条。

版式:主位通则

用户要看的、要点的放主位;说明、状态、设置退到次位并默认收起。 判据一句话:这东西用户「每次都要看」还是「出了事才看」?

| 位置 | 内容 |
| 标题行(右对齐) | 立即备份 / 恢复… / 合并… / 设置备份地址… 四颗按钮——要点的,跟标题同一行,不另占一张卡 |
| 标题下面一行 | 备份现状:地址 · 上次时间 · 库条数与条数差 · 份数/占用 · 盘上余量 · 「该备份了」 |
| 正文(主位) | 记忆条目:维度下拉一行 + 搜索/排序/每页/含已归档 + 勾选工具条 + 表格/卡片 |
| 页尾(默认收起) | 维护与状态:记忆文件托管明细 + 手上的备份列表(都是「出事了才看」的) |

备份 / 恢复 / 合并

- 没有默认备份地址。 地址存在 store/backup.json 的 backupDir,空就是没设。
点「立即备份」而地址为空 → 直接展开「设置备份地址」面板,不是弹一句提醒。
- 选一次就记住。 点「设置备份地址」会弹 DSH 的系统文件夹选择框——就是开新会话选工作区时
弹的同一个框(ctx.uiWorkspace.pickDirectory(),native / 内建浏览两种后端它自己适配)。
选中即落笔:面板顶上那行「当前地址」立刻跟着变,刷新 / 重开界面都还是它,直到你重选。
手填绝对路径的走右边「保存这个地址」。存地址与立刻备份是两件事——备份归标题行的「立即备份」。
- 地址只有一个落笔点:commitDir() → POST /backup/dir。选文件夹、内建浏览的「就选这个目录」、手填后保存,全汇到它;没有第二条路能偷偷改地址。
- 两级兜底:拿不到 uiWorkspace(或它没有原生选择能力)→ 退回内建目录浏览 (面包屑 + 子目录 + 「就选这个目录」);连这个也没有 → 输入框还在,手填绝对路径。
⚠️ 不自己 spawn 进程弹框——试过 PowerShell 的 FolderBrowserDialog:框开在别的窗口后面,等于弹不出来(Windows 只把前台激活权给前台进程)。用 DSH 那道门就没这问题。
- 立即备份:备 memory.csv / index.md / backup.json 三件套外加标签字典 (references/tags.schema.json,落成 schema-<戳>.json),文件名带戳 YYYYMMDD-HHMMSS;逐项校验(字节数 / SHA-256 / 解析器数的记录数 / 无 BOM)全过才更新水位;戳上已有内容不同的同名文件就往后挪一秒——备份从不覆盖已有内容;超过 keepCopies 份轮转最旧的。
字典是解释这份库的那本说明书,所以跟着库一起走:库换回旧的一版,判法也得跟着回到那一版。
- 恢复 = 整体替换(不是补缺失):先验它能读并把内容读进内存 → 强制先留档当前这一版 (失败就中止,一个字节不动)→ 写回 memory.csv + index.md(backup.json 不还原,那是设置不是记忆) → 复核记录数,对不上就报错并指出用哪份回退。
- 恢复时字典怎么办,明说、不静默:那份备份里有字典就一起换回去(换完当场生效,不必重启);与现在这份逐字节一样就说「没动」;老备份里没有字典(加字典之前留的)→ 字典原地不动,并说明「这是加字典之前的备份」。三种情形都会在恢复结果里写出来。
- 字典改过了、备份还是旧的这个状态会在备份卡上说出口(「字典改了,库还没重合」)——库与字典分家过一次,不靠人记得。
- 合并:给一份外来 memory.csv 的绝对路径 → 先预演(一个字节不落),把「新增 / 重复跳过 / id 撞号改新号 / 加不进去的 / 内容一样但标签不一样」逐条交代清楚 → 看过再「确认合并」。守的铁律是:先留档、只追加、不重写、不删任何条目、id 撞号按内容判。
预演自己就做校验,不合规的行当场挑出来(连原因一起),不会等真合并才一条条失败。
来源库没有 类型 列(迁移前的老库)时,并进来的条目按 记忆 落——不让它们留空,留空会被筛选当成「未判」而漏掉。

记忆文件托管(index.js / lib/notice.mjs)

插件的字典注入接管之后,各智能体的 MEMORY.md 里就只剩一段说明。三条规矩:

1. 只认标记:只动 之间的内容,块外一个字节不碰;文件没有标记就当用户自己写的,只追加、不重写。
2. 只追加 / 原位替换:有标记就替换块内,没有就追加到末尾;旧的手工字典块会被顺手接管替换掉。
3. 不能轻易丢:首次落笔先留 .bak-<时间戳>;插件每次挂载(以及新智能体出现时)核对标记,不在就补回。

探测哪些文件算「记忆文件」由 config.memoryFileNames 决定,默认只认 MEMORY.md(大小写不敏感)。
目录不存在、文件不存在 → 跳过。

接口

| 端点 | 方法 | 返回 |
| /tag-memory-api/overview | GET | 是否就绪、库/字典路径、维度取值与计数、记忆文件托管状态;health.ragged 点名「格子数跟表头对不上」的行 |
| /tag-memory-api/entries | GET | ?cond=<JSON>&q=<关键词>&sort=<排序>&limit=<n>&offset=<n> → 命中的条目(含 total / shown / hasMore) |
| /tag-memory-api/dict | GET | 标签字典(只读):四个维度 / 辅助列 / 通用规则 / 写入路由 / 召回口径,连同文件路径、改动时辰、version、stale(盘上与在用的是否一致)。不写文件、不改字典。 |
| /tag-memory-api/heal | POST | 立即重跑一次记忆文件自愈,返回 summary + 新状态 |
| /tag-memory-api/backup | GET | 备份现状:地址是否已设、上次时间/条数、条数差、份数/占用/盘上余量、最近几份、到点没 |
| /tag-memory-api/backup | POST | {label?} → 立即备份。没设地址时返回 code:'NO_DIR',界面据此切进「设置备份地址」状态 |
| /tag-memory-api/backup/dir | POST | {dir} → 设置备份地址(绝对路径;不存在就建;建不了/不可写就报错不落配置) |
| /tag-memory-api/restore | POST | {stamp} → 整体替换成那一版;先强制留档当前版,换完复核记录数 |
| /tag-memory-api/merge | POST | {path\|text, apply?} → 合并外来 memory.csv;不传 apply 就是预演(不落笔) |
| /tag-memory-api/picker | GET | 这台 DSH 的目录选择后端是 native(弹系统框)还是 browse(列目录) |

自检

node scripts/selftest.mjs # 235 项断言;全程沙箱(临时复制一份记忆库),不碰任何 live 数据
(「字典随备份」「写入去重 / 拒绝话术」「列数校验」三节要记忆库侧支持才跑得动,不支持会点名 SKIP 并写清是哪一份库,不假装通过)
node scripts/install.mjs # 只读校验:包结构 + profile 清单 + 预览会写哪些记忆文件

交付/测试用命令:selftest.mjs 全过 + install.mjs(check)全 PASS 即可视为可安装。
跑这两条不需要先装插件,随时可以在源码目录里复验。

在还没装记忆库的机器上,selftest.mjs 会把 A–K 整节报成 SKIP 并说清缺哪几个文件(pass=1 fail=0 skip=11,退出码 0)——那是「还没装」,不是「装坏了」,所以不报 FAIL。

安装

官方 DSH 的 bundle 安装方式(推荐):

dsh plugin --profile <profile> add <本仓库路径或 GitHub 地址>
若本机 pnpm 报 ERR_PNPM_ADDING_TO_ROOT,按它提示加 -w:
dsh plugin --profile <profile> add -w <本仓库路径或 GitHub 地址>

本包的 cordis.patch.yml 同时插入 Host 半(记忆文件托管 + 设置页 API)和 智能体半(字典/快照注入 + memory_write / memory_recall),所以按官方 bundle 方式安装后两半都会挂上,不需要再手工改 profile 补丁。

也保留了仓库内的离线安装脚本,便于冷启动、迁移和检查:

node scripts/install.mjs # 只读校验 + 预览会写哪些记忆文件
node scripts/install.mjs --apply # 安装(链接 + profile 清单 + 写记忆块)
node scripts/install.mjs --apply --seed # 顺带播初始记忆(只在该库 0 条时)
node scripts/install.mjs --uninstall # 预览要摘掉哪些(只读,一个字节不动)
node scripts/install.mjs --uninstall --apply # 卸干净:逐项摘回原样
node scripts/install.mjs --apply --dest <路径> # 先把本体拷到 <路径>,再从那儿发链接
node scripts/install.mjs --dsh-home <路径> --profile <名> # 装到别的 DSH / 别的 profile

--wire-presets 仍保留用于兼容老版安装产物;新版 bundle patch 已自带智能体半,正常情况下不需要再传。

装完需重启 DSH 才生效。所有被改的文件,脚本都会先留 .bak-<时间戳>。

本体别留在会话目录里。 链接默认直接指向源码目录。源码要是在 downloads\ 这类会话目录里,那里被清理=挂载本体凭空消失——界面上什么都不报,只是「本来有的东西不见了」。
--dest <路径> 先把本体拷过去、再从那儿发链接;只在本机、不该跟着搬的东西(.git / node_modules / 开发记录 / 运行时的库与备份)不拷。不填 --dest 则与从前完全一样。
check 会发现「本体在会话目录里」或「链接指着别处」,都会报出来;apply 时链接指着我们自己的另一份,会重指到本次要用的本体。

两半都在 bundle patch 里。 宿主半(index.js:设置页 API、记忆文件托管)和智能体半(preset.js:字典/快照注入 + 两个工具)都由 cordis.patch.yml 的 profile 平面声明接线。安装后应同时看到侧边栏「记忆」页和新会话里的 memory_write / memory_recall,不需要分别装两次。

--uninstall 摘的是安装时写下的那几处:node_modules 里的链接、profile 清单里的依赖与 bundle 条目、老版本可能留下的 profile 补丁声明、各智能体 MEMORY.md 里的说明块——摘回原样,逐条校验,有一项摘不干净就整批不落盘(宁可不卸,也不留半截)。
记忆库(store/memory.csv 与字典)不在清单里:卸插件不删记忆。
链接只解「当初是我们拉的那一条」(--dest 装的话,认的是清单里记的那个落点);别人写在别处的引用(比如某个 preset 自己的 plugins 里挂的那一行)是它的配置,我们不碰。

客户端那半(client.js)DSH 会自己热更,改完刷新浏览器即可;宿主那半(index.js)只在进程启动时装载,改了必须重启。

初始记忆(--seed)

全新装好、库里一条都没有时,智能体第一次面对的是一个空表格:它不知道该记什么,最容易先记进来的是流水账。--seed 往空库里播 3 条起步记忆:

| 条 | 内容 | 为什么是它 |
| 1 | 只记「下次还用得上」的东西(规矩、纠正、决策、实测做法);一次性的进度与闲聊不记 | 先立「什么不记」,库才不会被流水账淹掉 |
| 2 | 写「项目=通用」之前先问:换一个项目还成立吗? | 误标成通用的话会被带到每个项目里,且最像规矩、最不被怀疑 |
| 3 | 这台机器的库在哪个目录、备份地址设没设 | 换机器 / 换目录时,「库在哪」是最先要问清的事;由装机程序按实情生成 |

- 默认不写,要加 --seed;且只在该库 0 条时写。库里已经有条目就一条都不写 ——重复安装、升级插件都不会把种子塞回去(不会凭空多出你没用过的记忆,也不会和已有条目重复)。
- 3 条都走和智能体写入同一套校验(不是塞进去的例外);时间 按装机当天填。
- install.mjs(只读 check)会照实说这一次会不会写:「库是空的 → 加 --seed 会写入 3 条」或「库里已有条目 → --seed 不会写任何东西」。
- 只播已经存在的空库;库里连文件都没有时,先按记忆库的说明把库装好再跑。

安装后自验

装完并重启 DSH 之后,照这三步核对,每步都有一个不含糊的读数:

1. 宿主半(界面与记忆文件托管)——侧边栏应出现「记忆」按钮,点进去有数据。
命令行等价物:GET /tag-memory-api/overview 返回 ready: true,条数与你库里一致。
2. 智能体半(字典注入 + 两个工具)——新开一个会话再查(旧会话保留着装载那一刻的绑定,代表不了新会话):工具清单里应有 memory_write / memory_recall,系统提示词里应有「记忆字典」段与「记忆快照」段。
只有界面能用、新会话里却没有这两样 → 跑 node scripts/install.mjs 看输出;正常应显示 PASS 智能体半已接线(bundle patch、profile 平面全局)。
3. 真写一条——让智能体 memory_write 一条,回「记忆」页刷新,新条目应该在。

三条里最容易漏第 2 条:宿主半与智能体半是两码事。如果用了很老的安装产物或手工覆盖了 bundle 配置,可能只挂上宿主半——界面照样能用,但没有任何智能体拿得到字典。

卸载回滚

node scripts/install.mjs --uninstall # 先看要摘什么(只读,一个字节不动)
node scripts/install.mjs --uninstall --apply # 真摘
摘的是安装时动的那几处——node_modules 里的链接、profile 清单里的依赖与 bundle 条目、老版本可能留下的 profile 补丁声明、各智能体 MEMORY.md 里的说明块。逐条校验,有一项摘不干净就整批不落盘(宁可不卸,也不留半截)。每个文件动之前都留 .bak-<时间戳>,想回退就把 .bak 改回原名。

记忆库不在摘除清单里:卸插件不删记忆。 store/memory.csv、字典、备份目录一个字节都不碰;链接只解「当初是我们拉的那一条」,别人写在别处的引用(比如某个 preset 自己的 plugins 里挂的那行)不碰。

摘完同样要重启 DSH 才生效。

配置

index.js(profile 行):

- id: tag-memory
name: dsh-tag-memory
config:
libraryDir: 'D:\skills\marsma101-tag-memory' # 记忆库在哪;不填见下
memoryFileNames: ['MEMORY.md'] # 探测哪些文件算「记忆文件」
notice: true # 关掉则不写记忆块 migrateLegacyDict: true # 是否接管旧的手工字典块 presetRoot: 'D:\…\.agent-presets' # 找不到记忆文件时才填

preset.js(智能体半;bundle patch 已默认挂好,下面只是配置项说明):

- insert:
- id: tag-memory-preset
name: 'dsh-tag-memory/preset'
config:
agent: '某某' # 写进 角色 列;不填则依次取 preset 目录名、智能体 id libraryDir: 'D:\skills\marsma101-tag-memory' # 不填见下 snapshot: # 每轮注入「记忆快照」的预算;不填用缺省值 enabled: true # false = 退回「只注入字典」 maxPinned: 6 # 常驻条目最多放几条全文 maxBodyChars: 4000 # 全部全文条目的字数上限(常驻优先占) maxIndexLines: 40 # 索引行上限

快照的预算是每轮都要付的 token 账,按库的大小调。以一份约 150 条生效条目的库为例:

| 配置 | 注入总长 | 约合 |
| 6 / 4000 / 40(缺省) | 约 9.7k 字符 | ≈ 6.1k tokens |
| 5 / 2000 / 20 | 约 6.6k 字符 | ≈ 4.1k tokens |
| 10 / 6000 / 60 | 约 13k 字符 | ≈ 8.1k tokens |

嫌贵往下调,嫌记不住往上调;maxIndexLines: 0 就是「只放常驻,其余全靠 memory_recall」。

记忆库在哪,按这个次序找:libraryDir → 环境变量 DSH_TAG_MEMORY_SKILL_DIR → <DSH_HOME>/skills/marsma101-tag-memory。不填就是后面两条,与之前的行为逐字一致。
两半都挂了时,谁先装载谁说了算——两处都写、且写成不一样,是配置错误。

许可证

Apache-2.0
预览分享卡片长什么样 ↓marsma-101/dsh-tag-memory
分享 · DeepSeek Harness Hub
marsma-101/dsh-tag-memory

把智能体记忆存成可查可导的 CSV 表格

这是你的仓库?认领条目

回到顶部