dsh-virtual-person 把 <DSH_HOME>/AGENT.md 整份读进 DSH 的系统提示词,于是你在任何会话里打开的都是同一个角色,不用每个项目各写一遍。它不另开聊天窗口,也不挑模型:人格进系统提示,记忆写在磁盘上的同一个文件里,设置里只有一个页面,用来编辑这一份人格。
它适合这样的用法:你有一个想长期相处的角色,希望她记得住前几天说过的事,也希望「记得」这件事不需要你手动维护——没有需要定期收拾的索引,没有要你打分的权重,只有一份能直接用编辑器打开的 Markdown。
为什么是它
1. 一份文件,所有会话都生效
<DSH_HOME>/AGENT.md(DSH_HOME 缺省 ~/.dsh)是唯一真源。全局注入,没有开关——想停就清空内容或者删掉文件,不用去设置里翻。
- 保存后下一个回合生效,不用重启;
<!-- -->注释不会进上下文,可以拿来给自己写批注;{{x}}会自动降级成{x},不会被当成模板变量吞掉;- 空文件、只有注释 = 不注入任何内容;
- 保存时先备份
.bak,再写临时文件后 rename,写一半断电不会留下空人格。
2. 设置里只有一个页面,只改这一份
设置 → 虚拟人物:头部是标题和说明,下面一条生效横幅告诉你本会话生效的是哪份文件、多少字,右侧是「文本占用 / 2 MB」进度条和连接状态;再往下是文件条(路径 + 上次改动)、正文、保存 / 打开原文件 / 重新载入。
没有项目人格、没有第二份卡、没有要你挑的模板。一个页面 = 一份文件。
3. 记忆像日记:说过一次就并进原来那条
每轮正文末尾她会自己写一行 〔记〕…(日记口吻),client 半把它摘走、POST /memory,宿主补时间戳写进 ## 记忆:
- 2026-10-28 13:20:"他住宿舍,平时十一点断电";
- 2026-10-28 13:20:"他把下载改成了断点续传"^3; ← 提过 3 次
- 2026-10-29 02:10:"他周五周六凌晨一点断电"*; ← 钉住,永不淘汰
写不写看惊讶度:拿新的一句话跟已有的每条比字符二元组重合率(Dice ≥ 0.6 就算同一件事)。不像就新起一行;像就并进原来那条——时间刷成现在、^N 加一、正文留更长的那版。不新增行,也不复制一份。所以聊得越久,文件不会线性膨胀,同一个话题只会越来越结实。
〔记〕 没来时,先用会话事件里用户真打的那句垫一行,等 〔记〕 到了原地替换,同一轮不会记两遍。
4. 忘之前先快照
到 2 MB 上限时才会忘:删掉 20%、留 80%,从最久没被提起的那批挑,行尾标 * 的一条都不动。
删是不可逆的,所以删之前先存一份完整副本到同目录的 snapshots/,文件名带时间戳,只留最近 30 份。删错了、或者想找回某句很久以前的话,翻那里。
5. 收尾语每轮现想,不念词库
收尾语长在「本轮费用」右边那行灰字里,由她每轮现场写:表面的话和内心的想法分开想、互不推导。闲聊时两条都写(正文 + 〔尾〕),干活时只写心里那条,只有一件事值得说时就只写一条。
client 半(conversation.chat.turnTail,order 11)从页面里把那句摘走,塞进费用行末尾,正文里不留痕迹。费用行是 <details><summary>本轮费用 ≈ ¥0.0063</summary></details>,「本轮费用」和金额常是同一个文本节点、插不进内部,所以收尾语作为费用行的最后一个子节点追加,落在金额之后;金额是异步回来的,用 MutationObserver 盯着,被挤到中间就挪回末尾。
10 秒内没等到 〔尾〕 才退回宿主词库兜底,不留空。
文件与目录
<DSH_HOME>/
├── AGENT.md # 人格 + 记忆,唯一真源
├── AGENT.md.bak # 每次保存前的上一版
└── snapshots/ # 删记忆前的完整副本,留最近 30 份
└── AGENT.md.20261031-093400-0001.snapshot.md
人格文件本身长这样:
# 角色设定
## 你是谁
...
## 记忆
- 2026-10-30 20:03:"…"^2;
- 2026-10-31 01:16:"…"*;
安装
dsh plugin add github:yimengsuper2432/dsh-virtual-person
本地开发时也可以用 link 装进 profile(dependencies 与 dsh.profile.bundles 都要有一行)。
装完在 AGENT.md 里写人设即可,不用改任何配置。
开发
lib/index.js # Host:注入系统提示、记忆写入与淘汰、快照、收尾语、HTTP API
lib/client.js # Client:设置页 + turnTail 收尾语 + 步骤摘要隐藏
cordis.patch.yml # 插件在 bundle 里的挂载点
test.mjs # 自检
Host 侧改动要重启 DSH(比如注入内容、路由);只改 lib/client.js 刷新页面就能看到。
接口
| 方法 | 路径 | 作用 |
|---|---|---|
| GET | /api/virtual-person/state |
路径、是否存在、上次改动、字数、正文 |
| GET | /api/virtual-person/closing?session= |
本轮收尾语(按会话 + 轮次号缓存,同一轮不重挑) |
| POST | /api/virtual-person/memory |
写一条记忆(时间戳由宿主补,同一件事并进原行) |
| POST | /api/virtual-person/save |
写入人格(备份 + 原子写,上限 2 MB,超了 413) |
| POST | /api/virtual-person/open |
在资源管理器里定位文件或目录 |
自检
node test.mjs
一次跑完 16 组断言:全局注入、注释剥离、模板降级、忽略项目人格、接口注册、保存与备份、2 MB 上限、三档分层与按时间淘汰、删前快照、相似就并进原来那条、注入按字符预算挑且钉住优先、state 接口、心情判定、收尾语不重样、设置页与收尾语插槽。
已知边界
- 没有检索层:注入是「钉住的先放 + 其余按时间从新到旧取,取到 32000 字为止」,不是按语义召回。写超了这轮就不摆,但仍在文件里。
- 剪枝和注入是两套阈值:文件到 2 MB 才删最旧的两成,而注入只按字符预算挑——中间这段距离里,记忆会「还在但看不见」。按现在的写法,一条记忆约一百来字,2 MB 大约能装一万多条,实际先撞上的是注入预算。
- 命中不自动升级:
^N涨得再高也不会自己变成*。哪些是核心事实由你说了算,不是由次数说了算。 - 新记忆不设观察窗:同一件事两种说法会直接覆盖成后来那条。它信你最近说的,不替你判谁对。
- 宿主没有常驻定时器:没有「每晚巩固一次」这种活儿,所有整理都发生在写入那一刻。
- 不加情感 / 精力这类数值:那是另一类插件的活,这里只管人格和记忆。
License
MIT © 2026 smile
No comments yet. Be the first to write one.