🎁 福利专区全网大模型免费应用 + 新用户福利 + 注册活动入口,低成本玩转 AI

思维链记忆 canglight/dsh-chain

DeepSeek 客户端
canglight3 天前有提交已提供中文文档扫描:含敏感能力兼容 / 相关生态

记住每次判断的理由,按遗忘曲线自动整理记忆

36.8/100
综合分
6
GitHub 星标
—
npm 周下载
—/5
用户评分

数据截至 2026/10/31 · 周下载取自 npm 全网(含 CI / 镜像),非本站安装数;综合分由星标、活跃度与兼容性检查合成。

相关生态插件(非 dsh 原生,按其对应运行时安装)
git clone https://github.com/canglight/dsh-chain.git

状态速览

判定依据:实装验证 · 静态安装检查 · 自动扫描 · 仓库维护状态
信任档位:仅索引
是什么
生态插件
可安装,未声明 dsh 能力
装得上吗
静态安装检查有提示项,装前建议看一眼 README
安全吗
本站尚未对该插件做风险分级
暂未覆盖,不等同于无风险
还在维护吗
活跃:最近一次提交在 3 天前
档位由哪些信号合成

本站实装验证(真实安装 L4 覆盖全站;L5 为运行时冒烟或实机截图,覆盖范围小于 L4) · 验证所用 dsh 版本 · 静态安装检查 · 风险分级 · 仓库维护状态。 下方各区块是它的证据明细。

验证判据与等级说明 →
社区实装还没有人打卡 · 装过的第一个就是你
安装兼容性检查⚠ 装前注意

未发布到 npm registry,仅可从源码安装

展开查看检查明细 ↓

以下结论由程序自动检查 npm 包、engines 声明与入口文件得出,未做人工实机验证——能装不等于用着没问题。

✗npm 包dsh-chain-shared(未发布到 npm,仅可源码安装)
✓Node 引擎要求 >=20 · 基线 Node 22.19 满足
✓dsh CLI 依赖未声明 dsh 版本约束
✓入口文件main/exports/bin 已声明

验证方式:npm registry 存在性 + package.json 静态校验 · 最后检查 2026/10/31 17:42:15

安全扫描自动 · 静态分析源码
v2.7.0
🔵 扫描:含敏感能力尚无人工评估
展开查看扫描说明与证据 ↓

静态扫描发现插件具备触达本地系统的能力:执行外部命令、读写/删除本地文件、读取剪贴板或密钥类环境变量(至少其一)。这是能力清单,不是危险判定——具体能力项与证据见下方,安装前请自行判断是否需要。

可执行外部命令读写/删除本地文件读取剪贴板或密钥环境变量发起外部网络请求
查看扫描证据(共 91 处,展示前 84 条)
  • 读取本地文件
    lib/index.js:351
    if (existsSync(metaPath)) {
  • 读取本地文件
    lib/index.js:352
    const meta = JSON.parse(readFileSync(metaPath, 'utf8'));
  • 读取本地文件
    lib/index.js:360
    const text = readFileSync(p, 'utf8');
  • 读取本地文件
    lib/index.js:374
    if (!existsSync(path)) return true;
  • 读取本地文件
    lib/index.js:377
    const ageMs = now.getTime() - statSync(path).mtimeMs;
  • 读取本地文件
    lib/index.js:481
    if (!existsSync(script)) return;
  • 执行外部命令来自配置/变量
    lib/index.js:507
    const child = spawn(process.execPath, [script, 'prompt', target], {
  • 发起网络请求
    lib/client.js:629
    fetch('/chain-shared/api/patrol').then(function (r) { return r.ok ? r.json() : null; })
  • 发起网络请求
    lib/client.js:632
    fetch('/chain-shared/api/layers').then(function (r) { if (!r.ok) throw Error('layers'); return r.json(); })
  • 发起网络请求
    lib/client.js:645
    fetch('/chain-shared/api/pulse', { headers: { accept: 'application/json' } })
  • 发起网络请求
    lib/client.js:669
    fetch('/chain-shared/api/graph').then(function (r) { return r.json(); }).then(function (j) {
  • 发起网络请求
    lib/client.js:703
    fetch('/chain-shared/api/graph', { headers: { accept: 'application/json' } })
  • 发起网络请求
    lib/client.js:781
    fetch('/chain-shared/api/graph', { headers: { accept: 'application/json' } })
  • 读取本地文件
    lib/config.js:190
    const raw = readFileSync(settingsPath(), 'utf8').replace(/^ FEFF/, '');
  • 读取本地文件
    lib/config.js:205
    if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
  • 写入本地文件
    lib/config.js:205
    if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
  • 写入本地文件
    lib/config.js:207
    writeFileSync(settingsPath(), JSON.stringify(clean, null, 2) + '\n', 'utf8');
  • 读取本地文件
    lib/patrol.js:617
    const files = readdirSync(dir).filter((name) => /^\d{4}-\d{2}-\d{2}\.md$/.test(name));
  • 读取本地文件
    lib/patrol.js:621
    const text = readFileSync(join(dir, name), 'utf8');
  • 写入本地文件
    lib/patrol.js:694
    if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
  • 读取本地文件
    lib/patrol.js:694
    if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
  • 读取本地文件
    lib/patrol.js:700
    if (existsSync(metaPath)) {
  • 读取本地文件
    lib/patrol.js:702
    prevMeta = JSON.parse(readFileSync(metaPath, 'utf8'));
  • 读取本地文件
    lib/patrol.js:710
    const appended = options.append === true && existsSync(path);
  • 写入本地文件
    lib/patrol.js:727
    appendFileSync(path, written, 'utf8');
  • 写入本地文件
    lib/patrol.js:729
    writeFileSync(path, full, 'utf8');
  • 写入本地文件
    lib/patrol.js:747
    writeFileSync(
  • 读取本地文件
    lib/retrieval.js:75
    for (const line of readFileSync(idfPath, 'utf8').split('\n')) {
  • 读取本地文件
    lib/working.js:171
    if (!existsSync(path)) return { items: [] };
  • 读取本地文件
    lib/working.js:173
    const raw = JSON.parse(readFileSync(path, 'utf8'));
  • 写入本地文件
    lib/working.js:195
    mkdirSync(storeDir(), { recursive: true });
  • 写入本地文件
    lib/working.js:196
    writeFileSync(workingPath(), JSON.stringify({ items: state?.items ?? [] }, null, 2), 'utf8');
  • 读取本地文件
    lib/mood.js:45
    if (!existsSync(path)) return null;
  • 读取本地文件
    lib/mood.js:47
    const raw = JSON.parse(readFileSync(path, 'utf8'));
  • 读取本地文件
    lib/mood.js:77
    if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
  • 写入本地文件
    lib/mood.js:77
    if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
  • 写入本地文件
    lib/mood.js:78
    writeFileSync(moodPath(), JSON.stringify(record, null, 2), 'utf8');
  • 执行外部命令来自配置/变量
    lib/embed.js:66
    const worker = fork(fileURLToPath(new URL('./embed-worker.js', import.meta.url)), [], {
  • 读取本地文件
    lib/embed.js:132
    if (!existsSync(p)) return cache;
  • 读取本地文件
    lib/embed.js:134
    const raw = JSON.parse(readFileSync(p, 'utf8'));
  • 读取本地文件
    lib/embed.js:152
    if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
  • 写入本地文件
    lib/embed.js:152
    if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
  • 写入本地文件
    lib/embed.js:155
    writeFileSync(p, JSON.stringify({ model: MODEL_ID, builtAt: cacheMeta.builtAt, vectors }), 'utf8');
  • 读取本地文件
    lib/edges.js:61
    if (!existsSync(path)) return out;
  • 读取本地文件
    lib/edges.js:64
    raw = readFileSync(path, 'utf8');
  • 读取本地文件
    lib/edges.js:127
    if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
  • 写入本地文件
    lib/edges.js:127
    if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
  • 写入本地文件
    lib/edges.js:137
    appendFileSync(edgesPath(), JSON.stringify(rec) + '\n', 'utf8');
  • 读取本地文件
    lib/perceive.js:42
    const txt = readFileSync(join(here, 'lexicon', 'daily.zh.txt'), 'utf8');
  • 读取本地文件
    lib/perceive.js:250
    if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
  • 写入本地文件
    lib/perceive.js:250
    if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
  • 写入本地文件
    lib/perceive.js:273
    appendFileSync(path, JSON.stringify(row) + '\n', 'utf8');
  • 读取本地文件
    lib/perceive.js:276
    const size = statSync(path).size;
  • 读取本地文件
    lib/perceive.js:278
    const lines = readFileSync(path, 'utf8').split('\n').filter(Boolean);
  • 写入本地文件
    lib/perceive.js:280
    writeFileSync(path, lines.slice(-Math.floor(LOG_MAX_LINES / 2)).join('\n') + '\n', 'utf8');
  • 读取本地文件
    lib/perceive.js:299
    if (!existsSync(path)) return [];
  • 读取本地文件
    lib/perceive.js:300
    const lines = readFileSync(path, 'utf8').split('\n').filter(Boolean);
  • 执行外部命令来自配置/变量
    lib/store.js:68
    const child = spawnSync(join(process.env.SystemRoot || 'C:\\Windows', 'System32', 'WindowsPowerShell', 'v1.0', 'powershell.exe'),
  • 读取本地文件
    lib/store.js:80
    const stat = readFileSync(`/proc/${pid}/stat`, 'utf8');
  • 读取本地文件
    lib/store.js:82
    const boot = readFileSync('/proc/sys/kernel/random/boot_id', 'utf8').trim();
  • 删除本地文件
    lib/store.js:130
    try { unlinkSync(path); } catch (error) { if (error.code !== 'ENOENT') throw error; }
  • 读取本地文件
    lib/store.js:147
    const data = JSON.parse(readFileSync(join(dir, name), 'utf8'));
  • 读取本地文件
    lib/store.js:164
    for (const name of readdirSync(dir)) {
  • 读取本地文件
    lib/store.js:169
    if (Date.now() - statSync(path).mtimeMs < 3600000) continue;
  • 写入本地文件
    lib/store.js:197
    mkdirSync(domain.dir, { recursive: true });
  • 读取本地文件
    lib/store.js:207
    for (const peer of readdirSync(domain.dir)) {
  • 写入本地文件
    lib/store.js:214
    writeFileSync(temporary, JSON.stringify({ ...owner, number: String(number) }), { encoding: 'utf8', flag: 'wx' });
  • 写入本地文件
    lib/store.js:215
    lockFileOperation(() => renameSync(temporary, ticket), deadline);
  • 写入本地文件
    lib/store.js:319
    if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
  • 写入本地文件
    lib/store.js:357
    appendFileSync(path, prefix + line, 'utf8');
  • 写入本地文件
    lib/store.js:447
    truncateSync(storePath(), startSize);
  • 读取本地文件
    lib/patrol-status.js:33
    metas = readdirSync(dir).filter((n) => META_RE.test(n)).sort();
  • 读取本地文件
    lib/patrol-status.js:41
    const meta = JSON.parse(readFileSync(join(dir, last), 'utf8'));
  • 读取本地文件
    lib/patrol-status.js:55
    reportBytes: existsSync(md) ? statSync(md).size : 0,
  • 读取本地文件
    lib/patrol-status.js:55
    reportBytes: existsSync(md) ? statSync(md).size : 0,
  • 读取密钥环境变量
    lib/triage.js:100
    const apiKey = text(opts.apiKey ?? process.env.TRIAGE_API_KEY);
  • 发起网络请求
    lib/triage.js:108
    const response = await fetch(`${baseUrl}/chat/completions`, {
  • 读取密钥环境变量
    lib/wake.mjs:47
    const envToken = String(process.env.DSH_WAKE_TOKEN ?? '').trim();
  • 读取本地文件
    lib/wake.mjs:55
    files = readdirSync(logsDir)
  • 读取本地文件
    lib/wake.mjs:57
    .map((name) => ({ name, mtime: statSync(join(logsDir, name)).mtimeMs }))
  • 读取本地文件
    lib/wake.mjs:64
    try { text = readFileSync(join(logsDir, name), 'utf8'); } catch { continue; }
  • 发起网络请求
    lib/wake.mjs:105
    const res = await fetch(url, { redirect: 'manual' });
  • 发起网络请求
    lib/wake.mjs:116
    const res = await fetch(baseUrl + '/api/' + method, {
  • 读取本地文件
    lib/wake.mjs:144
    const raw = readFileSync(p, 'utf8').replace(/^ FEFF/, '');
扫描于 2026/11/4(0 天前) · 范围 入口闭包 + 发布文件清单 · 来源 GitHub 仓库

静态扫描只反映源码中出现的调用,不构成安全背书;人工评估与自动扫描相互独立、互不覆盖。

依赖的 DSH / Cordis 模块
@deepseek-ai/schemastery
给这个插件打分还没有人投票↓
用户评分
还没有人投票,来当第一个

神经元记忆网络 让 DSH 记住「为什么这么决定」,而不只是结论。思维链记忆插件:8 个池分级遗忘、每轮注入、神经元 3D 星云浏览、夜巡自动整理。

README

由 DeepSeek 最新模型翻译生成
dsh-chain-shared · 思维链记忆(全会话通用版) 当前版本:2.2.0。2.1 未单独发布,其间更新合入本版。 封面 给 DeepSeek Harness 用的记忆插件。 它记的不是「发生了什么」,而是「当时为什么这么定」。 每条记忆是一句判断 + 依据 + 犹豫 + 结论;它按时间衰减,只有被重新想起才会留住。 这个包是全会话通用版:你在哪个会话里说话,看到的都是同一套记忆。 (「每条会话各一套、互不串味」的隔离版不在这个包里。) 它长什么样 星云 八个池各成一簇(另有「元记忆」汇总簇),点与点之间是「依据」和「相关」的连线。整团在缓慢自转(可关),每颗记忆球绕自己的池心走固定轨道 —— 不会互相追、也不会飞出去。 | 标签模式 | 聚焦一条 | 设置面板 | | 标签 | 聚焦 | 设置 | 截图来自开发机(约 240 条记忆 / 8 池)。观感会随记忆条数增长而变化。
展开完整文档(README 剩余 1 节)
dsh-chain-shared · 思维链记忆(全会话通用版)

当前版本:2.2.0。2.1 未单独发布,其间更新合入本版。

封面

给 DeepSeek Harness 用的记忆插件。

它记的不是「发生了什么」,而是「当时为什么这么定」。
每条记忆是一句判断 + 依据 + 犹豫 + 结论;它按时间衰减,只有被重新想起才会留住。

这个包是全会话通用版:你在哪个会话里说话,看到的都是同一套记忆。
(「每条会话各一套、互不串味」的隔离版不在这个包里。)

它长什么样

星云

八个池各成一簇(另有「元记忆」汇总簇),点与点之间是「依据」和「相关」的连线。整团在缓慢自转(可关),每颗记忆球绕自己的池心走固定轨道 —— 不会互相追、也不会飞出去。

| 标签模式 | 聚焦一条 | 设置面板 |
| 标签 | 聚焦 | 设置 |

截图来自开发机(约 240 条记忆 / 8 池)。观感会随记忆条数增长而变化。

它解决的四个问题

| 问题 | 这个插件的做法 |
| 上下文一压缩,为什么这么定的理由就丢了 | 判断存成 JSONL,每轮从磁盘重新渲染进系统提示,跟对话历史无关,压不掉 |
| 记了一堆,真正用的时候一条也想不起来 | 用你的这句话去检索记忆(jieba 分词 + 通用 IDF),命中几条就把那几条的判断行贴回提示词 |
| 越攒越多,最后全是噪声 | 按池分半衰期自动淡出;从没被想起过的条目会先淡、再走归档 |
| 记了一堆,却不知道自己记得什么、哪条还可信 | 元记忆:chain_about 回答「关于 X 我知道什么 / 多可信 / 变过几次」—— 被想起几次、有没有人确认过、是否被取代、依据还在不在 |

安装

方式一:放进 DSH 的插件目录
cd ~/.dsh/plugins
git clone <repo> dsh-chain-shared

⚠️ 必须在插件目录里装一次依赖 —— 见下面「为什么这一步不能省」
cd dsh-chain-shared
npm install --registry=https://registry.npmmirror.com

再挂进 profile
cd ~/.dsh/profiles/web
pnpm add link:~/.dsh/plugins/dsh-chain-shared # 或用 dsh plugin add

然后重启一次 DSH(插件清单变更不热加载)。

默认不按会话过滤。如需限定会话,可在启动 DSH 前设置环境变量 DSH_CHAIN_TARGET_SESSION_IDS(逗号分隔会话 id);缺省空字符串表示全会话共享。画布标签从服务端读取该配置。

首次装上时记忆是空的 —— 需要你在对话里让它记东西,或者用 chain_append 工具写第一条。

依赖

- Node ≥ 20
- 运行中的 DSH(任意 profile)
- @node-rs/jieba(预编译二进制,不需要 C++ 编译器)
- @deepseek-ai/schemastery(可选:装了才有设置页,没装插件照常跑)
- @huggingface/transformers(可选,约 400 MB:装了才有语义检索;没装所有语义路径自动退回关键词路,不报错)

为什么那一步 npm install 不能省

pnpm add link: 只是把 profile 指向这个目录,不会替你装插件自己的依赖。
漏装的表现很安静,两条都对不上人会以为是版本问题:

| 没装的包 | 症状 |
| @node-rs/jieba | 检索层静默降级(不报错、只是召回变差)|
| @deepseek-ai/schemastery | 「思维链」画布右上角的设置按钮点了没反应 |

刻意做成可选依赖,是为了「装漏了也还能用」;但装一遍最省事。

它提供什么

2.2.0 新增矛盾候选工具、eventId 分组、受限自动退役,以及夜巡的矛盾 / 同事件审查;2.1 的小更新已合入,详见 CHANGELOG.md。

2.0 起多了什么:注入里会出现「这次联想到的」(在链上走 1–2 跳)与可选的「语义补位」;多了 chain_about 这个元记忆出口;画布上多了感知 / 元记忆两块面板和一行「夜巡 16:51 · N 条」状态。
感知层目前只记录、不改变行为 —— 这点写在 CHANGELOG 的「已知限制」里。

8 个工具

| 工具 | 干什么 |
| chain_append | 写一条判断(judgement / evidence / hesitation / conclusion / tags / weight,可选 eventId) |
| chain_recall | 按关键词 / 日期 / 权重查;最多补带 2 条 sameEvent 同组记录 |
| chain_graph | 导出图谱(Mermaid) |
| chain_touch | 给一条已有记录标「确认」或「被推翻」(确认 +0.05,推翻降到存档线) |
| chain_mood | 写当前情绪(单一状态牌,会自己过期) |
| chain_patrol | 手动跑一次夜巡,出报告 |
| chain_about | 问「关于 X 我知道什么」:返回被想起次数 / 人工确认次数 / 生命周期状态 / 是否被取代 / 依据条数。只读 —— 调它不算「想起」 |
| chain_conflict | 指定 ids: [a, b] 直接比较,或查同池候选;泛词过滤与 strong 排序,只报告线索 |

同一次决策可用可选 eventId 串起前后判断;chain_recall / chain_about 最多补带 2 条同组记录并标记 sameEvent: true。带 eventId 的新判断在前段出现推翻词时,可自动退役同组未退役旧条,核心池受保护,单次最多 2 条,相同载荷重试复用新条;历史正文不变。
生命周期事件共 5 种:touch(人工确认)、recall(自动命中)、supersede(退役)、rebalance(调权重)、retag(定池);eventId 是记录字段,不是新增事件类型。

1 个注入段

每轮往系统提示里注入一段 ## 我的思维链:

我的思维链(只对我可见)

我现在:踏实(20 分钟前·强度 0.80)因为:……

核心(32 条 · 不随时间淡):
- ……
事实(16 条):
- ……
技能(50 条):
- ……

你这句话让我想起来(检索词(分数)):
- ……

1 个可视化 tab

会话视图里的「思维链」tab:3D 星云图,八个记忆池各成一簇(另有决策 / 元记忆虚拟区),可拖拽旋转 / 滚轮缩放 / 点节点看详情;新记忆会在 9 秒内自己长出来。

注入预算:按字数装箱

注入不是「取最近 N 条」,而是拿字符预算往里装:

- 四个常驻候选池(事实 / 铁律 / 核心日常 / 片面日常;经验按需)的候选合成一个池,按权重全局排序逐条装;
- 装不下就跳过这条、继续试下一条(不是整段停);
- 装不下的条数写在段标题里:另有 N 条未展开(chain_recall 可查) —— 不静默丢;
- 核心段永不参与装箱(它本来就该全量在)。

这样注入量恒定:库里 100 条还是 1000 条,占用的 prompt 都一样。
实测:1000 条记忆时封顶 24000 字符;137 条时约 9100 字符。

预算是 injectMaxChars(默认 24000 字符 ≈ 8k token),在画布右上角的设置面板里可改。

记忆怎么分层(这是它的核心设计)

| 池 | 半衰期 | 归档 | 说明 |
| 核心(原则 / 人格) | 永不衰减 | 永不 | 你自己定的规矩,不该随时间变淡 |
| 事实(关于用户的) | 365 天 | 3 年 | 15 天宽限 |
| 铁律(必须 / 不许 / 红线) | 730 天 | 10 年 | 120 天宽限 —— 「规矩」和「经验」不是同一种东西 |
| 经验(教训 / 工具 / 排障) | 60 天 | 1 年 | 15 天宽限;按需注入,不常驻 |
| 核心日常 | 60 天 | 180 天 | 20 天宽限;多次被提起的日常会升级到这里 |
| 片面日常 | 20 天 | 45 天 | 5 天宽限;一次性的东西,留着也没用 |
| 情绪 | 1 天 | 3 天 | 3 小时宽限;一块可覆盖的状态牌,不是日志 |
| 海马体 | 1 天 | 14 天 | 原始片段缓冲区,永不注入,等夜巡整理 |

权重衰减不是只看时间:被检索命中的次数会拉长半衰期(1 + 0.6·log2(1+次数),封顶 4 倍)。
所以「被想起的东西活得更久,没人提的慢慢淡出去」是这套系统真正的遗忘机制。

核心层有上限(不是锁死)

核心池最多 32 条(CORE_MAX)。刚装时是 0 条 —— 它会随着你立规矩慢慢长, 满了之后 chain_append 再想写「原则/人格」就会被拒绝,并提示改去写 AGENTS.md。

为什么要有这条上限:核心是不可衰减的那一层,代表「我是谁」。
它一旦能被随手追加,就会越写越厚,最后变成一份谁都不读的清单。

标签没命中任何池怎么办

不猜。 池子决定的是「这条活多久」,猜错的代价不对称 —— 猜成 7 天的片面日常,那条就真没了。

标签没命中任何池 → 落「海马体」(原料缓冲区:14 天归档、永不注入)+ 打「未归类」标注 → 夜巡第 ⑥ 节单独列出来审 → 同一句重复写满 3 次 → 升级「需人工定池」

反复出现说明它确实值得留,只是标签体系还认不出它 —— 那就该由人给它一个池,而不是让系统猜。

海马体怎么清空

不用逐条点。 把同类片段合并成一条判断写进去(chain_append)——被它覆盖的原料会自动退役(判据:双向包含,或二元组相似 ≥0.55)。

所以「重复提交」不会让海马体膨胀:写完判断,原料自己让位。
剩下的纯噪声(「重启了」这类)按海马体寿命衰减,默认 14 天归档。

检索:两路融合

chain_recall 和每轮的「这句话让我想起来」都走同一套检索:

1. IDF 罕见词命中(jieba 分词 + 通用 IDF,阈值 9)—— 专有名词很准,换个说法就零命中;
2. 字符二元组重合 —— 补「说法变了但字面沾边」的那部分;
3. 两路各自排名,按 RRF(1/(60+名次))融合,只看名次、不看分数——避免某一路的尺度把结果带偏。

两条硬规矩:没有词面证据的条目不进候选(否则新鲜度会把无关项顶上来); 新鲜度与权重只做平局裁决,不参与主导排序。

实测(tools/eval-queries.json,14 条查询含 6 条换说法):

| 路径 | Recall@3 | Recall@5 | MRR |
| 只用 IDF | 21% | 29% | 0.15 |
| 整句子串(旧 chain_recall) | 0% | 0% | 0.00 |
| 两路融合 | 50% | 64% | 0.48 |

自己验:node tools/eval-retrieval.mjs。

夜巡(每天 18:00 后自动跑一次)

- 起一个检查:每 10 分钟看一次「现在 ≥ 18:00 且今天的报告还不存在」
- 满足就跑:生成审查报告,自动夜巡不写归整或退役事件;手动 chain_patrol({ apply: true }) 可显式执行权重归整
- 报告落在 ~/.dsh/memories/.chain-shared/patrol/YYYY-MM-DD.md

报告正文含 10 个检查小节(不计概览):久未命中 / 疑似重复 / 共享关键词但没连线 / 矛盾候选 / 同事件对账 / 半写断裂 / 历史 eventId 粗回填建议 / 海马体待整理 / 自动归整 / 未归类待审。有关系边时另附第 11 节「联想网络」。
矛盾与同事件检查只报告,不自动回填或退役;同事件自动退役发生在满足条件的 chain_append 新写入时。

开机时如果已过 18:00,60 秒后会补跑一次,一天只跑一次(用报告文件做幂等锁)。

自动唤醒(默认开:唤醒「最近说话的那个会话」)

夜巡只写报告的话,那一环会断在「报告躺在磁盘里没人读」。所以包里有 lib/wake.mjs:它会走 DSH 自己的 HTTP 接口,投递一条「请审核报告」的消息。

默认唤醒谁:最近说话的那个会话。
理由就是这个包的前提——「整个 DSH 共用同一条记忆链」,报告属于所有会话,所以最近还在说话的人,最该看到它。

想固定唤醒某一条(或关掉),在数据目录写 settings.json:

// ~/.dsh/memories/.chain-shared/settings.json
{
"wakeSessionId": "session-xxxxxxxx-....", // 可选:固定叫醒这一条 "wakeEnabled": true // 设 false 一键关掉 }

行为边界:

| 情况 | 行为 |
| 什么都没配 | 唤醒最近说话的会话 |
| 配了 wakeSessionId | 叫醒它 |
| 目标会话正在运行 | 排队(mode=queue,不打断)|
| wakeEnabled: false | 一律不叫 |

自检(不会发消息):

node lib/wake.mjs check # 能否发现宿主 / 认证 / 会选中哪个会话
node lib/wake.mjs prompt --dry # 只打印要投递什么

设置(就在「思维链」画布右上角)

打开「思维链」tab → 工具栏右边点 「设置」 → 画布右上角浮出面板:

| 设置项 | 说明 |
| 池 × (宽限 / 半衰期 / 归档) | 8 个池各三个数字 —— 最常调的就是这个 |
| injectMaxChars | 注入段最多多少字符(越大越贵,但记得越全;默认 24000 ≈ 8k token)|
| coreMax | 核心池上限(满了拒绝新增「原则/人格」;默认 32)|
| patrolHour | 夜巡每天几点之后开始(默认 18)|

池的 key:core / fact / skillRule / skillExp / dailyCore / dailyPartial / emotion / hippocampus。

改完立刻生效,不用重启:面板调的是插件自己的接口 /chain-shared/api/settings,宿主侧收到就直接换掉运行时配置,同时把值写进 ~/.dsh/memories/.chain-shared/settings.json(下次启动自动读回)。

为什么不塞进 DSH 自己的设置页:那套要走 plugins.item 槽位 + configForms 的内部约定,接起来脆弱(而且旧版槽位名一换就静默失效)。放在自己的画布里更直接,也不依赖宿主版本。

留空的输入框 = 用代码里的默认值(Host 只回传"你改过的")。

工作记忆块

注入段最前面可能有一块「还没做完的事」:

【还没做完的事】
- 待办:记得问对方吃没吃饭
- 答应过:明天把方案发你

它从你的话里认待办和承诺(记得 / 待会 / 明天 / 答应过 / 我准备),纯正则,不调用模型。
三条规矩:最多 3 条、3 天没提起自动过期、说"做好了/发过去了"就删相关那条。
它存在 working.json,不进记忆链 —— 待办不该参与衰减统计,也不该出现在星云图里。

数据在哪、隐私如何

~/.dsh/memories/.chain-shared/
├─ chains.jsonl ← 全部判断(追加写,一行一条) ├─ mood.json ← 当前情绪(单槽,会被覆盖) └─ patrol/ ← 夜巡报告

- 全在本地,插件不联网、不发遥测、不调用任何外部 API。
- 注入内容进的是你自己的模型请求,跟任何第三方无关。
- 「会话隔离」不在这个包里:所有会话共享同一份记忆。如果你在多条人格线之间切换、怕串味,用隔离版。

它不适合谁

- 想要"全自动记住一切"的:这套的写入靠模型判断,不是无脑记录。它会漏,也会记错。
- 不想让模型看到自己旧判断的:注入就是把旧判断塞回提示词,这点要想清楚。
- 多会话强隔离需求:见上,这版是共享的。

已验证 / 未验证

验证过(作者本机):

- 注入每轮现渲染,上下文压缩后仍在(实测 32 核心 + 17 事实 + 54 技能一条没少)
- 检索:两路融合(IDF + 二元组),14 条评估查询 Recall@5 64%(原话 100%);单用 IDF 时 29%
- 注入预算压测:合成 1000 条记忆时注入封顶 24000 字符,四个池都有配额、都标注了未展开条数;137 条时约 9100 字符
- 衰减曲线:同为 98 天前最后接触,被想起 0 次衰减 25.7%、3 次 12.6%、10 次 9.2%
- 夜巡:启动后 53 秒补跑成功,自动归整写入 3 条降权,报告 8.2 KB
- 可视化:真 3D 每帧投影(194 颗记忆球 + 依据微粒),30fps 上限,零 SVG filter

没验证:

- 只在 Windows + DSH 0.2.0-rc 上跑过;macOS / Linux 未测
- 夜巡本身仍是 O(n²):1000 条时配对比较约 50 万次,没测过耗时(注入那条已经压过,夜巡这条没有)
- 跨语义的召回(换个说法只字不沾边)没解决:约一半换说法查询仍会漏

许可

MIT,见 LICENSE。

署名:末苍和她的大肥鱼。

这名字是这么来的:插件是一起做出来的,不是谁给谁写的。
预览分享卡片长什么样 ↓canglight/dsh-chain
分享 · DeepSeek Harness Hub
canglight/dsh-chain

记住每次判断的理由,按遗忘曲线自动整理记忆

这是你的仓库?认领条目

回到顶部