dsh-gulu-diary 🐋📔
一个跑在 DSH(DeepSeek Harness)里的独立情感日记插件:每天在自己设定的日界(默认 04:00)之后读一个「04:00 → 次日 04:00」窗口里的会话,先判断这天值不值得写,值得才用完整人格 + 只读记忆写一篇给自己看的日记,存进独立的 diary.db,并导出一份可以直接双击打开的 Markdown。
它不创建、不恢复、不 steer 任何会话;写日记是一次独立的模型调用。
当前版本:0.9.1 | Node ≥ 24 | MIT
它做什么
- 按窗口采素材:把一天的会话转录(
session.v4.jsonl.zstd)解出来,按时间戳逐条过滤,只留04:00 → 次日 04:00的内容。 - 先判定,再动笔:用一次很短的模型调用回答 YES / NO / WAIT。判 NO 就只留一条判定和理由,不生成正文;窗口没结束不判;说话太少不调模型。
- 三段式记忆注入:先宽召回候选,再让模型挑真正和这天有关的几条,最后动笔前先让模型把那天想明白(四小节,内部留档)。
- 独立存储:日记写在自己的
diary.db,正文另存日记/YYYY-MM-DD · 标题.md;和记忆库完全分离。 - 可审计:每次生成的素材、信号、两段提示词、模型原始返回、记忆挑选记录、场景理解都留在
pending/<日期>/,生成日志另记一份。 - 素材自动清理:
pending/<日期>/默认保留 7 天,到期只删素材目录,不碰日记正文和.md。 - 设置页:DSH 设置里的「日记本」可以按日期阅读、切模型、看待写日期和本次注入了几条记忆。
它不做什么
- 不创建 / 恢复 / steer 会话,请求里不带
sessionId。 - 不写记忆库:
memory.db一律readOnly: true+PRAGMA query_only打开,读完即关。 - 不联网上传日记,不发消息,不做云同步。
- 不保证每天一篇——「不值得写」是这个插件的正常结论之一。
工作链路
DSH 启动 / 每 15 分钟
→ prepareDue:日界后缺哪天 → 采素材 + 拼基础提示词 + 压一份 ≤2000 字判定信号
→ judgeDay:只在窗口关闭后判;说话 < 3 句直接跳过,不调模型
→ WAIT:素材像半截,不记判定,下个 tick 再问(连续两次后要求给结论)
→ NO:写判定结果,素材留着,不写正文
→ YES:
loadPersona(完整读取人格文件,不改写、不概括)
recallMemoryCandidates(只读 memory.db,soul 全收 + 关键词/项目名/标题打分,上限 60)
selectMemoriesWithModel(素材摘要 + 候选清单 → 挑 5–12 条)
buildUnderstanding(素材 + 挑中的记忆 → 四小节场景理解,内部留档)
buildGroundedPrompt(system = 人格 + 挑中的记忆;user = 写法 + 素材 + 场景理解)
ctx.llm.stream(独立调用,不带 sessionId)
校验 frontmatter / 日期 / 视角
→ diary.db + 日记/YYYY-MM-DD · 标题.md + pending/<date>/ 留档
→ 每个 tick 末尾:清理超过 retentionDays 天的 pending 素材
每一天只判一次:判定结果落在 diary_verdicts,下个 tick 甚至重启之后都直接复用。想重判用 CLI 的 judge --force。
两个降级,不挡成篇
- 记忆挑选失败(超时、读不懂、id 全对不上)→ 降级成「soul 全部 + importance 最高的 3 条 user」。
- 场景理解失败或四小节不齐 → 丢掉它,直接拿素材写。
两种降级都会写进 generation.log 和 pending/<date>/meta.json 的 degradedSteps,设置页详情能看到。
安装
1. 依赖
- Node ≥ 24:用到
node:sqlite、node:zlib的 zstd 解码。 - 一个能跑 DSH 插件的 profile,并具备
llm、tools、webServer、settings这几项服务。 - 可读的会话转录目录(
sessionsDir)和人格文件(persona.path,默认~/.dsh/AGENT.md)。 - 一个 meow-memory 风格的
memory.db(见下面「记忆库约定」)。
2. 放进 profile
手动三步:
- profile 的
package.json:dependencies 里加"dsh-gulu-diary": "link:<这个目录的绝对路径>",并把dsh-gulu-diary加进dsh.profile.bundles。 - profile 的
cordis.patch.yml:加一条 entry(可以参考仓库根目录的cordis.patch.yml)。 - 在 profile 的
node_modules/里建一个指向本目录的链接(Windows 下是 junction)。
或者用脚本(会先备份 profile 的 package.json / cordis.patch.yml,再写配置、建链接、检查 BOM):
node scripts/install-profile.mjs --profile "$HOME/.dsh/profiles/desktop" --package-path .
脚本不会重启 DSH,也不会碰 memory.db。
3. 配置
参考仓库根目录的 cordis.patch.yml。最小可用配置:
- insert:
- id: dsh-gulu-diary
name: 'dsh-gulu-diary'
config:
enabled: true
dataDir: '~/.dsh-gulu-diary'
sessionsDir: '~/.dsh/sessions'
windowStartHour: 4
persona:
path: '~/.dsh/AGENT.md'
memory:
enabled: true
dbPath: '~/DSH-Memory/memory.db'
schedule:
enabled: true
~会展开成当前用户的家目录。配置里支持相对路径,最终都会解析成绝对路径。
4. 重启并确认
inject、工具、路由都是模块级注册,必须重启一次 DSH。重启后看 <dataDir>/plugin-load.log:
- 有
apply.done; - 有
routes.registered且failed为空; - 工具列表里出现
diary_*。
再刷新 DSH 设置 → 日记本,能看到库路径和篇数就说明 Host 和前端都挂上了。
配置项
| 键 | 默认 | 说明 |
|---|---|---|
enabled |
true |
总开关,关掉后不注册工具/路由/调度 |
dataDir |
~/.dsh-gulu-diary |
日记库目录(diary.db、pending/、日记/、日志都在这里) |
sessionsDir |
~/.dsh/sessions |
DSH 会话转录根目录,只读 |
windowStartHour |
4 |
一天从几点开始算(0–23),默认凌晨 4 点 |
persona.path |
~/.dsh/AGENT.md |
人格文件,整篇原样进 system prompt |
memory.enabled |
true |
关掉会拒绝生成(不允许裸模型写日记) |
memory.dbPath |
~/DSH-Memory/memory.db |
记忆库路径;也可用环境变量 DSH_DIARY_MEMORY_DB 指定 |
memory.maxEntries |
20 |
最终注入的记忆条数上限 |
memory.maxChars |
6000 |
最终注入的记忆总字数上限 |
memory.recallLimit |
60 |
第一段召回候选的上限,只求不漏 |
memory.pickMin / pickMax |
5 / 12 |
第二段让模型挑几条 |
memory.pickerMaxTokens |
1200 |
挑选那一次调用的 token 上限 |
memory.pickerTimeoutMs |
120000 |
挑选超时 |
understanding.maxTokens |
900 |
场景理解的 token 上限 |
understanding.timeoutMs |
180000 |
场景理解超时 |
schedule.enabled |
false |
定时器;cordis.patch.yml 里默认开成 true |
schedule.checkMinutes |
15 |
多久检查一次缺哪天 |
schedule.maxCatchUpDays |
7 |
关机期间最多补几天 |
generation.enabled |
true |
关掉后仍准备素材,但不调模型、不判定 |
generation.provider / model |
留空 | 留空 = 跟随 DSH 当前主模型;填的话两个都要填 |
generation.reasoningEffort |
留空 | 可选推理档 |
generation.maxTokens |
4096 |
正文最大 token |
generation.timeoutMs |
180000 |
正文超时 |
generation.retries |
2 |
失败重试次数 |
judge.minUserMessages |
3 |
当天用户说话少于这个数就不调模型,直接跳过 |
judge.maxSignalChars |
2000 |
判定信号硬上限 |
judge.maxTokens |
400 |
判定回答很短,给少一点 |
judge.timeoutMs |
60000 |
判定超时 |
retentionDays |
7 |
pending 素材保留天数;0 = 不自动删 |
CLI
不走 DSH 进程也能核对素材、调判定标准、手动补写。所有命令默认只写 --data-dir 和必要的 output/,不碰 memory.db。
$NODE = "node"
$DATA = "$HOME/.dsh-gulu-diary"
$SESS = "$HOME/.dsh/sessions"
# 采素材(只读转录,不写日记)
& $NODE src/cli.js collect --date 2026-11-01 --sessions-dir $SESS --print-prompt
# 把素材和提示词放进 pending/
& $NODE src/cli.js prepare --date 2026-11-01 --sessions-dir $SESS --data-dir $DATA
# 只判定,不写正文;--force 忽略已有判定重判
& $NODE src/cli.js judge --date 2026-11-01 --force --data-dir $DATA --sessions-dir $SESS `
--base-url http://127.0.0.1:8080/v1 --model your-model --api-style chat
# 手动写一篇:先判定,判 skip 就不写;--force 无视判定强写
& $NODE src/cli.js generate --date 2026-11-01 --data-dir $DATA --sessions-dir $SESS `
--base-url http://127.0.0.1:8080/v1 --model your-model
# 只跑记忆筛选,打印挑中/丢掉和原因(不写日记、不写 pending)
& $NODE src/cli.js select --date 2026-11-01 --data-dir $DATA --memory-db "$HOME/DSH-Memory/memory.db"
# 素材保留期:先干跑,再真删(只动 pending/<日期>/)
& $NODE src/cli.js cleanup --dry-run --data-dir $DATA
& $NODE src/cli.js cleanup --days 7 --data-dir $DATA
# 其他
& $NODE src/cli.js pending --data-dir $DATA
& $NODE src/cli.js list --data-dir $DATA
& $NODE src/cli.js read 2026-11-01 --data-dir $DATA
& $NODE src/cli.js search 关键词 --data-dir $DATA
& $NODE src/cli.js hooks --data-dir $DATA
& $NODE src/cli.js plan --now 2026-11-05T10:00:00+08:00
& $NODE src/cli.js doctor --data-dir $DATA
judge / generate / select 都需要一个 OpenAI 兼容端点(--base-url、--model、--api-key、--api-style chat|responses);也可以在环境变量里给 DSH_DIARY_JUDGE_URL / DSH_DIARY_JUDGE_MODEL / DSH_DIARY_JUDGE_API_KEY。
设置界面「日记本」
重启后出现在 设置 → 日记本(client 端的 settings.section)。只读为主:
- 顶部:库路径、
日记/路径、总篇数、素材保留天数。 - 写日记用的是谁:默认「跟随 DSH 当前主模型」,也可以按 provider 挑具体模型和推理档;选中即写回 profile 配置,热生效。
- 列表:按日期倒序,只取元数据,不点开不加载正文。
- 被跳过的日子:列表里一行淡字(
11-03 无值得一提的事 · 理由),不会被误看成漏写。 - 详情:按标题 / 段落 / 引用渲染;有 pending 时顶部可以「现在生成一篇」。
- 日记正文只读,不提供编辑/删除,也不自动打开资源管理器。
HTTP 接口(列表/详情只读;生成和模型切换是显式写操作):
GET /gulu-diary/api/list
GET /gulu-diary/api/detail?date=YYYY-MM-DD
GET /gulu-diary/api/pending
POST /gulu-diary/api/generate {"date":"YYYY-MM-DD"}
GET /gulu-diary/api/models
GET /gulu-diary/api/model-info?provider=...&model=...
POST /gulu-diary/api/model {"mode":"follow"} 或 {"mode":"explicit", ...}
GET /gulu-diary/api/health
list 不返回 content,只带 pending 和 skipped;detail 才返回全文。模型接口只接受 ctx.llm 当前目录里真实存在的组合。
数据在哪
<dataDir>/
├── diary.db 日记库(独立 SQLite,不是记忆库)
├── 日记/
│ └── YYYY-MM-DD · 标题.md 可直接双击阅读的正文
├── pending/<日期>/ 素材与留档,默认保留 7 天
│ ├── materials.md 素材原文
│ ├── signal.md 判定用的 ≤2000 字信号
│ ├── prompt.md 基础写作提示词
│ ├── prompt.system.md 实际注入的人格 + 记忆
│ ├── prompt.user.md 实际注入的写法 + 素材
│ ├── prompt.grounded.md system + user 合并
│ ├── response.md 模型原始返回
│ ├── understanding.md 动笔前的四小节场景理解(内部)
│ ├── memory-selection.json 候选 / 挑中 / 丢掉 + 理由
│ ├── attempts.json 重试记录
│ ├── error.json 终态错误
│ └── meta.json status / diaryId / 模型 / 各种 hash
├── generation.log 每次判定与生成
└── plugin-load.log 插件加载与路由注册
diary.db 里除了日记本身,还存了生成记录(人格文本与 hash、注入的记忆 id、候选/挑中/丢掉、两段提示词 hash、模型原始返回 hash、是否降级),用来回答「这篇到底是怎么写出来的」。
记忆库约定
插件只读,不 import meow-memory,也不调用任何写方法。需要的是一份 meow-memory 结构的 SQLite:
| 表 | 需要读的列 |
|---|---|
soul |
id、title、content、importance、keywords、status、created_at |
user |
同上 |
project |
同上,另加 project、subcategory |
只取 status = 'active' 且 created_at <= 当天窗口结束 的条目。没有 soul 条目、或者 memory.db 里一条 active 记忆都没有时,生成会明确报错而不是悄悄写一篇没有记忆的日记。
不想用记忆注入的话,别把 memory.enabled 设成 false——那会直接拒绝生成。正确做法是准备一个只有一条 soul 的最小库,或者改代码。
提示词
prompts/ 下四个文件都是纯文本,改提示词不用改代码:
| 文件 | 用在哪 |
|---|---|
diary-zh.md |
写正文 |
judge-zh.md |
判「值不值得写」 |
memory-picker-zh.md |
从候选里挑记忆 |
understanding-zh.md |
动笔前的四小节场景理解 |
它们默认是中文、并且带着一套具体的人设(咕噜 / 主人)。人设来自你自己的 AGENT.md,代码不硬编码称呼;把这四个文件换成你自己的人设和口吻即可。
隐私与隔离
- 会话转录只读,不复制进仓库;测试夹具全是合成数据。
memory.db只读打开,读取前后会记文件 hash,生成记录里如实写memoryUnchanged。- 素材清理只删
<dataDir>/pending/<YYYY-MM-DD>/:目录名必须是真实日期、解析后必须仍在 pending 根下、符号链接跳过;diary.db、日记/*.md、memory.db不在删除逻辑的任何分支里。 .md文件名会过滤 Windows 非法字符,中文标题保留。- 生成失败保留 pending 和错误日志,不写空日记、不写「占位」正文。
- 视角自检:正文里出现「用户说」「作为 AI」「第三人称提自己」这类口径错位会重试一次;仍然不对就标记
needs-review,不写库,等人工看。
测试
npm test # 等价于 node --test --test-isolation=none
当前 132 项,全过(Node 24.21.0)。覆盖采集窗口与 04:00 日界、判定闸门与 WAIT、信号压缩、三段记忆链路、降级路径、场景理解四小节、保留期删除边界、HTTP 路由、CLI 端到端、Zstd 多帧解码、插件配置与工具接口。
已知限制
- 必须有记忆库:设计上不允许「没有记忆的裸模型」写日记,缺库/空库会报错。
- 只在 DSH 里验证过:插件依赖 DSH 的
ctx.llm、tools、webServer和settings.slot;DSH 改接口签名需要跟着改。 - 安装脚本偏 Windows:
install-profile.mjs用 junction;核心逻辑是普通 Node,跨平台,但安装那一步在 Windows 上最顺。 - 提示词是中文的:想写别的语言,需要自己换四个 prompt 和校验规则。
- 成本:一次成功生成 = 判定 + 记忆挑选 + 场景理解 + 正文,共四次模型调用;判定为 NO 时只有一次。端点慢的时候挑选/理解会降级,不会挡住成篇。
- 定时器只在 DSH 运行时工作:关机期间漏掉的日期,启动后按
maxCatchUpDays补,补写同样先判定再写。
许可
MIT。见 LICENSE。
No comments yet. Be the first to write one.