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

工作区记忆 miseryrua/dsh-wb-memory

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

用 Markdown 为 Agent 保存跨会话长期记忆

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

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

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

状态速览

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

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

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

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

读写/删除本地文件发起外部网络请求
查看扫描证据(共 104 处,展示前 56 条)
  • 读取本地文件
    lib/index.js:94
    return existsSync(legacy) ? legacy : null
  • 读取本地文件
    lib/index.js:260
    return JSON.parse(readFileSync(p, 'utf8').replace(/^ FEFF/, ''))
  • 写入本地文件
    lib/index.js:271
    mkdirSync(MEM_DIR(), { recursive: true })
  • 写入本地文件
    lib/index.js:272
    writeFileSync(CONFIG_FILE(), JSON.stringify(cfg, null, 2), 'utf8')
  • 写入本地文件
    lib/index.js:276
    mkdirSync(MEM_DIR(), { recursive: true })
  • 读取本地文件
    lib/index.js:308
    let s = readFileSync(p, 'utf8')
  • 读取本地文件
    lib/index.js:319
    return readFileSync(p, 'utf8')
  • 读取本地文件
    lib/index.js:354
    const s = readFileSync(p, 'utf8')
  • 读取本地文件
    lib/index.js:472
    return readdirSync(workspaceRoot())
  • 写入本地文件
    lib/index.js:898
    mkdirSync(dir, { recursive: true })
  • 写入本地文件
    lib/index.js:899
    appendFileSync(logPath, (existing ? (existing.endsWith('\n') ? '' : '\n') : `# ${day} 日志(含自动流水)\n`) + line + '\n', 'utf8')
  • 写入本地文件
    lib/index.js:1225
    mkdirSync(dir, { recursive: true })
  • 发起网络请求
    lib/client.js:102
    const r = await readJson(await fetch('/api/wb-memory/recall-eval?candidateK=' + candidateK));
  • 发起网络请求
    lib/client.js:156
    const d = await fetch(api + '?workspace=' + encodeURIComponent(workspace) + '&state=' + state + '&offset=' + offset, { cache: 'no-store' }).then(readJ
  • 发起网络请求
    lib/client.js:165
    const d = await fetch(api + '?workspace=' + encodeURIComponent(workspace) + '&id=' + encodeURIComponent(id), { cache: 'no-store' }).then(readJson);
  • 发起网络请求
    lib/client.js:172
    const d = await fetch(api, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ ...body, workspace }) }).then(rea
  • 发起网络请求
    lib/client.js:279
    fetch("/api/wb-memory/files" + q, { cache: "no-store" })
  • 发起网络请求
    lib/client.js:288
    fetch("/api/wb-memory/status" + q, { cache: "no-store" })
  • 读取本地文件
    lib/governance.js:52
    ents = readdirSync(memoryDir, { withFileTypes: true })
  • 写入本地文件
    lib/governance.js:56
    mkdirSync(arch, { recursive: true })
  • 写入本地文件
    lib/governance.js:66
    renameSync(src, dst)
  • 读取本地文件
    lib/governance.js:81
    content = readFileSync(memPath, 'utf8')
  • 写入本地文件
    lib/governance.js:88
    mkdirSync(arch, { recursive: true })
  • 写入本地文件
    lib/governance.js:92
    writeFileSync(bakPath, content, 'utf8')
  • 读取本地文件
    lib/governance.js:107
    if (!existsSync(src)) return { ok: false, error: 'not found' }
  • 写入本地文件
    lib/governance.js:109
    mkdirSync(trash, { recursive: true })
  • 写入本地文件
    lib/governance.js:112
    mkdirSync(dstDir, { recursive: true })
  • 读取本地文件
    lib/governance.js:129
    ents = readdirSync(trash, { withFileTypes: true })
  • 删除本地文件递归
    lib/governance.js:142
    rmSync(join(trash, e.name), { recursive: true, force: true })
  • 读取本地文件
    lib/governance.js:161
    ents = readdirSync(arch, { withFileTypes: true })
  • 删除本地文件
    lib/governance.js:177
    rmSync(join(arch, e.name), { force: true })
  • 读取本地文件
    lib/governance.js:210
    const md = readFileSync(join(memoryDir, 'MEMORY.md'), 'utf8')
  • 读取本地文件
    lib/summary.js:116
    const raw = readFileSync(logPath, 'utf8')
  • 读取本地文件
    lib/summary.js:161
    existing = readFileSync(logPath, 'utf8')
  • 写入本地文件
    lib/summary.js:165
    mkdirSync(dirname(logPath), { recursive: true })
  • 写入本地文件
    lib/distill-cursor.js:7
    mkdirSync(dirname(path), { recursive: true })
  • 写入本地文件
    lib/distill-cursor.js:9
    writeFileSync(tmp, text, 'utf8')
  • 写入本地文件
    lib/distill-cursor.js:10
    renameSync(tmp, path)
  • 读取本地文件
    lib/heat.js:7
    const value = JSON.parse(readFileSync(join(dir, 'heat.json'), 'utf8'))
  • 读取本地文件
    lib/distill-diagnostics.js:48
    if (statSync(file).size > 262144) throw new Error('oversize')
  • 读取本地文件
    lib/distill-diagnostics.js:49
    const d = JSON.parse(readFileSync(file, 'utf8').replace(/^ FEFF/, ''))
  • 读取本地文件
    lib/distill-schedule.js:11
    if (statSync(file).size > 1048576) throw new Error('oversize')
  • 读取本地文件
    lib/distill-schedule.js:12
    const d = JSON.parse(readFileSync(file, 'utf8').replace(/^ FEFF/, ''))
  • 读取本地文件
    lib/summary-store.js:45
    const raw = readFileSync(pendingStorePath(dir), 'utf8')
  • 写入本地文件
    lib/summary-store.js:59
    mkdirSync(dir, { recursive: true })
  • 写入本地文件
    lib/summary-store.js:62
    writeFileSync(tmp, JSON.stringify(store, null, 2), 'utf8')
  • 写入本地文件
    lib/summary-store.js:63
    renameSync(tmp, p)
  • 删除本地文件
    lib/summary-store.js:65
    try { rmSync(tmp, { force: true }) } catch {}
  • 读取本地文件
    lib/summary-store.js:123
    return readFileSync(logPath, 'utf8').split('\n').some(line => line.startsWith('## 会话小结 ') && line.includes(batchMarker(batchId)))
  • 写入本地文件
    import-wb-memory.mjs:14
    await mkdir(join(MEM_DIR, 'projects'), { recursive: true })
  • 读取本地文件
    import-wb-memory.mjs:29
    const projects = await readdir(wbRoot)
  • 读取本地文件
    import-wb-memory.mjs:33
    const s = await stat(mem)
  • 写入本地文件
    import-wb-memory.mjs:39
    await mkdir(dest, { recursive: true })
  • 读取本地文件
    import-wb-memory.mjs:51
    const files = await readdir(mdir)
  • 写入本地文件
    import-wb-memory.mjs:53
    await mkdir(dest, { recursive: true })
  • 写入本地文件
    import-wb-memory.mjs:66
    await mkdir(MEM_DIR, { recursive: true })
扫描于 2026/11/4(0 天前) · 范围 入口闭包 + 发布文件清单 · 来源 GitHub 仓库

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

给这个插件打分还没有人投票↓
用户评分
还没有人投票,来当第一个

功能亮点

  • 跨会话长期记忆
  • Markdown真值源

给 DSH 里的 Agent 一份跨会话长期记忆:真值源是工作区里的纯 Markdown,无向量库、无外部服务、零运行时依赖;每回合按预算分层注入 systemPrompt,后台自动流水 → 提炼 → 整理 → 治理。

README

由 DeepSeek 最新模型翻译生成
dsh-wb-memory 给 DSH 里的 Agent 一份跨会话长期记忆。 记忆就是工作区里的 Markdown 文件——没有向量库、没有外部服务、没有运行时依赖;每回合按 token 预算分层注入到 systemPrompt。 - 看得见、能 grep、能进 git:真值源是 /.workbuddy/memory/ 下的普通 Markdown,Agent 自己也能直接读写 - 零运行时依赖:只用 Node 内置模块,不引第三方包 - 后台自己维护:流水、提炼、整理、治理四个流程自动跑,不需要你手工整理记忆 快速开始 在你的 profile 里做两件事:装依赖、把插件名加进 bundle 列表。 { "dependencies": { "dsh-wb-memory": "link:C:/Users//.dsh/plugins-dev/dsh-wb-memory" }, "dsh": { "profile": { "bundles": ["@deepseek-ai/dsh-base", "@deepseek-ai/dsh-web-app", "dsh-wb-memory"] } } } 然后重启 DSH 宿主。
展开完整文档(README 剩余 1 节)
dsh-wb-memory

给 DSH 里的 Agent 一份跨会话长期记忆。 记忆就是工作区里的 Markdown 文件——没有向量库、没有外部服务、没有运行时依赖;每回合按 token 预算分层注入到 systemPrompt。

- 看得见、能 grep、能进 git:真值源是 /.workbuddy/memory/ 下的普通 Markdown,Agent 自己也能直接读写
- 零运行时依赖:只用 Node 内置模块,不引第三方包
- 后台自己维护:流水、提炼、整理、治理四个流程自动跑,不需要你手工整理记忆

快速开始

在你的 profile 里做两件事:装依赖、把插件名加进 bundle 列表。

{
"dependencies": {
"dsh-wb-memory": "link:C:/Users//.dsh/plugins-dev/dsh-wb-memory"
},
"dsh": {
"profile": {
"bundles": ["@deepseek-ai/dsh-base", "@deepseek-ai/dsh-web-app", "dsh-wb-memory"]
}
}
}

然后重启 DSH 宿主。插件通过自己的 cordis.patch.yml(由 package.json 的 dsh.bundle.patch 字段声明)插进 profile 的层栈,不用手改 profile 的其它配置。

启用后确认两件事:

1. 设置面板出现 wb-memory 页签,总闸 enabled 为开;
2. 面板里选好「当前激活工作区」——会话 cwd 落在 // 下会自动匹配,不落在时用面板这个值兜底。

记忆目录不需要预先创建,第一次写入时会自己建。已在 DSH 0.1.7-rc.2 + Node 24(Windows)上日常使用。

它会往上下文里塞什么

注入块同步拼装,结构固定、顺序固定:

WorkBuddy 记忆(由 dsh-wb-memory 插件注入,启用时生效)全局用户记忆(USER.md) ← L0 全局画像当前工作区:程序开发(已自动匹配)置顶记忆(PINNED.md,用户明确钉住,长期有效) ← N3 优先级最高长期记忆 / 相关条目(按当前任务召回 top-5) ← L2 召回;注入时机拿不到长期记忆 / 摘要(完整内容请按需读取 MEMORY.md) 用户消息时退化为 L1 今日日志(2026-09-26.md) ← L3 保尾截断近 6 日回顾(2026-09-20 ~ 2026-09-25) ← N1 零 LLM 可用工作区记忆纪律 ← 读/何时写/不写/写到哪/冲突消解

长期记忆的正文不全量注入:只给条目标题和首行,细节留给 Agent 用文件工具按需打开,或用 memory_search 工具按关键词召回。

| 层 | 内容 | 预算(config.json) | 截断方式 |
| L0 | 插件目录 USER.md 全局画像 | profileMaxChars = 2000 | 保头 |
| N3 | 工作区 PINNED.md 置顶 | pinnedMaxChars = 1000 | 保头并标注 |
| L1 | MEMORY.md 条目标题 + 首行 | summaryMaxChars = 1500 | 按条目预算 |
| L2 | MEMORY.md 相关条目 top-K | relevantTopK × entryMaxChars | 每条 800 |
| L3 | 当日日志 | logMaxChars = 1500 | 保尾(最新记录在文件尾部) |
| N1 | 近 weekWindowDays 日回顾 | weekMaxChars = 1200 | 每日子预算 400 |

记忆存在哪

真值源是各工作区自己的目录,根目录可用环境变量 DSH_WORKSPACE_ROOT 覆盖(默认 D:/datas/Deepseek Harness):

/.workbuddy/memory/
├── MEMORY.md # 长期记忆:## 标题 分段,可带 frontmatter 注释行 ├── YYYY-MM-DD.md # 每日日志:人工收尾记录 + 自动流水 ├── PINNED.md # 置顶记忆(不参与整理与治理) ├── archive/ # 过期日志归档 + MEMORY.md 容量备份 └── .trash// # 删除的回收站

插件目录 ~/.dsh/plugins-dev/dsh-wb-memory/ ├── USER.md # 全局用户画像(跨工作区只读;旧版留 USER.md.bak) ├── USER-CANDIDATES.md # 画像候选(Agent 按行追加,巡检自动评估并入) ├── config.json # 配置(面板可改,热生效) └── distilled.json # 幂等标记(工作区 + 日期 → 已提炼)

删文件走 rename 进 .trash/,所以误删可回滚;真删只发生在 .trash/ 与 archive/ 里已过保留期的副本上。

自动化流水线

- auto-digest(零 LLM):订阅 session/event,每轮成功结束追加一条 HH:MM【自动】用户要求 → 结论 进当日日志。助手侧头尾各取——结论常在尾部,只取头部会把它丢掉。带去重与每日上限 digestMaxPerDay。
- auto-distill(每小时,LLM):把昨天及更早的日志提炼成带 frontmatter 的条目追加进 MEMORY.md,按(工作区,日期)幂等,失败自动重试,顺带扫 archive/。提炼 prompt 强制三分类 + 「坑」条目四段式(现象 → 根因 → 绕过办法 → 影响边界)+ 可信边界标注。
- auto-compact(LLM):MEMORY.md 超 memoryCharThreshold 时整理。四道护栏:必须变短、压回阈值内、条目数不少于一半、每天每工作区至多一次;落盘前先备份到 archive/。目标字符数按上次实测超幅自适应(threshold/超幅×0.95,限幅 [0.3, 0.8]×threshold)——固定系数和护栏之间没余量就会拒收死循环。
- 会话小结(LLM):会话空闲 summaryIdleMs 后生成小结写进当日日志。四道闸:用户文本 ≥ summaryMinUserChars、当天 ≤ summaryMaxPerDay 次、同会话只小结一次、LLM 路由就绪。
- 画像候选:Agent 不直接改 USER.md。跨项目通用的新发现按行追加到 USER-CANDIDATES.md,巡检调 LLM 评估并入(先备份,护栏:非空、≤ 6000 字符、不得缩水到旧版 30% 以下),成功后清空候选并留痕 USER-CANDIDATES.log。

写入路径与护栏

MEMORY.md 有三个写者,没有任何握手:

| 写者 | 时机 | 写法 |
| 会话流水 | 每轮对话后 | appendFileSync 追加当日日志 |
| auto-distill | 每小时 | 读日志 → LLM → 追加条目进 MEMORY.md |
| auto-compact | 超阈值 | 读 MEMORY.md → LLM 整体重写 |

distill 的读-改-写落在同一个 tick 里(中间没有 await),本来就没有窗口。compact 和 USER.md 画像候选却有真窗口:从读原文到写盘之间夹着完整的 LLM 调用,最坏 distillTimeoutMs × 2 轮 ≈ 6 分钟。这期间 Agent 若按纪律往 MEMORY.md 追加了条目,整体重写会把它静默抹掉。

v9.0 因此加了 guardedWrite(p, content, expectedRaw):写盘前重读文件,与「送进 LLM 的那份原文」按内容逐字节比对,不一致就放弃本次写入。

[wb-memory][compact] 程序开发: 写入前发现 MEMORY.md 已被改动(285 -> 306 字符),本次放弃,下一轮重做

几个刻意的选择:

- 不比 size/mtime,也不写文件内版本号:比的是内容本身(同长度不同内容也拦得住),且期望值必须取自「读原文那一刻」并由调用方传进来——护栏自己在写前才 stat/重读的话,比较会恒等成立(TOCTOU),等于没护栏。
- 冲突不报错、不覆盖:compact 冲突时提前返回,不写「今天已尝试」的日锁,下一轮巡检自动重试;USER.md 冲突时候选条目保留,同样等下轮。
- 不改 HTTP 写入路径:POST /wb-memory/file 是面板里人工确认过的覆盖(先弹「文件 X 已存在,覆盖写入?」),属于显式动作,加护栏反而破坏既有流程。

治理

面板「治理」按钮或 POST /wb-memory/gc 触发;gcEnabled=false 时整段跳过。

TTL 归档过期日志(logRetentionDays)、容量检测(memoryCharThreshold)、近重复检测(dupThreshold,Jaccard + 重叠率双指标,只报告不合并)、回收站清理(trashRetentionDays)、归档清理(archiveRetentionDays)。两个保留天数设为 ≤ 0 即禁用对应清理。

工具与 HTTP 路由

memory_search 原生工具(开关 memorySearchTool,改动需重载插件):参数 q 关键词,可选 workspace / date_from / date_to,同时检索长期记忆条目与近 searchLogDays 天的日志,比让模型读全文省 token。

webServer 路由(exact):

| 路由 | 方法 | 用途 |
| /wb-memory/config | GET/POST | 读/改配置(POST 需同源,字段白名单) |
| /wb-memory/status | GET | 运行状态:占用、条目数、今日流水、待提炼、上次整理、回收站与归档计数(只读) |
| /wb-memory/workspaces | GET | 工作区列表 |
| /wb-memory/files | GET | 激活工作区的记忆文件列表(含 size) |
| /wb-memory/file | GET/POST/DELETE | 读/写/回收单个文件(basename 防穿越) |
| /wb-memory/search | GET | 关键词召回(?q=&workspace=) |
| /wb-memory/gc | POST | 手动治理(会执行归档与真删,别拿它轮询) |
| /wb-memory/distill | GET | 触发全工作区巡检(?wait=1 同步等结果) |
| /wb-memory/models | GET | 可用 LLM 路由(面板模型下拉用) |
| /wb-memory/debug-cwd | GET | 调试:最近会话 cwd 与工作区解析 |

术语表

| 术语 | 配置 / 代码 | 含义 |
| 流水 | digest / autoDigest | 每轮对话自动追加的简短记录(做了什么 → 结论) |
| 提炼 | distill / autoDistill | 历史日志 → MEMORY.md 条目(LLM 分类抽取) |
| 整理 | compact(含于提炼巡逻) | MEMORY.md 超阈值时 LLM 压缩 |
| 治理 | gc / gcEnabled | TTL 归档 + 近重复检测 + 回收站与归档超期清理 |
| 画像 | USER.md | 全局用户画像(只读,由候选机制统一并入) |
| 逻辑日 | dayBoundaryHour | 分界时刻默认 4 点:凌晨 3 点的会话算「昨天」 |

配置项

- 注入预算:profileMaxChars summaryMaxChars logMaxChars entryMaxChars relevantTopK pinnedMaxChars weekWindowDays weekMaxChars weekSummaryDailyChars
- 流水与小结:autoDigest digestMaxPerDay digestUserChars digestAssistantChars sessionSummary summaryIdleMs summaryMinUserChars summaryMaxPerDay
- 提炼与整理:autoDistill distillProvider distillModel compactProvider compactModel compactMaxTokens compactRetryFeedback distillMaxInputChars distillMaxTokens distillTimeoutMs distillIntervalMs distillStartDelayMs
- 检索:memorySearchTool searchLogDays searchLogTopK
- 治理:gcEnabled logRetentionDays memoryCharThreshold dupThreshold trashRetentionDays archiveRetentionDays
- 其它:enabled activeProject dayBoundaryHour

缺字段自动用默认值补齐;坏 JSON 备份为 config.json.bak 后重建;带 BOM 的合法 JSON 正常加载。BOM 这一条是踩过坑的:PowerShell 5.1 的 Set-Content -Encoding UTF8 必写 BOM,而 Node 的 JSON.parse 不接受——历史事故是 distilled.json 被写进 BOM 让幂等标记全丢,全部历史日志被当作未提炼重跑,各工作区 MEMORY.md 追加了大量重复条目。

已知边界

展开

- 召回靠词面不靠语义。没有向量库,L2 召回与 memory_search 都是 Jaccard + 重叠率这类词面匹配,换个说法问同一个问题可能召不回。省 token 与零依赖换来的就是这个。
- PINNED.md 是唯一能无限增长的文件。它按约定不参与整理也不参与治理(用户明确钉住的内容不该被自动改写),注入永远封顶 pinnedMaxChars;文件超预算时注入会标注截断,需要人工精简。
- 整理质量取决于模型指令遵循。实测 compact 是「恒超目标」的重灾区:deepseek-v4-flash 连续 3 天无视「条目数不少于一半」被拒收;glm-5.3 尊重条目数但输出恒超目标 27%~47%(target 6400 → 实际 9380、8000 → 10180、9600 → 12745)。教训是压缩类任务配指令遵循强的模型,靠自适应 target 兜底,别跟阈值玩军备竞赛。
- 改 memorySearchTool 需要重载插件,其余配置热生效。
- import-wb-memory.mjs 是一次性遗留脚本,指向旧版 ~/.dsh/wb-memory 存储,当前版本不使用它。

开发

node --check lib/index.js # 语法检查
node --check lib/client.js
node test/smoke.mjs # 冒烟测试:108 项

冒烟测试覆盖 retrieve 召回 / governance 治理 / BOM 容错 / compact 自适应目标 / 头尾截取 / 注入装配 / 写入护栏(含 e2e:用假 llm 在窗口内真实写盘,验证冲突被拦下且并发内容留存)。

License

MIT
预览分享卡片长什么样 ↓miseryrua/dsh-wb-memory
分享 · DeepSeek Harness Hub
miseryrua/dsh-wb-memory

用 Markdown 为 Agent 保存跨会话长期记忆

这是你的仓库?认领条目

回到顶部