dsh-web
zhu1090093659/dsh-webDeepSeek Harness(DSH)Web 插件聚合生态包 · 一切皆插件,创意工坊分发(dsh-market.com)
DSH 本地记忆插件:BM25-lite 历史召回 + 记忆使用协议 + 结构化固化 + 情感状态。全部落本地 Markdown/JSONL,无数据库、无云端。
给 DeepSeek Harness(DSH) 里的持久化 AI 角色加一层记忆与状态:常驻记忆协议、按需历史召回、结构化记忆固化、两标量情感状态。
面向的场景很简单:模型每次重启都是同一份权重,跨会话的“记性”只能靠外部文件。这个插件把那份档案变成能找回来、能追溯、且不会压过人设的一层。
大模型像一块孤立的前额叶:工作记忆短、无状态、换个会话就忘。这个项目按仿生学思路给它补了海马体(档案 + 召回 + 固化)和一点点杏仁核(情感状态)。
但它不承诺意识。用中文房间的话说,我们只是给房间加了一个档案柜——回答更连贯了,符号仍然没有接地。所以这里的定位是弱人格同一性:不承诺内在体验,只保证行为上的连续、可追溯、一致。
| 层 | 干什么 |
|---|---|
| P0a 记忆使用协议 | 用 systemPrompt.section 注入 5 条规则:历史参考不是本轮发言、可能过期、要能追溯、以新事实为准、不许编造。刻意不重复 AGENTS.md 的人格段,避免权威倒置。 |
| P0b 按需召回 | agent/pre-step 且 step <= 1(首轮与 step 未定义时),用中文 2-gram + BM25-lite + 覆盖率阈值,从档案召回 top 3,前置注入一条带 source.kind='memory-recall' 的消息。150 ms 目标超时,失败静默。 |
| P0c 常驻记忆导航 | 另一个 systemPrompt.section(order 960),把「档案有哪些主题、最近几条记忆的一行摘要 + 关键词」压到 600 字以内注入。没有记忆时不注入;模型需要细节时按关键词自己回查,不靠召回碰运气。 |
| P0d 后台总结 | setInterval(默认 10 分钟,unref())检查对话文件:空闲超过 30 分钟且有新增小节时,按「标题 + 首句」追加到 ~/.dsh/autocapture/summaries.jsonl。纯规则、不调 LLM,内容 hash + 小节 key 双重水位保证幂等。 |
| P1 结构化固化 | consolidate.mjs 把对话小节转成 structured.jsonl:稳定 ID、source_id、逐字校验的 quote、幂等水位。 |
| P2 情感状态 | valence / arousal 两标量 + 规则分类 + 指数衰减 + 事件去重,只影响语气,不碰调度与安全规则。 |
| P3 写入脱敏 | sanitize.mjs 在自动捕获与摘要落盘前过滤 sk- / gh*_ / github_pat_ / AKIA / Bearer / password= / token= / api_key= 等常见凭证,命中替换成 [REDACTED]。纯本地正则,可在面板关掉。 |
这个插件刻意不做数据库、不做向量库、不调任何外部服务,记忆就是一堆普通文件:
memory/
├── 身份.md # 我是谁(只读,面板不改)
├── 日记.md # 按条目追加
├── 生长/
│ ├── 状态.md # 分节正文
│ └── 生长记录.md # 一行一条
└── 记忆/
├── 事件流.jsonl # 只追加(面板只读)
└── structured.jsonl # P1 固化产物:id / source_id / quote / status
memory/:~/.dsh/autocapture/
├── captures.jsonl # 自动捕获(已有)
├── summaries.jsonl # 后台摘要:id / title / summary / keywords / source_id
└── summaries.jsonl.watermark.json # 内容 hash + 已处理小节 key,保证幂等
structured.jsonl,每条都带 source_id 和一段逐字校验过的 quote——在原文里搜不到就丢弃。state.json,两个标量 + 触发原因。dsh plugin --profile <你的 profile> add file:<这个目录>
或在 DSH Desktop 的「插件管理」里装本地目录。装完重启 DSH。
cordis.patch.yml(整行替换,没写的字段走默认):
- insert:
- id: dsh-memory-core
name: dsh-memory-core
config:
memoryRoot: "D:\\path\\to\\memory" # 档案根(身份/日记/生长/记忆)
dialogueFile: "D:\\path\\对话记录.md" # 逐轮对话日志
logFile: "D:\\path\\recall.log" # 召回日志
stateFile: "D:\\path\\affect\\state.json"
topK: 3
timeoutMs: 150
minCoverage: 0.2
minRatio: 0.3
enableStable: true
enableAutoCapture: true
enableMemoryNav: true
summaryMaxChars: 600
enableSummary: true
summarizeIntervalMinutes: 10
summarizeIdleMinutes: 30
enableSanitize: true
DSH 设置页左侧多一节 「记忆核心」(客户端 slot settings.section,id: memory-core,order: 58)。面板只做两件事:读当前生效值、把改动存回配置。
| 参数 | 类型 | 默认 | 范围 | 说明 |
|---|---|---|---|---|
memoryRoot | 路径 | D:\dsh-workspace\memory | 非空 | 日记 / 生长记录所在根目录(只读) |
dialogueFile | 路径 | D:\dsh-workspace\对话记录.md | 非空 | 当语料读的对话记录 |
topK | 数字 | 3 | 1–10 | 每次注入几条 |
timeoutMs | 数字 | 150 | 50–1000 | 召回超时(毫秒) |
minCoverage | 数字 | 0.2 | 0–1 | 查询 2-gram 覆盖率下限(0 = 不设门槛) |
minMatched | 数字 | 2 | 1–5 | 最少命中片段数 |
halfLifeDays | 数字 | 30 | 1–365 | 权重半衰期(天) |
dailyQuota | 数字 | 10 | 0–100 | 自动捕获每日配额(0 = 不写) |
enableStable | 开关 | 开 | — | 是否注入常驻「记忆使用协议」 |
enableAutoCapture | 开关 | 开 | — | 是否开启自动捕获 |
enableMemoryNav | 开关 | 开 | — | 是否注入常驻「记忆导航」(无记忆时不注入) |
summaryMaxChars | 数字 | 600 | 80–2000 | 导航总长度上限(字),超出按新鲜度截断 |
enableSummary | 开关 | 开 | — | 是否跑后台摘要 |
summarizeIntervalMinutes | 数字 | 10 | 1–1440 | 后台检查间隔(分钟) |
summarizeIdleMinutes | 数字 | 30 | 1–1440 | 对话文件空闲多久才总结(分钟) |
enableSanitize | 开关 | 开 | — | 自动捕获 / 摘要写入前是否脱敏 |
保存链路:客户端 POST /dsh-memory-core/config/save → config.mjs 严格校验(越界 / 类型错直接点名,不偷偷改值)→ ctx.configEditor.edit() 写进 profile 的 patch 层(宿主自己的配置机制:保留 YAML 注释与 !!js,重启后仍在)→ 同时就地更新运行中的配置,下一轮召回立刻按新值走。
config.mjs 一张表里,客户端从 GET /dsh-memory-core/config 拿表渲染,不重复写死中文标签。configEditor(如 headless)时:改动当次生效,面板会明说「存不进配置文件」。cordis.patch.yml 里改。记忆导航(summary.mjs 的 buildNav)是常驻的,但只给“索引”不给全文:
memoryRoot 下的 .md、dialogueFile、autocaptureStore,按文件 mtime / 标题日期 / created_at 排新鲜度;summaryMaxChars(默认 600)硬限制,放不下的旧条目先丢;一条记忆都没有时返回空串,systemPrompt.section 不会注入空块;status.mjs 的状态判定:superseded / archived / draft 的对话小节不会出现在导航里。否则一条被淘汰的旧决策会从召回那个口子消失,又从导航这个口子回到上下文。日记与自动捕获没有结构化状态,不受此过滤影响。后台总结(summary.mjs 的 summarizeDialogue)默认每 10 分钟检查一次:
summarizeIdleMinutes(默认 30 分钟)→ 认为还在聊,跳过;## / ### / #### 切小节,取「标题 + 首句(截断 120 字)」,追加到 ~/.dsh/autocapture/summaries.jsonl;定时器用的是 unref() 的 setInterval,不会阻止 DSH 退出;面板改开关或间隔会当场重排定时器。
写入脱敏(sanitize.mjs)在两条写路径生效:autocapture.mjs 的 autoCapture(redact 参数)和 summarizeDialogue 的摘要记录。覆盖 sk-、gho_/ghp_/github_pat_、AKIA、Bearer、password=/token=/api_key=;已替换的位置用负向前瞻跳过,所以重复调用不会二次计数。默认开启,enableSanitize: false 可关。
npm test # 全量闸门,任何机器上都应该是绿的
npm run eval # 单独跑召回评测,需要私有档案(DSH_MEMORY_ROOT / DSH_MEMORY_DIALOGUE)
npm run consolidate # 跑一次固化(幂等)
固化脚本也接受环境变量,换机器不用改源码:`DSH_MEMORY_DIALOGUE`(对话文件)、`DSH_MEMORY_ROOT`(档案根)、`DSH_MEMORY_STRUCTURED` / `DSH_MEMORY_WATERMARK`(覆盖输出与水位)。都不设时从当前目录往上找「对话记录.md」,找不到直接报错,不会静默当成空对话跑完。
selftest 用 mock 宿主验证:section 注册、注入前缀、不相关查询不注入、情感状态变化、日志写入。
test/sanitize.test.mjs 覆盖 9 类密钥模式、幂等、大小写与普通文本不误伤。
test/summary.test.mjs 覆盖空档案不注入、导航内容/长度上限/截断、摘要抽取、水位幂等、追加只摘新增、空闲判定、写入脱敏与开关。
test/panel-routes.test.mjs 用假 ctx / webServer / configEditor 跑一遍读写路由:校验拒绝、落盘调用、0 值保留、headless 降级。
minScore 地板拦掉。主题分散的档案不受影响。recall(cfg, query, deadline) 把 deadline 贯穿冷启动加载、索引构建和候选扫描;循环每 64 步检查一次,超时就熔断返回已算出的部分结果,并废弃没建完的索引缓存。它不能抢占正在执行的一次同步 readFileSync——Node.js 单线程上,同步 I/O 一旦开始就没法从外部打断。所以准确的说法是「计算阶段协作式检查 + 主动熔断」,不是「整条链严格不超过 150ms」。真遇到超大单文件或慢磁盘,单次同步读取本身就可能吃掉整个预算。整条链路是同步内联检查,用 Promise.race 包裹是伪超时、救不回时间。fact 取小节标题)。status / supersedes 已经真正参与召回——active 以外的记录不进候选池,且 supersedes 关系优先于状态字段。但系统不会自己判断两条记忆是否冲突:新决策落盘后,仍要手动把旧记录标成 superseded,或在它上面填 supersedes。LLM 事实提取与自动冲突识别没做。source_id 是 dialogue:行号,对话记录被编辑后行号会整体漂移。状态表因此同时按内容签名(完整内容的 sha1 前缀,没有截断)对齐,行号对不上时按签名兜底。改内容(不只是挪位置)会让签名也变,那条记忆回到未标记状态——这是有意的:内容变了就是另一条记忆。详细推导见仓库外的两份文档(架构方案 v0.4 / 实施方案 v1.0)。几条关键决定:
systemPrompt:不接 selfwake 的调度闸门,避免两套系统抢方向盘。MIT