本机优先的记忆服务:memd(HTTP + SQLite)+ 两个 DSH 插件 + QQ 桥接 / 桌宠适配器。
目标只有一个:让 DSH 主会话、QQ 桥接、桌宠读同一份文本记忆,并且各自只看得到该看的部分。 记忆以 Markdown 和 append-only JSONL 落盘,索引随时可以从文件重建;不需要数据库服务,也不需要把记忆交给任何云。
- 协议:
/v1HTTP(JSON),OpenAPI 见packages/protocol/openapi.yaml - 接口:20 个
/v1路由,路由表是唯一契约源(packages/protocol/src/openapi.ts) - 测试:
pnpm test190 项(单测 / 集成 / 隐私矩阵 / 契约 / MCP / 插件) - DSH 版本:0.2.1-rc.2 / 0.2.1-rc.3 已验证(MCP server、两个插件的加载契约、
webServer.register、session/*RPC);其它版本未验证,升级后先跑node scripts\doctor.mjs - 运行时:工作区便携版 Node
..\tools\node\node.exe(pnpm fetch:node安装)。memd / pet-llm / 桥接都是常驻进程,跑 DSH 自带的node.exe会锁住安装目录,升级时安装器改名失败(WinError 32)。细节见../tools/README.md - 监听:默认只绑
127.0.0.1:8787 - 数据:
../memories/(Markdown +facts/*.jsonl),索引data/index.db可重建
一句话:Agent 每开一个新会话就从零开始,而记忆又不能全塞进上下文。
所以分成三条路径——常驻的少量事实、按需检索的长期证据、会自动过期的近期聊天:
| 形态 | 存在哪 | 怎么进入上下文 |
|---|---|---|
| 长期事实 / 文档 | memories/**/*.md、facts/*.jsonl |
检索到才给,/v1/search、/v1/recall、/v1/timeline |
| 短期聊天 | 同一份 JSONL,stage=short + TTL |
24 小时内可召回,过期自动失效 |
| 身份与受众 | memories/user/identity.md、memories/audiences/(可选) |
/v1/bootstrap 拉取(MCP 工具 memory_bootstrap),会话启动时给一次 |
| 最近记得的事 | 同一份 JSONL 的最近 8 条长期记忆 | 每次唤醒追加一次(/v1/recent,10 分钟缓存),补「召回按 query 命中、跑题就空」的盲区 |
一条命令装完(幂等;普通 PowerShell 窗口,~/.dsh 对沙箱只读):
powershell -ExecutionPolicy Bypass -File scripts\setup-all.ps1
# → 重启一次 DSH → 再跑 -Check 应该全绿依次做:构建 → MCP 注册 + root 令牌(存在且有效就复用)→ dsh-memory → dsh-memory-panel →(可选)看护任务 →(可选)语义检索 → 写安装戳 → 自检。
| 开关 | 作用 |
|---|---|
-WithAutostart |
顺带注册 memd-watchdog(每分钟)和 pet 的 pet-revive |
-Semantic |
下模型 + 打开 semantic.enabled + 回填向量 |
-Check |
只跑自检(= node scripts\doctor.mjs) |
-Uninstall |
反向卸载(MCP + 两个插件 + 任务) |
-SkipBuild |
复用现有构建 |
只起服务、不接 DSH 时:
pnpm start # memd → 127.0.0.1:8787
node packages\admin\dist\cli.js health没有 config.json 时,scripts/start.ps1 会从 config.example.json 复制一份。
┌──────────────── DSH 主会话 ────────────────┐
│ MCP stdio server(9 个工具,root 令牌) │
│ dsh-memory 插件:全会话消息 → /v1/ingest │
│ dsh-memory-panel:设置页 + 同源代理 │
└──────────────────┬─────────────────────────┘
QQ 桥接 ── 逐场景令牌 ──┐ │
桌宠 pet-llm ─ 独立令牌 ─┼── HTTP ───► memd(Fastify, 127.0.0.1:8787)
│ │
│ ┌────────┴────────┐
│ SQLite 索引 搜索/召回/时间线
│ (FTS5 + 可选 vec) 隐私过滤在 SQL 里
│ │
└────────► memories/(Markdown + JSONL,append-only)
- 索引是派生物:
data/index.db随时删除重建,备份里不含它。 - append-only:遗忘写 tombstone,不物理改写历史;索引重建时 tombstone 生效。
- 过滤在 SQL 层:群令牌的查询根本不会把 owner 或敏感行载入内存,不是取回后再删(
packages/server/src/visibility.ts)。 - 夜间整理:
supervisor.mjs每天config.sleep.at(默认 03:00)跑一次sleep-consolidate.mjs——它唤起一个sleep-consolidatorpreset 的 DSH 会话(同 workspace、同AGENTS.md,只有 read/glob/grep + MCP 记忆工具,没有 shell),把当天记忆交给它整理,写回走普通memory_write/memory_forget。
四段,按请求进入的顺序:
- 分词:中文按 bigram 切开,拉丁词与数字保留,
extractKeywords去停用词。 - 两趟 FTS:先 AND(精确),结果太弱再降级 OR;两趟都跑完后在 JS 里重排(bm25 不懂标题与新鲜度)。
- 两处修正:
- 长口语查询的覆盖度加分——整句口语(token > 6)AND 必然失败、OR 之后 bm25 偏爱短文本,所以按「高信号 token 命中比例」补刀(
COVERAGE_BOOST = 6.0,短查询不受影响)。 - 短期降权——返回前按
score × stage权重重排,searchShortWeight默认0.5,让长期记忆不被大量聊天碎片挤走。
- 长口语查询的覆盖度加分——整句口语(token > 6)AND 必然失败、OR 之后 bm25 偏爱短文本,所以按「高信号 token 命中比例」补刀(
- 语义(可选):本地
bge-small-zh-v1.5(int8 ONNX,512 维)写入 sqlite-vec 的vec0表,与 FTS RRF 融合(倒数排名,RRF_K = 60);alpha只是两路各贡献多少比例,不再拿裸分相加。
默认取 5 条、最多 10 条;searchShortWeight = 1 关闭降权。
加权模型(未实现):
docs/MEMORY-WEIGHTING.md定义了相关分之外的四个因子——重要性、紧迫性、使用强度、未闭合——以及它们如何互相影响。当前检索只有「相关 + stage 降权」,这些还没接。
对话 ──/v1/ingest──► short(TTL 24h,可召回延长,封顶 168h)
│ 到期:purge 只删索引,JSONL 保留原文
│ 整理(每天一次,见架构那节):agent 读当天 short + long,
│ 用 memory_write 新增 / supersedes 改写 / memory_forget 撤回
└──► long(写同 id 的 stage=long 行,reindex 后覆盖)
│
/v1/forget → tombstone(deleted:true)
│
归档(可选,默认关):更老月份压 gzip,索引透明读回
- 回顾:原来的
/v1/review→/v1/persist这条链已移除(2026-10-28)——短期转长期现在只有夜间整理一条路,review:pending也不再写。 - 夜间整理:见上面架构那节;
config.sleep.enabled默认关,开了之后每天一次,失败按retryMinutes退避,当天成功过就不再触发。 - 去重:同
scene + kind + 规范化文本 + audience + privacy + subject复用 id;转长期时按 CJK bigram / Jaccard ≥ 0.8 折叠。 subject:owner(默认)/self(我自己的判断)/other。去重键包含它,所以「我认为 X」和「他说 X」是两条。- 时间线:
/v1/timeline按月倒出,supersedes/superseded_by双向标出「改过主意」;满页返回next_before当翻页游标。
两条轴,没有第三种:
- audience:记忆住在哪个场景(
main、qq:owner、qq:group:<id>、qq:private:<id>、pet、shared)。 - 一个敏感位:
normal/personal/sensitive/secret。敏感记忆钉在sensitiveAudiences,读它需要令牌带sensitive:read。 secret:读写各要一个 scope(secret:read/secret:write),root 令牌两者兼备,audience 钉在secretAudiences(默认main)。群、第三人私聊、pet 的令牌都不带这两个 scope,读不到。它不是加密存储:明文落盘、进备份、append-only 不可删,写进去就永远在磁盘上。
令牌 = surface + scopes + scenes:
| 令牌 | scopes | scenes | 实际能读 |
|---|---|---|---|
dsh-desktop(MCP) |
* + sensitive:read |
main |
全库(含 secret) |
qq-owner |
* |
qq:owner |
全库(含 secret),但只能在该场景 |
pet |
main / qq:owner / pet |
三个场景 | 仅这三类 |
qq-group-* |
只含本群 | 本群 | 本群 |
qq-private-* |
只含该私聊 | 该私聊 | 该私聊 |
qq-disclose |
disclose:group |
qq:bridge |
一次披露的目标群 |
- scene 由客户端传,但必须落在令牌允许的范围内——模型不能自己扩权。
- 主人主动提起:桥接标记
from_owner后,该场景可临时读到主人自己的记忆;敏感、他人私聊、secret不放开。- 跨场景披露:群友私聊问「刚才群里聊了什么」,桥接先做实时成员校验(持久成员表 + 增删事件 + 每日对账 + fail-closed),通过后才用disclose:group读那一个群的chat/note、normal/personal;每次披露写审计。 - 审计:
data/audit.jsonl记录 route / token / 命中 ids / 拒绝数 / 耗时,30 天轮转。
| 方法 | 路径 | 说明 |
|---|---|---|
| GET | /v1/health |
存活 + 索引统计(唯一免鉴权) |
| GET | /v1/capabilities |
工具、限额、feature 开关 |
| GET | /v1/config-schema |
设置页 JSON Schema |
| GET | /v1/config-state |
读取可编辑配置(root) |
| POST | /v1/config |
更新白名单配置(root,热生效) |
| GET | /v1/tokens |
令牌元数据(root,不含 hash) |
| POST | /v1/tokens/revoke |
吊销一个令牌(root;签发仍在 CLI) |
| POST | /v1/bootstrap |
身份 + 受众脸 + 最近标题 + 最近 self |
| POST | /v1/search |
关键词检索(语义可用时 RRF 混合) |
| POST | /v1/get |
取一条(chunk id 或 doc id) |
| POST | /v1/recent |
最近记忆;stage:"long" 只取长期层;传 format:"block" 时多返一段已渲染的 block |
| POST | /v1/recall |
按最近对话召回,可带 live 侧通道;传 format:"block" 时多返一段已渲染的 block |
| POST | /v1/timeline |
时间线:按月分组、可分页 |
| POST | /v1/disclose |
跨场景披露:一个群的最近消息 |
| POST | /v1/write |
追加一条事实并索引 |
| POST | /v1/forget |
忘记一条(JSONL 写 tombstone) |
| POST | /v1/ingest |
普通聊天入短期记忆(带入口去噪) |
| POST | /v1/audience |
读受众卡 |
记忆块:/v1/recall 传 format:"block" 时,memd 直接返回一段能塞进 prompt 的文本(标题、时间、截断、(对方自己提起过的) 标记都在服务端做,纯函数在 packages/server/src/recall-format.ts):
【最近在聊的】(近 30 分钟,是现状,不是回忆)
- 21:40 ……
【长期记忆】(只是背景,不要原样复述)
- (对方自己提起过的)……
取数仍由各端决定(哪个 scene、k 多大、要不要 live),渲染由 memd 统一。
ID(2026-10-27 统一,packages/core/src/ids.ts):
事实 <source>-<scene>-<time36>-<rand4> 例:fact-qq-group-650866701-m8x2k-7f3a
文档 doc-<pathHash8> 例:doc-e8cc8701
scene里的:归一化成-(qq:group:123→qq-group-123),所以段数不固定。- 文档用路径 hash,重命名文件不换 id。
- 旧前缀
f_<epoch>_<hex>/chat:<scene>:<hash>/import:<scene>:<hash>/doc:<path>#<ord>由scripts/migrate-ids.mjs一次性迁移,脚本只处理这四种,已迁的会原样跳过。 - 幂等键不从 id 派生:ingest 写
source_message_id = <provider id>#<内容 hash 前 12 位>。QQ 会在同一会话里复用短 messageId,只用 id 当键会把「第二条不同的消息」当成重复丢掉。
MCP server(packages/adapter-dsh/dist/mcp-server.js,stdio)暴露 9 个工具:
memory_bootstrap · memory_search · memory_timeline · memory_get · memory_recent · memory_recall · memory_write · memory_forget · memory_call(兜底入口)。
memory_write 带 supersedes(替代旧记忆)、idempotency_key、source(来源标记,只有 root 能设;不传按场景记 main/qq,夜间整理传 sleep 便于整批回滚);privacy 枚举是 normal / personal / sensitive / secret(work、financial 作为旧别名按 sensitive 处理)。memory_forget 走 /v1/forget,写 tombstone。
两个插件:
plugins/dsh-memory:挂agent/pre-step,把用户消息 fire-and-forget 上报/v1/ingest;跳过桥接会话与pet-chat预设;memd 挂了也不能影响这一轮。plugins/dsh-memory-panel:设置页 +/memory-hub/*同源代理(绕开 Chromium 的 Local Network Access),宿主侧零配置注入 root 令牌;加载 8 秒后自动跑一次 doctor 写logs/doctor-boot.log。
qq-bridge/ 是 qq-bridge-enhanced 的二次开发,源码直接放在本仓库,有自己的 package.json,不参与 pnpm workspace。
- 令牌:
node scripts\issue-qq-tokens.mjs按场景签发(owner / 每个群 / 每个私聊 / disclose),写进qq-bridge/state/;配置里memory.enabled=true并映射tokens。 - 镜像:轻量记忆(
social-v2.json)写入时镜像到 memd;失败落logs/mirror-failures.jsonl,删除失败进state/forget-queue.jsonl,每 5 分钟自动重试。 - 对账:
qq-bridge/scripts/reconcile-memory.mjs只读打开index.db,分present / legacy / partial / missing / superseded五桶,只补不删;桥接内置定时器每天 17:30 跑一次(graceHours: 2),桶非 0 时给 owner 发一条只含计数与路径的私聊。 - 看护:memd 的
supervisor.mjs可选第 2 步——config.bridge.enabled为真时探活(401 也算活着),掉线跑qq-bridge\start-bridge.ps1,失败退避 10 分钟。
部署与配置见 qq-bridge/docs/guides/CONFIGURATION.md,源码同步用 scripts\sync-qq-bridge.ps1(-Apply 才写,-Restart 顺带重启)。
dsh-pet-configurable-llm(独立仓库)以 scene=pet 接入,持有自己的令牌(main + qq:owner + pet,非 root),每轮带 live:{k:6,minutes:30,scenes:['main']} 拉主会话最近的短期聊天,记忆块走 format:"block" 由 memd 渲染。它的失败不回落到普通对话模型——内部失败返回 200 + 降级文案,进程不在时由 pet-revive 每分钟补(这个任务独立于 memory-hub 的 supervisor,两个项目保持隔离)。
node scripts\doctor.mjs # 自检:核心 + 提示
node scripts\doctor.mjs --json # { ready, checks[] }
node scripts\doctor.mjs --shallow # 只查文件,不发请求- 重启后验证:插件
apply()写data/plugin-load/<name>.json,doctor 与data/setup-stamp.json比时间——「装了但没重启」显示FAIL … 需要重启 DSH。 - 看护:
memd-watchdog每分钟跑supervisor.mjs——探活并拉起 memd;config.bridge.enabled为真时盯桥接;metrics.csv超 60 分钟补采样;过了 04:00 且今天没备份就跑备份;config.sleep.enabled为真时按周期跑整理(日每天 / 周周一 / 月1 号 / 年1 月 1 日,都在config.sleep.at之后,失败按retryMinutes退避)。memory-hub 侧只有这一个看护任务;桌宠有自己的pet-revive(每 1 分钟,见它的仓库),两个项目保持隔离、互不管对方的进程。 - 备份:
pnpm backup每天一份到../backups/memory-hub/YYYY-MM-DD/(memories + tokens + config + manifest,不含 index.db),同日覆盖、keepDays清理。 - 监控:
pnpm monitor采样/v1/health到data/metrics.csv。 - 归档:默认关;开启后更老月份压成
facts/archive/*.jsonl.gz(回读校验后才删原文件)。 - 升级 DSH:
powershell -ExecutionPolicy Bypass -File scripts\post-upgrade.ps1检查 memd / doctor / pet-llm / 桥接四处,一处挂就红。 - 恢复:索引可重建——
pnpm build && node packages\admin\dist\cli.js reindex,tombstone 与过期规则都会在重建时重新生效。
pnpm test # 190 项
pnpm test:e2e # 真实 DSH 会话:建 minimal 会话 → 发标记 → 等 memd 收到(会调一次模型)- 单测:tokenize / chunk / frontmatter / policy / 令牌热重载 / 可见性 / RRF 融合 / 语义纯函数 / 面板冒烟 / 旧库补列
- 检索基线:
node scripts/verify-recall.mjs --baseline按 12 条固定查询比排名升降(--update重写,基线在data/recall-baseline.json);只报告不阻塞。 - 集成:临时目录 + 真 SQLite +
app.inject——recall 作用域、披露、配置热更新、live、timeline、subject、短期权重 - 隐私矩阵:群搜工作=0、群 get owner=404、私聊互不可见、伪造 scene=403、
secret只对带secret:read/root 的令牌可见、披露不串场景 - 契约:路由 ↔ OpenAPI 一致;MCP 工具(InMemoryTransport);DSH 插件
- 沙箱里跑
node --test需要--test-isolation=none(脚本里已带)
memory-hub/
packages/
protocol/ TypeBox schema → JSON Schema / OpenAPI,路由表(唯一契约源)
core/ tokenize(bigram)、chunk、frontmatter、policy(纯函数)
server/ Fastify 路由、SQLite FTS5 索引、鉴权、审计、可见性 SQL、语义索引
sdk/ 类型化 HTTP 客户端
adapter-dsh/ MCP stdio server + bootstrap 注入文本
adapter-qq/ 给 QQ 桥接用的零依赖客户端(编译成 dist/memory-client.mjs)
admin/ 令牌签发/吊销、reindex、health、social-v2 导入
tests/ unit / integration / privacy / contract / mcp / plugin
plugins/
dsh-memory/ 全会话消息 → /v1/ingest
dsh-memory-panel/ 设置页 + 同源代理
scripts/ setup-all · doctor · supervisor · backup · monitor · archive-facts
verify-recall · build-timeline · e2e-dsh-session · issue-qq-tokens
sync-qq-bridge · install-autostart · fetch-model · embed · set-semantic
sleep-consolidate · install-sleep-preset · migrate-ids
preset/ sleep-consolidator.patch.yml(夜间整理专用 preset)
docs/ RUNBOOK · PLAN · TODO · IMPLEMENTATION · SPIKE-DSH · QQ-BRIDGE-PATCH · HANDOFF
qq-bridge/ QQ 桥接(独立 package.json;MIT,见其 NOTICE.md)
| 文档 | 什么时候看 |
|---|---|
docs/SLEEP-CONSOLIDATION.md |
夜间整理 + 桌宠记忆分层的正式方案 |
docs/SLEEP-PENDING.md |
夜间整理的执行记录、跑过的验证、还欠什么 |
docs/RUNBOOK.md |
部署、看护、备份、恢复演练、故障对照 |
docs/PLAN.md |
里程碑与取舍 |
docs/TODO.md |
单一待办来源(含明确不做与待决策) |
docs/IMPLEMENTATION.md |
实现细节与设计理由 |
docs/SPIKE-DSH.md |
为什么选 MCP instructions + 工具,而不是别的注入方式 |
../memories/50-knowledge/2026-10-24-memory-hub-deploy-from-zero.md |
从零部署的踩坑记录 |
memory-hub本体:MIT(见LICENSE)。qq-bridge/:MIT,保留上游 Copyright 与NOTICE.md;SnowLuma 本体是专有组件,不随仓库分发。- 语义模型
bge-small-zh-v1.5由脚本按需下载,不随仓库分发。