Standing instructions edited in the DSH settings UI and injected into the system prompt of every subsequent turn.
Install
# from npm (prebuilt)
dsh plugin --profile web add dsh-instruction-memory
# from GitHub (first run asks for allowBuilds approval — follow the hint, retry)
dsh plugin --profile web add github:HERO476/dsh-instruction-memory
Any plugin you install runs third-party code with your own permissions — it can read your files, use your credentials, and reach the network, and tool approvals don’t sandbox it. GitHub-sourced plugins also run build scripts at install time — pnpm blocks those until you allow them, so an install can stop with ERR_PNPM_GIT_DEP_PREPARE_NOT_ALLOWED or ERR_PNPM_IGNORED_BUILDS; dsh prints the exact key to add under allowBuilds in your profile’s pnpm-workspace.yaml, and the install works on the next run. Allowing a build is a trust decision: only install sources you trust, and pin a commit (github:owner/repo#sha).
README
This plugin publishes its README in Chinese only.
在 DSH 设置界面中维护长期指令记忆:保存后自动注入此后每一轮对话的系统提示词。

想先看看界面?在 GitHub 仓库中打开
docs/demo.html,无需安装 DSH,浏览器直接体验真实设置页(内置演示数据)。
安装
dsh plugin --profile web add dsh-instruction-memory
宿主版本必须落在兼容性声明的范围内。范围之外宿主会拒绝安装/启动(1.0.14 起,
因为 peerDependencies 已声明);确需强行安装可用 dsh plugin allow-version 授予该精确版本的
豁免,风险自负。
装完重启 DSH,设置页会出现「指令记忆」入口。卸载:
dsh plugin --profile web remove dsh-instruction-memory
只由用户本人维护
这份记忆的内容只属于用户。 本插件刻意不注册任何面向模型的工具——模型没有任何 途径自行添加、修改或删除记忆条目。写入入口只有设置页(以及用户本人直接编辑存储文件), 因此不会出现 AI 悄悄给用户"记下"指令的情况。
条目内容由用户输入并被原样注入,本插件不对其做语义判断或自动生成。
组成
| 文件 | 作用 |
|---|---|
lib/index.js |
Host 半:读取/保存记忆,注册 systemPrompt 段落,暴露同源 JSON 路由 |
lib/client.js |
Client 半:在 settings.section 注册「指令记忆」设置页(手写模块,无需构建) |
cordis.patch.yml |
bundle patch:把 Host 行插入宿主组合 |
install.mjs |
开发安装脚本:从本仓库检出安装时,把包链接进 web profile 的 node_modules(junction)、注册 dsh.profile.bundles,并在 profile dependencies 里声明 link:<本仓库路径>(缺这一步插件能跑,但插件市场的「已安装」列表看不到它——市场只读 dependencies);--remove 三步完整回滚,改 profile 前自动备份 |
contract-test.mjs |
契约测试:驱动真实路由,验证两半之间的响应格式(含配置集、版本历史、源码模式、备份位置) |
ui-test.mjs |
设置页行为测试:用一个 ~60 行的 React 垫片真实渲染面板、点按钮/填输入框,再断言注入内容随之变化(不出图、不依赖浏览器) |
verify.mjs |
装载验证:按 DSH 的方式加载两半,并逐项断言 memory.schema.json 与代码中的上限一致 |
smoke-test.mjs |
注入渲染逻辑的单元测试(含「只渲染激活配置集」的隔离断言与 cwd 红线) |
host-range-test.mjs |
宿主版本范围契约测试:固定矩阵 × 两种 semver 判定模式,并把 peerDependencies(被执行的那个)、dsh.engines.dsh、dsh.compatibility.dsh 三处声明锁到同一个生成值 |
lint.mjs |
语法门禁:node --check 解析所有随包发布与本仓库运行的 JS 文件,已并入 npm test |
live-host-smoke.mjs |
双端冒烟:对正在运行的 web / 桌面宿主各测 state p50/p95、SSE 首事件、注入注册与字符数,并比对两端快照(除 injection.pulls 外须逐字段相同);npm run smoke:hosts,--require-pulls 可把「从未经历真实 Agent 回合」判为失败。刻意不进 npm test——CI 没有桌面端。请求走 Node http 直连,绕开 http_proxy(否则回环请求也会被代理转发,读数虚高) |
docs/demo.html |
离线演示页:桩掉 DSH 外壳、加载真实 Client 半渲染设置页(不随 npm 包发布) |
memory.schema.json |
手改存储文件用的 JSON Schema(随包发布,并被宿主复制到 memory.json 旁边) |
生效方式
- 始终 条目:无条件遵守。
- 按需 条目:由模型按相关性自行判断是否适用,不相关时忽略;可填「适用场景」辅助判断。
- 优先级 高 > 普通 > 低,决定注入顺序。
- 注入字数上限:超出时按优先级保留,被略过的条目数会在注入文本末尾注明。 上限单位是字符而非 token——按 DeepSeek 官方分词比例约 1 个中文字符 ≈ 0.6 token、 1 个英文字符 ≈ 0.3 token(不同模型分词有差异,实际用量以模型返回为准),上限 40000 字符约合 24000 token。
- 预算用量可视化:设置页在上限输入框下方显示用量条——「实际注入 x / y 字符(约 z token, 估算)」(token 数按 0.6 系数估算,仅作参考);被上限略过的生效中条目会在列表里逐条 标注橙色「本轮不注入」(悬停可见原因),极端情况下首条被迫截断时标注红色「已截断」。 调高上限或调整优先级后标记实时变化。这些数据只是对注入取舍的展示,不改变注入文本本身; 存储格式不变,旧宿主/旧客户端对新字段一律忽略,互不影响。
- 标签、筛选与排序:每条指令可加若干标签(回车或逗号添加,最多 8 个、单个不超过 24 字符)。 标签只用于设置页分组与筛选,不会进入注入文本;标签缺失的旧记忆照常加载(等同无标签)。 列表可按标签、生效方式(始终/按需)、启用状态组合筛选,并在「按优先级 / 按更新时间 / 按标题」 之间切换排序;列表排序纯为查看方便,不改变宿主侧的注入顺序。
- 批量操作:点「批量操作」进入多选,可对勾选条目一次性启用、停用、设置优先级、加标签或删除;
多选与上方的筛选/搜索叠加(「全选」只作用于当前可见条目)。整批修改在宿主侧是单次写入
(只产生一次落盘、一次修订号递增与一次同步推送),全部条目已是目标状态时不写盘;批量删除需确认。
批量的启停/优先级/标签调整不重盖更新时间,因此不会改变同优先级下的注入先后。
「加标签」在浏览器里用原生输入框;桌面版(Electron)不提供
window.prompt(调用即抛错), 此时自动改用面板内的行内输入框(回车提交、Esc 取消),功能不受影响。 - 用户形象:除了"要求 AI 怎么做"的指令,你还可以记录关于你本人的信息(身份背景、喜好、性格、 长期追求等),每张形象卡片可单独勾选「使用该形象」——可以一个都不用、只用一个、或同时勾选多个 (多个会作为同一画像的不同侧面综合)。启用后,形象段会以「## 关于用户(用户形象)」为抬头注入在 指令块之前,抬头明确把它限定为"仅供调整回答侧重":AI 应在保证回答正确、完整、客观且扣题的 前提下据此调整解释深浅、详略、举例与建议方向,不得为了迎合而改变事实、省略风险提示或偏离问题本身, 无关时直接忽略。形象不是让 AI 扮演的角色。形象段与指令共用同一个注入字数上限: 形象优先占用,指令只拿剩下的余量(早期版本让形象完全绕过预算,十几个填满的形象能在指令预算还没被 考虑之前就塞进约 5 万字符)。形象自身另有 4000 字符的硬上限,超出部分按顺序截断并说明。 页面上的用量条同时计入形象与指令,所以它显示的比例就是宿主真正用来裁剪的那个数字。 总开关关闭时形象与指令一起停用。
总开关关闭时,该提示词段落(指令与用户形象)整个注销,系统提示词与未安装本插件时逐字节一致; 总开关开启但没有任何生效指令、也没有启用形象时,段落同样不出现。
临时静音(暂停注入一段时间,F7)
总开关旁提供「暂停注入…」:30 分钟 / 1 小时 / 到今天结束(本地时区 24:00)。暂停期间
指令与用户形象都不注入,系统提示词与「未安装本插件」逐字节一致;到期由插件内的
宿主侧定时器自动恢复——无需用户操作,也不产生写盘(存储文件与修订号不变)。暂停
状态保存在存储文件的 disabledUntil 字段里(仍是格式版本 2 的可选字段,缺失即未暂停),
因此跨重启保留。手动切换总开关会立即清除暂停(显式开关优先于定时)。设置页在暂停中
显示剩余时间倒计时,并可随时「立即恢复」;倒计时只是展示,权威判定始终在宿主快照
(injection.muted)里。到期判定与注入渲染都是可注入时钟的纯函数(isMuted、
planInjection(data, now)),相同输入永远得到相同字节。
存储
记忆保存在 DSH 数据根目录下(设置页会显示完整绝对路径):
<DSH_HOME>/instruction-memory/memory.json # DSH_HOME 默认为 ~/.dsh
<DSH_HOME>/instruction-memory/memory.json.bak # 上一版(崩溃恢复用)
<DSH_HOME>/instruction-memory/memory.schema.json # 手改用的 JSON Schema 副本
<DSH_HOME>/instruction-memory/history/ # 版本历史快照(见下文 F14)
路径优先级与官方 @deepseek-ai/dsh-home-paths 一致:显式 $DSH_HOME → ~/.dsh。
空白的 $DSH_HOME 视为未设置,绝不会退回当前工作目录——否则用户换个目录启动
DSH 就会得到第二份空记忆,看起来像"记忆消失"。相对的 $DSH_HOME(如 ./data)会在
启动时按当前目录解析为绝对路径,因此设置页显示的始终是完整路径。
1.0.0 之前的构建曾把数据写在宿主进程 cwd 下(
.dsh-instruction-memory.json)。 首次启动新版本时会自动迁移到上述位置,并把原文件改名为*.migrated保留。
当前存储格式为版本 2(1.1.0 及更早写的是版本 1,见下文「多情境配置集」)。
两者都能载入:版本 1 的文件会平滑迁移——其中的条目与用户形象全部归入一个名为
「默认」的配置集,文件在下次保存时写为版本 2(载入本身不写盘),并明确提示这一点。
反过来,用 1.1.0 及更早的版本打开版本 2 的文件会被拒绝载入并暂停修改(原文件保留、
数据完好),这是刻意设计:旧版看不到 sets 字段,若按版本 1 读进去,下一次保存就会把
配置集信息抹掉。升级回新版即可继续。
该文件可手工编辑,保存后已打开的设置页会自动刷新(见下文「外部改动自动刷新」);
流式更新不可用时「重新载入」按钮仍可手动读回。每次保存先写临时文件再原子替换,
进程崩溃不会留下损坏的半截 memory.json;保存前把上一版复制为 memory.json.bak,
若 memory.json 解析失败会自动临时回退到该备份并明确提示。
备份为什么是"复制"而不是"改名":曾经的做法是先把
memory.json改名成.bak、 再改名临时文件顶上。这两步之间memory.json是不存在的——只要第二步失败(文件被占用、 磁盘满、杀软扫描),主路径就一直空着,下次启动找不到文件就会建一份空存储,用户的 记忆看起来凭空消失,而真实数据其实好好躺在.bak里。改为复制后主文件在原子替换成功 前始终待在原位;同时启动时若发现memory.json缺失而.bak可用,会从备份恢复并写回 主路径,而不是从零开始。覆盖
.bak之前会先确认主文件确实可用:只有解析得出合法存储(或主文件为空/缺失)才覆盖。 早期版本只判断"字节数不为 0",于是一份被截断的memory.json(非空但解析不了)会把唯一完好的.bak覆盖掉——恰好在最需要它的时刻销毁了它。现在"非空但不可解析"的主文件不会动.bak。
多实例与多窗口(并发保护)
两个 DSH 进程共享同一 $DSH_HOME(如 web 与 tui profile 同时运行),或两个设置页
窗口同时打开时,数据可能被并发修改。两层保护:
- 跨进程写锁:每次写盘前创建
memory.json.lock(独占创建),写完即删。锁存在超过 10 秒视为持锁进程已崩溃、直接接管;等待 2 秒仍拿不到锁不会导致保存失败——宁可继续 写入(last-write-wins)也不让用户的保存被卡死。原子替换保证任何交错下文件本身不会损坏。 - 修订号(
rev)与冲突拒绝:每次成功写入使data.rev加一并落盘。客户端的每次修改 请求都带上它看到的rev(baseRev);若与服务端当前rev不一致(另一个窗口/进程刚 提交过),服务端拒绝本次修改并返回最新快照,面板自动刷新并提示重试——防止旧窗口 把别的窗口刚删除的条目"复活"回去。不带baseRev的旧客户端保持原有行为。 - 内存与磁盘的修订号比对:写盘前先读一次磁盘上的
rev。若磁盘已经被另一个进程推到更高的rev,说明本进程内存里的那份快照已经过期,此时拒绝写入并提示"另一个 DSH 进程刚保存过", 而不是继续写。早期版本只把rev递增到max(自己, 磁盘)+1,看起来修订号单调,实际上是把 过期内存整份盖回磁盘——另一个窗口刚存的东西会静默消失,调用方还收到"已保存"。baseRev是"同一进程内"的乐观锁,这一条补的是"跨进程"的那一半。
外部改动自动刷新(SSE)
设置页打开期间通过 EventSource 订阅同一路由上的事件流(GET +
Accept: text/event-stream),存储一旦在面板之外发生变化,已打开的面板自动更新,
无需手动「重新载入」:
- 触发来源:其他窗口经同一宿主保存(写盘成功即向所有订阅者推送变更);
memory.json被外部进程或手工改写(宿主以fs.watch监听存储目录——原子替换在 Windows 上会 使文件级监听失效——去抖 250ms 后经与写盘相同的队列重读,内容确有变化才重载并推送; 自己的写入盘后磁盘与内存一致,天然被同一比较抑制,不会回声)。 - 推送的是变更,不是全量:设置页以
?lite=1订阅,宿主只推{ rev, lite: true }(约 40 字节),面板发现 rev 比自己渲染的更新时才回拉一次state。满配存储的一份快照 约 1.24 M 字符(中文约 3.4 MB),逐次全量推送会把这些字节乘以打开的窗口数。不带该参数 的旧客户端仍收到完整快照,两代客户端与两代宿主可任意组合。 - 回声抑制:发起修改的窗口也会收到自己那次写入的推送,但帧里的 rev 与它已吸收的响应 快照相同,客户端按 rev 比对直接丢弃(旧客户端的完整帧则按内容逐字节比对)——否则刚出现 的「已保存」提示会被空消息的推送帧瞬间冲掉。
- 降级而非报错:宿主不支持或连接中断时
EventSource按retry: 5000自动重连, 重连失败期间面板照常可用(「重新载入」仍是兜底),不会弹出错误。 - 编辑器中未保存的草稿不受推送影响;保存时按当前
rev提交,外部若已抢先修改 会被冲突拒绝拦下。
响应信封中的 saved 字段语义为「本次操作是否把存储写入了磁盘」,只反映真实写入:
普通读取(state/reload/export-data)一律 false;首次运行时 state 物化文件、
或 reload 触发从 .bak 恢复/旧位置迁移这类"载入即写回"的分支才为 true。早先版本
对读取操作也回 true,使该字段不携带任何信息。
若手工编辑后的文件有条目为空或超过 200 条上限,设置页会明确提示哪些条目未能载入
(它们将在下次保存时从文件中移除),不会静默丢弃。手改的选项值同样有披露:enabled
写成非布尔值、或 budgetChars 超出 800–40000 范围/不是数字时,载入会按规则修正
(回退到 true / 夹到边界),并在设置页明确提示原值 → 修正值——否则下次保存会把
修正值悄悄写回文件,你以为改过的设置其实从未生效。导入时若某条超出长度上限
(标题 120 / 内容 6000 / 适用场景 200 字符),会明确提示有几条、哪个字段被截断,
而不是悄悄改短。文件若声明了未来的格式版本(version 非 1),插件会拒绝载入并
暂停一切修改(此时保存会用 v1 格式覆盖文件,可能损坏数据),直到你升级插件或把
文件改回版本 1 后点「重新载入」。
设置页支持导出 / 导入:导出为自带说明的 JSON 文件,导出的是整份存储(所有配置集,
不是只看当前激活的那一套)——备份只带走一半就没有意义了;导入采用合并语义——在同一个
配置集内按 id 与「标题+内容」去重,只新增、绝不覆盖或清空现有条目。导入文件里带
sets 时会按需创建对应配置集并把条目放回原集合(本插件的导出天然带上);指向不存在集合
的条目会归入当前激活集合,并在结果里说明有几项如此处理。要"整体还原"请用上文
「从备份恢复…」,导入不做覆盖。
请求体上限由各项上限推导(`(MAX_ENTRIES × (MAX_CONTENT + MAX_TITLE + MAX_WHEN + 8×24 + 200)
- MAX_PROFILES × (MAX_PROFILE_CONTENT + MAX_PROFILE_NAME + 200) + MAX_SETS × (MAX_SET_NAME + 200)) × 2 + 100000`,另加形象与配置集的开销),保证本插件导出的文件一定能被自己导回——早先 写死的 1,000,000 小于满配导出(200 × 6000 = 1.2 M 字符),导致满配记忆导出后无法导入。
首次运行不写文件(惰性创建)
全新安装、尚无任何记忆时,插件不会在宿主启动那一刻创建空的 memory.json。
这里曾主动物化一份空存储,只为让设置页有个真实路径可显示。但那意味着插件在进程启动、 零会话、零任务的状态下执行了一次磁盘写入——这正是行为审计中唯一能被正当称为 "没活干还在动"的副作用。现在文件由第一个真正需要它的动作创建:一次保存、一次导入, 或设置页读取
state(面板打开时就会读)。
在这之前:storage.path 仍会显示路径,但 storage.onDisk 为 false,设置页显示
「尚未写入磁盘」(该文案本就存在),state.injection.registered === false,pulls === 0。
挂载日志相应为 from=first run (no store on disk yet) onDisk=no。
不受影响:从 .bak 恢复、以及从旧位置迁移——这两条分支只有在磁盘上已有真实数据时才会走,
它们仍然会立即写回。
多情境配置集(配置集 / sets)
一份记忆装不下"工作 / 个人 / 某客户"三套互不相干的规则,所以设置页新增了配置集:
多个命名集合共存于同一个 memory.json,用户显式一键切换,注入只渲染当前激活的那一套。
命名:存储字段叫
sets(配置集),不叫profiles——profiles已经被 1.1.0 的 「用户形象」占用了,两者是完全不同的东西:配置集是"记忆的分区",用户形象是"关于你本人的 画像"。文档里的 "Profiles" 指的就是配置集。
- 配置集本身只有
id与名称;条目与用户形象通过各自的setId指向它。因此"切换"只是 改一个字段——不会移动、复制或删除任何一条用户文本,也不可能出现"切到一半"的中间态。 - 设置页顶部列出全部配置集(含各自的条目/形象数量),点击「切换」立即生效并写盘(重启后 保持);「新建」「重命名」「复制」「删除」齐备。新建与复制都不会自动激活——激活是 唯一会改变注入内容的动作,它始终是一次单独的、看得见的点击。
- 删除当前激活的配置集会被拒绝(请先切换);删除其他配置集会连同其中的条目与形象一起 删除,确认框里写明数量。文件里至少要保留一个配置集。
- 「复制」用于从模板派生:条目与形象会以全新 id 复制到新集合,
客户A → 客户B就是这么来的。 - 全部配置集合计最多 200 条指令、50 个用户形象、20 个配置集。总开关与注入字数上限是 全局的,不分集合。
- 提示词里不会出现配置集名称或任何"当前属于哪套"的标记——注入的就是激活集本身,和只有 一套记忆时的文本逐字节一致。切换集合会改变提示词前缀,这是预期的(缓存会随之失效一次)。
红线:绝不按进程 cwd 隐式建集或切换。 这条规则来自一次真实事故:早期构建把数据文件 解析到宿主进程的工作目录下,用户换个目录启动 DSH 就得到第二份空记忆,看起来像"记忆凭空 消失"。配置集沿用同一条纪律:
activeSetId只能由用户显式命令写入,载入时它只从文件里读, 任何"根据当前项目目录推断集合"的想法都不要加进来。
版本历史与回滚(F14)
现在只有一份 .bak,误改或批量误删之后回不到更早的状态。每次成功保存后,插件会把这一版
原样写入 <备份位置>/history/ 下的一个快照文件:
20261015T152355123Z-r12-laptop.json # UTC 时间戳 - rev - 写入设备
- 命名是固定宽度的:按文件名排序就是按时间排序,列出历史只需读目录,不必逐个打开文件。
- 每台设备各保留最近 20 份(按文件名里的设备标签分组),逐出最旧的。两台机器共用一个 备份目录时,谁也不会把对方的历史剪掉。不符合该命名的文件(手工放的、别的工具写的、 云盘的冲突副本)一律不碰。
- 份数之外还有32 MB 字节预算(同样按设备计算):满配存储(200 条 × 6000 字符)的一份 快照约 3.6 MB,只按份数保留会到约 72 MB;超出预算时从最旧的开始逐出,但最新一份永远 保留(哪怕它自己就超过预算)。
- 为什么放在
history/子目录:memory.json所在目录被fs.watch监听以做外部改动 自动刷新,快照绝不能混进去触发重载;子目录不在非递归监听的范围内,文件名过滤也会兜底。 - 设置页「版本历史」里可展开列表(时间、rev、条目/形象数、体积、是否来自其他设备),
一键回滚或删除某一份快照。回滚是把快照内容作为新的一版写回(rev 只增不减),并且
它自己也会留下一份快照——所以回滚本身也能再回滚回来;被替换掉的内容仍然躺在
memory.json.bak里。 - 快照写失败不会让这次保存失败(存储已经安全落盘),只会在面板上说明"历史快照未写入"。
另外,导入(见下文)是只增合并,无法还原整份记忆;因此另有一个**「从备份恢复…」入口:
选择本插件导出的文件,确认后整体替换**当前记忆。同样是"可反悔"的——替换前的内容已经在
.bak 与版本历史里。
手改与 JSON Schema(F15)
随包发布一份 memory.schema.json(JSON Schema draft-07),描述版本 1/2 两种文件形状与全部
长度上限。宿主还会把它的副本写到 memory.json 旁边,而 memory.json 里写死了:
{ "$schema": "./memory.schema.json", "version": 2, … }
于是把 memory.json 拖进任何支持 JSON Schema 的编辑器(VS Code 等),就能直接得到字段校验、
补全与悬停说明,不需要知道这个包装在哪里。Schema 与代码的一致性由 verify.mjs 逐项断言
(上限、枚举、最大条数),因此它不会和实现悄悄漂移。
CLI 管理命令不在本次承诺范围内。 它依赖宿主是否向插件开放命令能力,目前未经验证;
tui/headless环境下设置页不可用这一点没有改变,手改 JSON + 这份 Schema 是当前 官方支持的路径。
源码模式(F16)
设置页的「源码模式」把整份存储文件(与 memory.json 内容一致)放进一个文本框直接编辑:
- 输入时实时做 JSON 语法校验(防抖 250ms),并摘要显示"几条指令 / 几个形象 / 几个配置集"; 语法不通过时保存按钮直接禁用。
- 保存走的是与手改文件完全相同的一条路:解析 → 归一化 → 披露(未能载入的条目、被修正的
选项值、被截断的字段、指向不存在配置集的
setId、未来格式版本拒绝),再按rev提交。 因此它比裸编辑器安全,也不会绕过任何既有校验。 - 草稿不会因为一次拒绝而丢失:冲突(其他窗口/进程在你编辑期间提交过)、版本拒绝、 语法错误都只显示原因,文本框里的内容原样保留;点「重新载入最新内容」才会放弃本地修改。
- 提交时带的
rev是打开源码时读到的那个,而不是当前面板上的最新值——这样"你编辑期间 别人改过"会被如实判为冲突,而不是把你的整份文本盖到别人的修改上。 - 与卡片编辑互斥:正在编辑某条指令/形象时不能进入源码模式(按钮禁用),避免两份草稿互相覆盖。
自定义备份位置(F18,不含网络同步)
默认一切都在存储目录里。设置页可以把备份位置指向别处(例如本地云盘文件夹):
- 影响三件事:
history/快照目录、「导出到备份目录」的去向、以及额外一份memory.json.bak副本。存储文件本身与它的跨进程锁不会移动——崩溃恢复用的memory.json.bak始终留在存储目录,因为那是"下次启动必须能找到"的东西。 - 设置时会当场校验:必须是绝对路径、目录存在、且实际写入一个探针文件成功;不满足就直接 拒绝并说明原因(例如"目录不存在或不可访问"),而不是等到某次保存悄悄失败。
- 它是本机设置:从备份文件"整体替换"或回滚历史时不会被改写——否则一台机器上的路径 会被带到另一台机器上,把备份悄悄写到一个不存在的目录里。
- 多设备建议:把备份位置指向本地云盘目录,只让导出文件与
history/快照同步; 不要同步存储目录本身(memory.json)——两台机器同时写入会互相覆盖,插件那套 rev 冲突检测与写锁都只在同一台机器内有效。换机器时用「导出 → 另一台导入/恢复」。
按需指令的本地预演(F17)
「按需」条目到底会不会被采纳,本质上是模型的判断,插件看不到也猜不准。设置页提供一个 本地预演:输入一句当前任务描述,前端纯字面关键词匹配各条按需指令的 「适用场景 / 标题 / 内容」(场景权重最高、正文最低),列出字面重合度较高的条目并给出 高/中/低分档,同时在列表里给这些条目打上「预演·高/中/低」标记。
它的用途只有一个:帮你把「适用场景」写清楚、预判取舍(例如"没写场景的条目只能靠正文 碰运气"、"这条优先级低、一旦超出字数上限会先被丢下")。因此:
- 措辞是克制的:界面上明确写着**「本地预演 ≠ 模型的真实判断」**,它不参与注入,也不代表模型
实际会不会采纳;
verify.mjs断言了这句免责声明在两种语言里都存在。 - 只有「按需」条目参与;「始终」条目无条件生效,与预演无关。
- 匹配是启发式的:中文按 2/3 字滑窗切词(无分词依赖),英文/数字按词切分,术语上限 40 个, 纯前端计算、不发任何请求。
无 Web 服务器时依然生效
systemPrompt 是本插件唯一的硬依赖。Web 服务器不是前置条件:设置页那部分路由通过
非阻塞的 ctx.inject(['webServer'], …) 子 fiber 注册,服务出现时自动挂上;没有 Web 服务器
的 profile(headless、tui)里,系统提示词注入照常工作,只是设置页不可用。
这里曾声明
inject = ['systemPrompt', 'webServer']。声明式依赖是前置条件,于是没有 Web 服务器的 profile 里整行插件永远停在 pending、apply()根本不执行——用户同时失去设置页 和记忆注入,而记忆本身根本不需要 Web 服务器。非阻塞注册同时把最初那个竞态修得更彻底: 回调在服务发布时就会执行,而不只是在它"抢在插件挂载之前"发布时才生效。
诊断日志
插件此前只有失败才有输出:每次载入、保存、注入成功都不留任何记录,只有失败分支走
console.error。于是"记忆到底有没有在干活"完全无法从日志回答。现在它挂载时写一行
info,过程细节写 debug(走宿主的 ctx.logger):
[instruction-memory] mounted: store=<DSH_HOME>\instruction-memory\memory.json from=memory.json onDisk=yes entries=2 sets=2 activeSet=st-default enabled=true section=437 chars (pending assembly) pulls=0
挂载行含数据来源(memory.json / memory.json.bak (main file missing) / memory.json.bak (main file corrupt)
/ legacy cwd file (migrated) / first run (no store on disk yet))、是否已在磁盘上(onDisk)、
条目数、总开关状态、段落就绪时的字符数,以及 pulls。
为什么是 section=… (pending assembly) 而不是 injected=…
「段落已就绪」≠「已经注入」。 注入发生在宿主装配提示词时;插件在挂载那一刻什么都没注入过,
打印 injected=437 chars 会让人误以为记忆已经进入了某个提示词。这是本插件此前一处会误导排查的措辞。
真正的"到达了提示词"的信号是 pulls:宿主每次对这段做装配,插件注册的那个 thunk 就被求值一次,
计数器加一。因此
pulls=0= 已准备、从未装配(进程刚起来、或会话空闲);pulls=N= 已被拉进提示词 N 次。
插件看不到会话或轮次,不会报告 sessions= 之类的数字——那会是编造。它只报告自己能真正观测到的东西。
pulls 同时出现在路由的 state 快照里(injection.pulls)。每次拉取另有一条 debug 行:
section pulled into a prompt (pull #N)。
这是纯日志,绝不影响注入内容:同一份存储在"有 logger"和"没有 logger"下渲染出的文本
逐字节相同(verify.mjs 有断言锁定这一点)。debug 级别由宿主日志级别过滤;
没有 logger 服务的环境下需要 DSH_INSTRUCTION_MEMORY_DEBUG=1 才会输出到标准错误。
诊断与安全状态面板(F12)
设置页的「诊断信息」卡片把散在快照里的排障信息集中展示(默认收起,纯展示、无写路由):
- 存储:路径、是否已在磁盘、载入来源(
loadedFrom:首次创建 / 采用现有文件 /.bak恢复 / 旧版迁移等)、最近保存时间、修订号(rev); - 注入:是否已注册、
pulls拉取计数(自本次插件启动,含义见上文)、当前字符数 / 上限; - 备份与历史:备份位置、是否默认、
.bak覆盖副本路径、版本历史目录与快照数、Schema 文件位置; - 错误与警告:存储错误 / 存储警告 / 历史错误 / 注入错误原样显示。
「复制诊断信息」把以上内容序列化成纯文本(附 navigator.userAgent、语言与当前时间,
便于提 issue)。它绝不包含条目正文、形象内容等用户文本;剪贴板 API 不可用时降级为
execCommand 复制,再不可用则把文本放进可手动全选的文本框(序列化逻辑是导出的纯函数
__diagnosticsText,verify.mjs 断言其输出含关键字段且不含任何用户文本)。
卡片同时补上了此前缺失的唯一安全信号:security.hostGuarded。设置路由绑定在非回环
地址(主动对外发布)时,页面顶部会出现醒目橙色警告:同一网络内的设备可无鉴权访问此页面、
请勿在不可信网络暴露,并指向下节「经反向代理对外发布」。该判定与路由实际执行的 Host 校验
是同一个开关;快照同时携带 boundHost(路由注册时捕获的绑定地址),警告会点名具体地址。
反馈邮件(可选配置)
设置页最上方(第一块)新增「反馈」按钮:填写问题类型、标题、正文与可选联系方式后,由 Host 进程 直接通过 SMTP 把邮件发到维护者邮箱。客户端只提交表单,不接触任何服务凭证; 未配置时按钮仍可点开,提交会返回一条带配置指引的错误,并保留已填内容以便重试。
隐私边界:邮件自动附带插件版本、Node/DSH 版本、平台与架构、界面语言、时区、
User-Agent 和提交时间;绝不附带任何条目正文、形象内容或整份 memory.json。
回复路径:From 固定为认证的发件账号;若「联系方式」是一个纯邮箱地址,Host 会把它写成
Reply-To,维护者点「回复」即可直接回到反馈者(微信、GitHub 用户名等自由文本不会进入邮件头)。
配置只认环境变量或仓库外的 JSON 文件,不要写进 cordis.patch.yml 或任何提交进仓库的文件。
环境变量(优先)
| 变量 | 必填 | 说明 |
|---|---|---|
DSH_INSTRUCTION_MEMORY_FEEDBACK_TO |
是 | 收件邮箱(单个地址,仅 ASCII;非 ASCII 地址会明确报错而不是发出乱码头) |
DSH_INSTRUCTION_MEMORY_FEEDBACK_SMTP_HOST |
是 | SMTP 服务器主机名 |
DSH_INSTRUCTION_MEMORY_FEEDBACK_FROM |
否 | 发件地址,默认取 SMTP 用户名;部分服务商要求与登录账号同域 |
DSH_INSTRUCTION_MEMORY_FEEDBACK_SMTP_PORT |
否 | 1–65535,默认按 _SMTP_SECURE 取 465 / 587 |
DSH_INSTRUCTION_MEMORY_FEEDBACK_SMTP_SECURE |
否 | 1/true/yes 走隐式 TLS(465);默认 0 并用 STARTTLS 升级(587) |
DSH_INSTRUCTION_MEMORY_FEEDBACK_SMTP_USER |
否 | 登录用户名;与密码必须成对出现 |
DSH_INSTRUCTION_MEMORY_FEEDBACK_SMTP_PASS |
否 | 登录密码 / 应用专用密码 |
DSH_CRB_SMTP_URL |
否 | 复用的 URL 形凭据(格式见下节),优先级低于上面五个 ..._SMTP_* 变量 |
DSH_INSTRUCTION_MEMORY_FEEDBACK_CONFIG |
否 | 配置文件路径,默认 <存储目录>/feedback.json |
DSH_INSTRUCTION_MEMORY_FEEDBACK_COOLDOWN_MS |
否 | 两次发送的最小间隔,默认 30000,0 关闭 |
DSH_INSTRUCTION_MEMORY_FEEDBACK_SECRET |
否 | 解密 toEncrypted 的密钥,至少 16 个字符;只在环境变量里提供,不要写进配置文件或仓库 |
存储目录默认是 %USERPROFILE%\.dsh\instruction-memory(受 DSH_HOME 影响),
即默认配置文件为 …\instruction-memory\feedback.json。
复用已有的 DSH_CRB_SMTP_URL(可选)
如果本机其他工具已经在 Windows 用户级环境变量 DSH_CRB_SMTP_URL 里保存了可用的
SMTP 凭据,插件可以直接复用,不必再把主机/账号/密码抄一份到本插件的变量或文件里:
smtps://用户:密码@smtp.qq.com:465
smtp://用户:密码@smtp.example.com:587?secure=0&from=bot@example.com
smtps://走隐式 TLS(通常 465);smtp://默认 STARTTLS,可用secure=1/0覆盖;smtp://且端口为 465 时按隐式 TLS 处理(服务商常见约定)。- 用户名、密码中的
@ : / %等字符必须 URL 编码,例如1234567890%40qq.com。 - 逐字段优先级:本插件专用
..._SMTP_*变量 >DSH_CRB_SMTP_URL>feedback.json。 因此可以只把密码留在共享 URL 里,其他字段仍用插件变量覆盖。 from可用?from=指定;缺省取 SMTP 用户名(若像邮箱),否则取收件人。- 这个变量携带密码:不要写进仓库、不要截图;修改后需要重启 DSH 才会被新进程读到。
配置文件(与环境变量逐字段合并,环境变量优先)
{
"to": "feedback@example.com",
"from": "dsh-bot@example.com",
"smtp": {
"host": "smtp.example.com",
"port": 465,
"secure": true,
"user": "dsh-bot@example.com",
"pass": "应用专用密码"
}
}
文件权限请自行收紧(Windows 可用 icacls feedback.json /inheritance:r /grant:r "%USERNAME%:R")。
凭据缺失时只报"未配置",不会把已配置的密码回显到页面或日志。
收件邮箱加密(可选)
如果不想让收件邮箱以明文留在 feedback.json 里(例如担心截图、误提交或备份泄漏),
可以把 "to" 换成密文字段 "toEncrypted",密钥只放在环境变量里:
{
"toEncrypted": "im1:……",
"smtp": { "host": "smtp.qq.com", "port": 465, "secure": true, "user": "…", "pass": "授权码" }
}
- 设置密钥环境变量(至少 16 个字符,不能写进仓库):
DSH_INSTRUCTION_MEMORY_FEEDBACK_SECRET=…… - 用插件自带的小工具生成密文(密钥从环境变量读取,不会打印到终端):
node lib/mailer.js encrypt-to you@example.com - 把输出的
im1:…粘贴为toEncrypted,删除"to"(同时保留两者会被拒绝),然后重启 DSH (环境变量只在启动时读取)。
实现是 AES-256-GCM + scrypt 派生密钥,密文带认证标签:改错一个字符或密钥不匹配都会明确报
无法解密反馈收件邮箱,不会静默换收件人。配置文件里同时留下 to 和 toEncrypted 会被直接拒绝
(否则明文一直有效,加密只是摆设);环境变量 DSH_INSTRUCTION_MEMORY_FEEDBACK_TO 仍优先于
文件里的 toEncrypted,用于临时改收件人。
能力边界(请勿误解):SMTP 信封必须把收件人明文交给服务器,TLS 只保护传输链路;
本加密保护的是配置文件静态副本,不防"能以同一 Windows 用户身份读取环境变量和文件"的攻击者,
也不隐藏邮件服务商看到的收件人。需要更强隔离时,应使用独立的 Windows 账户或独立机器运行 DSH。
node lib/mailer.js encrypt-to 生成的密文与密钥绑定,换机/换密钥后需重新生成。
国内邮箱示例:QQ 邮箱填 smtp.qq.com、端口 465、"secure": true,用户名为完整 QQ 邮箱地址,
密码填在 QQ 邮箱「设置 → 账户」开启 SMTP 后生成的授权码(不是登录密码)。
若收件人是该 QQ 邮箱、发件人另用其他账号,from 要与发件账号同域,否则可能被服务端拒收。
发送行为与排查
- 465 端口:直接 TLS;587 端口:EHLO → STARTTLS → 再次 EHLO →(有账号时)
AUTH LOGIN/AUTH PLAIN。 - 拒绝在明文连接上发送密码:服务器不提供 STARTTLS 时会直接失败,而不是降级。
- 主题与正文均为 UTF-8(RFC 2047 编码主题、base64 正文),中文不会乱码。
- 成功后清空草稿并提示"反馈已发送";失败时保留草稿,页面显示原因,宿主日志记一行脱敏错误。
- 面板为一次发送最多等待 90 秒,宿主自身的整封投递上限是 60 秒:面板总比宿主晚放弃, 因此"超时"提示出现时邮件不会再迟到地发出去。
- 默认 30 秒冷却:同一进程内连续提交会被拒绝(防误触重复发信),可用上面的变量调整; 冷却在发送开始前就占位,所以两个同时到达的请求也只有一个会被投递。
常见失败:尚未配置反馈收件邮箱 → 环境变量/文件未生效(环境变量改完要重启 DSH;
配置文件每次发送都会重新读取,无需重启);反馈收件邮箱已加密,但未设置 …SECRET 或
无法解密反馈收件邮箱 → 密钥缺失/不匹配,重新执行 encrypt-to 并同步更新环境变量;
配置文件同时包含 to 与 toEncrypted → 删掉明文 to;反馈收件邮箱含非 ASCII 字符
→ 换成 ASCII 地址(未实现 SMTPUTF8);配置文件 smtp.pass 类型无效 → 密码写成 JSON 字符串;
SMTP 总耗时超过 60000 ms → 服务端长时间无响应,检查主机/端口/网络;
SMTP 连接失败 → 主机/端口/防火墙;STARTTLS 失败 → 服务商要求 465 隐式 TLS;
认证失败 → 用户名/密码或应用专用密码,部分邮箱需先开启 SMTP 服务;
发件地址被拒绝 → 显式设置 FROM 为与账号同域的地址。
请求来源防护(Host / Origin)
设置页路由之所以要求 content-type: application/json,是因为 HTML 表单无法伪造 JSON body,
而跨源 fetch 带 JSON 会先在 preflight 失败。这两条防线都建立在"浏览器认为这是跨源"之上,
DNS 重绑定恰好推翻了它:evil.com 解析到 127.0.0.1 之后,页面与服务器在浏览器眼里是
同源——不发 preflight、JSON 也合法,于是任何网页都能改写用户那份"注入到此后每一轮
对话"的长期指令。此时唯一无法伪造的就是浏览器发出的 Host 头。
因此当服务器绑定在回环地址(默认)时,路由要求 Host 是回环,另外校验 Origin(若存在)
与 Host 同源。命中即 403,并记一行日志:
[instruction-memory] route rejected a request: Host 头不是回环地址(可能是 DNS 重绑定攻击):evil.com:8080
- 绑定到任何非回环地址(
--host 0.0.0.0、::或具体 IP,即主动对外发布)时 Host 校验自动关闭——那种情况下无法预知合法 Host,强行限制只会打断所有正常客户端;Origin校验仍然生效。 - 无
Host的 HTTP/1.1 请求由 Node 的 HTTP 解析器先以 400 拒掉,根本到不了路由。 - 端口参与
Origin比对(早期版本忽略端口,于是127.0.0.1:1这种同主机不同端口的页面也算同源):比对前先把默认端口归一(http:80/https:443视同未写端口),其余情况要求 scheme、主机名、端口三者一致。Host末尾的 FQDN 点(localhost.)会被忽略,避免同一台机器的两种写法被当成跨源。
此外路由还读取 Sec-Fetch-Site(Fetch Metadata):现代浏览器给每个请求都打上它,
即使某些技巧(如 no-cors fetch)不带 Origin,evil.com 发来的请求仍会被标记为
cross-site——命中即 403。在回环绑定(受保护)时 cross-site 与 same-site 都拒绝:
"同站"判定不看端口,而回环上的任何其他端口都是别人跑的本地服务,放行 same-site 等于让任意
本地页面改写你的长期指令。none(用户直接导航)放行;curl、Node 与 2020 年前的老浏览器根本
不发这个头,也不受影响(它们由 Host 与 content-type 校验兜底)。非回环绑定(主动对外发布)时
只拒绝 cross-site,因为反向代理可能合法地改写端口。
经反向代理对外发布(自行承担风险)
默认部署(回环绑定 + 本机浏览器)不需要这一节。若你把 DSH 的 Web 服务器绑定到非回环地址
(--host 0.0.0.0 或具体 IP)并置于 nginx / Caddy 等反向代理之后远程访问,需要注意:
先说清楚代价:该路由没有任何鉴权。一旦发布到网络上,任何能访问它的人都能增删改你
那份"注入到此后每一轮对话"的长期指令。Host 校验在非回环绑定时自动关闭,Origin 与
Sec-Fetch-Site 只能拦"来自其他网页的请求",拦不了直接发 HTTP 的攻击者。更安全的替代
是隧道方案(SSH 端口转发 / Tailscale / WireGuard):宿主仍绑定 127.0.0.1,远程客户端
通过隧道以回环身份访问,所有校验原样生效。
仍要走反向代理时,代理必须满足:
- 原样转发
Host(nginx:proxy_set_header Host $host;)。GUI 页面与 API 走同一个 域名时,浏览器发的Origin与转发的Host主机名一致,Origin校验通过;改写或剥掉Host会得到 403。 - 不要注入
Origin:代理自己添加的Origin头若与Host不同源,会被当作跨源拒绝。 - 透传
Sec-Fetch-Site:代理不应剥掉 Fetch Metadata 头;正常同站访问浏览器发的是same-origin,不受影响。 - 透传
content-type与请求体:路由只接受application/json的 POST, 路径前缀为/instruction-memory/api,代理转发规则需覆盖它。 - 限制来源:至少用代理层做 IP 白名单或加 Basic Auth——插件本身不提供任何鉴权。
兼容性
同一串范围写在三处,而只有一处会被宿主强制执行——这一点以前写错过,现在按实测更正:
| 位置 | 谁读它 | 作用 |
|---|---|---|
peerDependencies(@deepseek-ai/dsh-system-prompt) |
宿主:evaluatePluginCompatibility()(dsh-app-boot/lib/index.js),在安装与 profile 启动时执行 |
真正的门禁。不满足即拒绝(或要求显式授予精确版本豁免:dsh plugin allow-version)。门禁把 peer 的范围值与宿主版本(dsh --version)比较,peer 的包名不参与解析,只用来点名报错 |
dsh.engines.dsh |
插件市场(第三方,服务端读取) | 市场卡片上的兼容性判定与展示 |
dsh.compatibility.dsh |
未找到读取方(官方文档与 DSH 树内均无) | 仅作人类可读的重复声明 |
1.0.14 之前这里只有后两处,没有 peerDependencies——而宿主的门禁在读到缺失的 peerDependencies 时会直接 return undefined(if (!Object.hasOwn(fields, "peerDependencies")) return void 0),也就是跳过检查。换句话说,范围之外的宿主在此之前可以装上而没有任何拦截或警告;那串范围当时只是市场元数据,不是门禁。1.0.14 补上 peer 之后,它才真正生效。
同一串范围(三处逐字节一致,由测试锁定):
>=0.1.3-alpha.2 <0.1.4 || >=0.1.4-0 <0.1.5-0 || >=0.1.5-alpha.1 <0.1.6-0 || >=0.1.6-alpha.0 <0.1.7-0 || >=0.1.7-alpha.0 <0.2.0-0 || >=0.2.1-alpha.0 <0.3.0-0 || >=0.2.0-0
验证范围与证据等级——三者不可混为一谈,下面每一行都标了它到底证明了什么:
| 等级 | 证明什么 | 覆盖 |
|---|---|---|
| ① 真实启动 | 在该宿主版本上真的把 DSH 跑起来:插件装载、注入真的进入组装后的提示、插件 HTTP 路由可应答、客户端设置页模块被下发(Windows / Node 22) | 0.1.3-alpha.2 · 0.1.5-alpha.1 · 0.1.5-alpha.2 · 0.1.5-rc.1 · 0.1.5-rc.2 · 0.1.5-rc.3 · 0.2.0-rc.1 ⁽⁾ · 0.2.0-rc.2 ⁽⁾ · 0.2.1-alpha.2 ⁽⁾ · 0.2.1-alpha.3 ⁽⁾ · 0.2.1-alpha.4 ⁽⁾ · 0.2.1-rc.2 · 0.2.1-rc.3 ⁽⁴⁾ |
| ② 契约核验 | 抓该版本真实发布的源码,逐个核验本插件实际用到的每个符号:systemPrompt.section()、assemble() 是否求值函数型 text、FILE_REFERENCE=900 / TOOL_BASH=1000 仍在(决定 order 950 是否空档)、重名注册是否抛错、webServer.register() / get host()、ctx.inject() / ctx.effect()、dsh.client.inject 两包在该版本是否存在且声明了 dsh.client |
声明范围接住的全部 18 个已发布版本(0.1.3-alpha.2 → 0.2.1-alpha.4) |
| ③ 真实执行 | 把插件真的挂载进该版本的 @deepseek-ai/cordis,跑装载 / 路由注册 / 卸载共 9 项断言 |
cordis 4.0.2 与 4.0.4(各 9/9) |
⁽⁾ 0.2.0-rc.1 的①档有一条要说明:插件装载、注入进入组装后的提示、HTTP 路由可应答三项*均已实测(路由 HTTP 200;injection.registered=true、chars=437、pulls=38——即 0.2.0-rc.1 的 agent loop 真的求值了 38 次函数型 text)。第四项"客户端模块被下发"没有直接抓取:首页需要鉴权(/ 返回 401),我无法取到 window.__DSH_BOOT__。这一项的支撑是间接但强的两条:① dsh-client-modules 在合成阶段对 dsh.client 声明做硬校验,不合格会抛 ClientPackageCompositionError 直接导致启动失败——宿主正常启动即说明声明通过了 0.2.0-rc.1 的校验;② 本插件用到的三个客户端契约包(-slots / -renderer / -settings)相对升级前真正在跑的 0.1.7-rc.2 逐字节未变。未做人工视觉确认(没有打开设置页看)。
⁽⁾ 0.2.0-rc.2 的①档:桌面版 0.2.0-rc.2(私有发行版 dsh-desktop-runtime,见仓库 dsh-memory-verification/DSH-VERSION-COMPAT.md §12)上完成过路由 200 + registered=true + 注入段落真实出现在会话提示中的活体实测;网页版宿主 0.2.0-rc.2 上对本插件路由的 state 探测同样返回 200。同样未做设置页人工视觉确认**。
⁽⁾ 0.2.1-alpha.2(2026-10-10)与 0.2.1-alpha.3 / 0.2.1-alpha.4(2026-10-16)的①档:隔离安装该版本宿主树 + 独立 DSH_HOME 装入 npm 正式发布物,真实启动 web 宿主后以 21 项 HTTP 断言驱动(CRUD、落盘持久化、SSE 连接帧、section 注册/注销、4 项请求门禁、IPv6 同源等价与混源拒绝、30 连发稳定性、清理回基线)——三个版本各 21/21 PASS。证据:dsh-memory-verification/out/compat-1016-live-live-alpha2.json、dsh-memory-verification/out/compat-133-live-alpha{3,4}.json。同样未做设置页人工视觉确认*。
⁽⁴⁾ 0.2.1-rc.2 / 0.2.1-rc.3 的①档(2026-10-24,插件 1.4.2):rc.3 上隔离 DSH_HOME + 真实 dsh web 宿主驱动 21 项 HTTP 断言 21/21 PASS,桌面端活体 pulls=5052、p50 0.72 ms;并且首次直接抓到了客户端模块的下发证据——boot HTML 的预加载清单含 dsh-instruction-memory/client.js,该合并 bundle 返回 200 text/javascript(2,204,270 字节),正文含 Client 半源码与「指令记忆」文案。因此上一条 ⁽⁾ 里"无法直接抓到 window.__DSH_BOOT__"的限制*已对 rc.3 解除。rc.2 未单独起宿主,靠它与 rc.3 的逐文件字节比对(仅 package.json 与一行构建号不同)承接结论。证据:dsh-memory-verification/out/compat-1016-live-latest-rc3.json、…-rc3b.json。仍未做设置页人工视觉确认。
此外 host-range-test.mjs 的固定矩阵覆盖 0.1.6-alpha.2、0.1.7-rc.2、0.2.0-rc.1、0.2.0-rc.2 与 0.2.1-alpha.1~0.2.1-alpha.4(回归守卫把最新验证过的宿主逐名钉住),并会读取本机实际安装的宿主版本做实时断言。
关于 1.0.10 新增的 ctx.inject。 这是新版唯一的硬依赖增量:inject 去掉了 webServer,设置页路由改为非阻塞的 ctx.inject(['webServer'], …) 子 fiber(原因见上)。为此逐个核验了 cordis 全部 8 个已发布版本(4.0.1-rc.1 → 4.0.5-alpha.2)——每一个都具备 ctx.inject(其中 4.0.2 / 4.0.4 另做了真实挂载执行,4.0.5-alpha.1 与 4.0.5-alpha.2 为源码符号核验),因此声明范围内不存在因它而失效的宿主版本。
关于声明范围是否"超范围承诺"。 已对全部 33 个已发布宿主版本核验(2026-10-05 穷举 30 个;新增版本随发布补充——0.2.1-alpha.2 于 2026-10-10、0.2.1-alpha.3 / 0.2.1-alpha.4 于 2026-10-16,npm 官方 registry):范围内 18 个版本的 API 面全部齐备,超范围承诺 0 项,且默认与 includePrerelease 两种语义在这 18 个版本上零分歧。范围外的 15 个版本(0.0.1-rc.1 → 0.1.2-rc.1)API 面同样齐备,但它们从未真实启动验证过,所以下限维持不动、不向外放宽——不把没验证过的东西写进声明。
关于 0.2.1-alpha.1(2026-10-03 发布,1.0.16 起覆盖)。 它是前述"未来 minor 预发布版本在默认语义下接不住"的第一个真实发生案例:1.0.15 的范围在宿主门禁与市场判定(均用 includePrerelease)下放行它,但在 npm/pnpm 默认 peer 解析下拒绝(ERESOLVE);其耦合面(9 个契约点)经最近 10 版本矩阵核验全部齐备,cordis 为 4.0.5-alpha.1。1.0.16 新增 >=0.2.1-alpha.0 <0.3.0-0 枚举段后两种语义一致放行。该版本仅有契约层证据,未做真机启动实测。
0.2.0-rc.1 的核验口径(2026-09-29 增补):基准取 npm 上真实发布的 0.1.7-rc.2(升级前本机真正在跑的版本),逐文件 SHA256 与 0.2.0-rc.1 比对。本插件的 6 个耦合点里 6 个逐字节未变(dsh-system-prompt 4/4、dsh-client-ui-slots 4/4、-renderer 12/12、-settings 11/11、dsh-host-webserver 3/3、dsh-client-modules 10/10);变动的只有 dsh-agent-loop(注入通路 preStep / assemble 逐行等同)与 dsh-app-boot(兼容门禁函数逐字节等同)。方法带阴性对照:同法比对 0.1.6-alpha.2 得到的哈希不同,证明这套比对确实能检出变化,而不是恒真。
一个运维上值得知道的事实。 早期 DSH 用 caret 范围声明同族依赖(如 ^0.1.5-alpha.2),范围会随时间"往前漂":今天安装旧版 DSH,npm 在 includePrerelease 语义下会把同族子包解析到当前最新的 0.1.x,而不是当年那份(实测 7/10 个采样版本会漂移,3 个因精确锁定不会)。这不影响本插件的结论——漂移目标同样在已核验范围内——但"今天装出来的树"与"当年装出来的树"可能不同。
为什么写成分段形式:node-semver 规定"候选版本带预发布标签时,区间里必须存在同一个
major.minor.patch 且自身带预发布标签的比较符"。这条规则是集合级的——一个 || 分支最多
覆盖一个 minor 的预发布版本。由此产生两个必须遵守的约束:
- 宽写法接不住预发布:
>=0.1.3-alpha.2、>=0.1.3-alpha.2 <0.2.0-0都匹配不到0.1.4-0/0.1.5-rc.1/0.1.6-alpha.2——因为0.1.3与0.2.0的 tuple 都不等于 它们各自的 tuple(已用 node-semver 7.8.5 实测确认)。 - 每段的上界不能省:省略会被上一段的比较符"续接"。例如单写
>=0.1.5-alpha.1(不封顶), 在把0.1.3当 token 时会放行0.1.3-alpha.0/1。
⚠️ 一个必须知道的上限(注意它是"模式相关"的,别当成普适结论):未声明的未来 minor 的预发布版本,在 npm 的默认语义下无法被任何范围覆盖。
0.1.8-rc.1 这类版本 tuple 与所有比较符都不同,在 npm 默认语义下永远匹配不到——与
^0.1.0 无法匹配 0.1.8-rc.1 是同一个限制。正式版(0.1.8、0.2.0、1.0.0 等)不受影响。
这个限制不再只是假设:0.2.1-alpha.1(2026-10-03 发布)就是第一个真实撞上它的版本,
1.0.16 已通过新增 0.2.1 枚举段闭合;下一个未声明 tuple(如 0.2.2-rc.1、0.3.0-rc.1)
仍会重演同一现象,需要发一版加一行——host-range 的实时守卫会在本机升级到该宿主时
直接让测试失败并给出可直接套用的修复行。
但在 includePrerelease: true 下这条上限不成立。 实测同一串范围:0.1.8-rc.1 在默认语义下 false、
在 includePrerelease: true 下 true(已用 DSH 自带的 semver 7.8.5 验证)。而这个区别是有后果的,
因为两条消费路径用的模式不同:
| 消费方 | 模式 | 0.1.8-rc.1 会被放行吗 |
|---|---|---|
npm / pnpm 解析 peerDependencies(包管理器) |
默认 | 否 |
宿主门禁 evaluatePluginCompatibility |
includePrerelease: true(dsh-app-boot L300) |
是 |
| 插件市场(第三方,服务端) | includePrerelease: true |
是 |
也就是说:对包管理器而言那个上限成立,对宿主门禁而言它其实已经被宽松模式接住了。
所以"未声明的新 minor 预发布版会不会被拒"不能只看一句话,要问清楚是哪种模式。
(host-range-test.mjs 里那条 0.1.8-rc.1 is unreachable 的断言取的是默认模式,其用例名
因此容易被误读成普适结论——这一点已在 1.0.14 的核验里记录。)
这个限制无法用代码消除,但它的后果已经被消掉了。 范围现在是生成出来的:
host-range-test.mjs 顶部的 COVERED_LINES 是唯一事实来源,package.json 里那串长字符串必须
逐字节等于由它生成的结果(有专门的漂移断言),所以手改长串导致"枚举漏了一段"这类错误不可能
再发生——DSH 发新 minor 时只需在列表末尾加一行。
同时实时守卫(直接读本机实际安装的宿主版本)在发现未覆盖的宿主时,会打印精确到行的 修复配方,并且在打印前先验证该配方真的能让那个版本被放行:
FAIL LIVE GUARD: the installed host 0.1.8-rc.1 is admitted by both modes -> ...
Fix: append { floor: '0.1.8-alpha.0', below: '0.2.0-0' } to COVERED_LINES
and set the preceding line's below to '0.1.8-0'
[verified: that edit admits 0.1.8-rc.1]
这份配方本身也有测试:host-range-test.mjs 会拿 0.1.8-rc.1 走一遍"确认当前范围拒收 →
套用配方 → 确认放行 → 确认下方版本仍被拒收 → 确认所有已覆盖版本仍被放行"。配方失效会先失败在
测试里,而不是失败在用户的安装上。
历史修复说明(同一个 bug 出现过两次,值得记下来):
- 1.0.7 及以前,范围末段是
>=0.1.5-alpha.1,使宿主0.1.6-alpha.2在 npm 默认语义下 被判为不兼容(预发布门禁要求 tuple 相同:0.1.5 ≠ 0.1.6,>=的大小比较根本走不到)。 - 1.0.8 改为逐段枚举,但只枚举到
0.1.6。DSH 随后发布0.1.7-rc.2,同一个假阴性再次出现: 范围里最后一个预发布 tuple 是0.1.6,而0.1.7 ≠ 0.1.6。1.0.9 补上0.1.7段。 - 1.0.10 把枚举抽成
COVERED_LINES单一事实来源 + 漂移/可达/递增断言,并让实时守卫给出 自验证的修复配方——这个 bug 的成因(手改长串)从此被消掉。
两次都因为插件市场传入 includePrerelease: true 而侥幸放行,但任何标准 semver 判定
(npm / pnpm 的依赖解析)都会拒绝。现在 host-range-test.mjs 同时具备固定矩阵与实时守卫。
未验证范围:macOS / Linux、headless 与 tui profile,以及除上表等级①那几个之外的任何版本的真实启动,均未实测。
等级②只证明"该版本里有这些 API",不证明端到端可用。
关于"市场读哪个字段"这条的证据等级。 这里说的是**插件市场(第三方)**的行为,不是 dsh 自身:
市场从已发布的 npm manifest 读 engines.dsh(顶层优先)或 dsh.engines.dsh,与 @deepseek-ai/dsh* 的
peerDependencies(若有)求交,再用 semver + includePrerelease: true 与宿主版本比较,据此在卡片上
显示兼容性;dsh.compatibility.dsh 不是市场读取的那个字段。
需要如实说明它的来源等级:这一条在官方文档里没有,树内也没有可核验的市场客户端
(dshmarket / alldsh 均为第三方,读取发生在服务端)。它的依据是一份独立的第三方实证记录
——本机安装的 dsh-vibe-math@2.3.16 在其 dsh.compatNote 里逐字写明了上述读取路径与"自用的
dsh.compatibility.dshReleases 市场不读"。所以请把它当作第三方实证,不要当成官方保证。
(1.0.14 之前这段把它与官方事实并列陈述,措辞已按证据分级修正。)
范围之外的宿主会发生什么(1.0.14 起变了)。 在此之前,本插件没有 peerDependencies,宿主门禁
直接跳过检查,范围外宿主可以装上且没有警告。1.0.14 起 peer 生效:
- 宿主(
dsh plugin add/ profile 启动)会拒绝,除非用户显式授予该精确版本的豁免 (dsh plugin allow-version),这正是 DSH 设计的正常流程; - 市场会把"确认不兼容"的条目隐藏并拒绝安装/更新("未声明"或"无法确认"的条目照常显示)。
host-range-test.mjs 会断言 peerDependencies 与另外两处字段逐字节一致,并断言 peer 只声明这一个包名——
三处任一被手改都会在测试里失败。
发布(维护者)
正规流程是推 tag,由 CI 通过 OIDC(Trusted Publishing)发布,不需要任何长期令牌:
# 1. 自检(prepublishOnly 也会自动跑;占位符未替换会中止发布)
npm test
node check-metadata.mjs
# 2. 升版本并提交
# package.json 的 version 必须与 tag 完全一致,否则工作流会拒绝发布
git commit -am "1.0.N: ..."
git push origin main
# 3. 打 tag 推送 → 触发 .github/workflows/publish.yml
git tag -a v1.0.N -m "1.0.N: ..."
git push origin v1.0.N
工作流会先跑 npm test 与"tag ↔ version 一致"校验,再 npm publish --access public;
公开仓库 + OIDC 会自动附带 provenance 存证。
npm 侧的一次性配置(发布前必须完成,且 npm 不会校验你填了什么): Package → Settings → Trusted publishing → Add trusted publisher
| 字段 | 值 |
|---|---|
| Organization or user | HERO476 |
| Repository | dsh-instruction-memory |
| Workflow filename | publish.yml(只写文件名,须完全一致) |
| Environment name | (留空) |
| Allowed actions | 勾选 npm publish |
⚠️ 2026-09-03 之后创建的 trusted publisher 默认只允许
npm stage publish,直发必须显式勾选。 字段填错不会在保存时报错,只会在第一次发布尝试时以ENEEDAUTH之类的错误暴露。
这就是本仓库 1.0.10 / 1.0.11 / 1.0.12 由本地 npm publish 发布的原因:这三版发布时该配置尚未就绪,
Publish 工作流的唯一失败步骤正是 npm publish(其余步骤含 npm test 全部通过),因此那三版没有 provenance、也没有对应的 git tag。
不要为这三个版本补推 tag —— 版本已被占用,重跑只会再得到一次必红的 CI。从下一个新版本起走上面的正规流程。
本地兜底发布(仅在 OIDC 不可用时):npm login 后直接 npm publish --access public。
本机若已有 ~/.npmrc 令牌即可直接发布,但产物不含 provenance。
数据格式
版本 2(当前写入的格式;版本 1 的文件同样能载入,见上文「存储」):
{
"$schema": "./memory.schema.json",
"version": 2,
"rev": 3,
"enabled": true,
"disabledUntil": 0,
"budgetChars": 4000,
"backupDir": "",
"activeSetId": "st-default",
"sets": [
{ "id": "st-default", "name": "默认" },
{ "id": "st-work", "name": "工作" }
],
"entries": [
{
"id": "im-xxxx",
"setId": "st-default",
"title": "回答语言",
"content": "所有回答使用简体中文。",
"mode": "always",
"when": "",
"priority": 1,
"enabled": true,
"tags": ["沟通"],
"updatedAt": 1700000000000
}
],
"profiles": [
{
"id": "pf-xxxx",
"setId": "st-default",
"name": "职业背景",
"content": "有 8 年后端开发经验,技术问题可跳过入门概念。",
"enabled": true,
"updatedAt": 1700000000000
}
]
}
mode 为 always | auto;priority 为 2 高 / 1 普通 / 0 低。rev 是存储修订号,
由插件在每次成功写入时递增,无需手填(缺失视为 0)。tags 为可选的短标签字符串数组
(最多 8 个、单个不超过 24 字符,仅用于设置页筛选,不参与注入);缺失或非数组一律按无标签处理,
非法项会被剔除、超长会被截断、重复项会去重。
sets 为配置集列表(每项只有 id 与 name,单行不超过 30 字符;最多 20 个),
activeSetId 指向当前激活的那一个——注入只渲染它。每条 entries / profiles 用
setId 声明归属:指向不存在的集合时归入当前激活集合(并在设置页披露);留空视为"跟随当前
激活集合"。缺失 sets 字段即版本 1 文件,载入时会合成一个名为「默认」的集合。
backupDir 为本机的自定义备份位置(留空 = 与存储文件同目录,最多 512 字符);它不是
可移植内容,从备份恢复时不会被改写。
disabledUntil 为临时静音(F7)的到期时刻(epoch 毫秒):0 或缺失 = 未暂停;未来值 =
暂停中,到期由插件自动恢复(无需操作、不写盘、rev 不变),跨重启保留。手动切换总开关会
清零它(显式开关优先于定时)。这是格式版本 2 的可选字段:旧文件缺失即未暂停,不触发
迁移;手改成非法值(非数字/负数)会被重置为 0 并在设置页披露。
profiles 为可选的用户形象数组(缺失按空数组处理):name 单行不超过 40 字符,
content 不超过 4000 字符,最多 50 个;enabled 控制是否使用该形象。名称与内容同时为空的形象会在
载入时剔除并明确披露;名称为空时自动取内容前 20 字符。
上限一览(memory.schema.json 与 verify.mjs 保证它和代码一致):
指令最多 200 条(全部配置集合计)/ 标题 120 字符 / 内容 6000 字符 / 适用场景 200 字符 /
每条最多 8 个标签、单个 24 字符;用户形象最多 50 个 / 名称 40 字符 / 内容 4000 字符;
配置集最多 20 个 / 名称 30 字符;注入字数上限 800–40000 字符。
Links
More in this category
vectorize-io/hindsight#coding-agents★ 54464
Hindsight, agent memory that learns: long-term project memory with auto recall and retain, knowledge pages, deep reflection, and per-repo memory banks.
volcengine/OpenViking#examples/dsh-memory-plugin★ 40976
OpenViking memory and context bundle for DeepSeek Harness: pre-step auto-recall and profile injection, session capture, `viking://` URI guarding, and recall/write memory tools backed by an OpenViking server.
MemTensor/MemOS#memos-local-plugin★ 12136
Local-first memory plugin for DeepSeek Harness: layered L1/L2/L3 memory with automatic capture, manual memos_* retrieval tools, and a built-in memory viewer.
agentscope-ai/ReMe#dsh★ 3702
Connects DeepSeek Harness to ReMe's local-first, self-evolving personal knowledge base: automatically captures completed main-agent conversations as user-owned Markdown memory, searches conversations and source material through reme_search with BM25, optional embeddings, and wikilink expansion, and schedules daily memory consolidation.
zilliztech/memsearch#MemSearch★ 2802
Shared Markdown memory for DSH and other coding agents, with automatic capture, pre-step context injection, searchable recall, and memory-to-skill self-evolution through a review panel.
vshulcz/deja-vu#extensions/dsh★ 1321
Reads the session files forty-two other coding agents on this machine already wrote (Claude Code, Codex, Cursor, VS Code Copilot Chat, opencode, OpenClaw, Hermes, Kimi, Cline, Zed and more), including sessions from before it was installed: six tools (deja_recall, deja_session, deja_blame, deja_fix, deja_how, deja_remember), a /deja command, and optional automatic recall added to the runtime context. Local BM25 index, no LLM, no embeddings, no network (dsh plugin --profile web add dsh-deja).
Community comments
Comments are public GitHub Discussions. Loading them connects to GitHub and Giscus; a GitHub account is required to post.