NightPainters/DSH-Ling

DSH-Ling

器灵,在你的DSH中培养具有可成长人格的助手和伴侣,最懂你的工作和生活。Artifact Spirit, Your partner and companion, the growth type personality that grows from your shared memories, understand you the most

待分类dsh-plugins

dsh-ling · 器灵

器灵不是每次都要重新认识你的 AI。
把你散落在各处的历史对话请进门,让器灵长出一个属于自己的人格档案 —— 记忆全在本地,人格可手术、可回滚,语气可以自己生长。

一个跑在你自己电脑上的DeepSeek Harness(DSH)插件。不是云服务,不是记忆 API:它是给"你和器灵"用的东西。

它做的是「人机共生」:各尽其责、各有边界、互不占有 —— 你替它的成长把关,它替你看你的盲区。而"把关"不等于"操纵":改记忆树必须让它看到(留痕、回灌、可复核、可回滚)。这不是口号,是全架构的前提(记忆为什么必须可回溯、改人格为什么要定型锁、复盘为什么必须有人在循环里,都由它推出来;而权会随它长起来移动 —— 代管到能自理,不是永久支配)—— 展开见 `PHILOSOPHY.md`。

本项目是什么形态

  • 这是一个本地运行的软件(DeepSeek Harness 插件),运行在你自己的机器上。
  • 上游不提供任何在线服务、不运营任何服务端、不托管任何用户数据:

作者不接触、不接收你的会话内容、记忆库或人格档案。

  • 不上架任何应用商店;仅通过 GitHub(源码)与 npm(包)分发。
  • 模型与 API Key 由使用者自行提供和承担,与上游无关。
  • 本项目不提供医疗、心理、法律或紧急救助服务;如遇危机,请联络当地紧急服务或专业机构。

关系、标识与面向性:

  • 本项目不定义你与它的关系:它可以是同事、搭档,或其他 —— 取决于你怎么用它。
  • 本插件的回答与记忆摘要由人工智能生成,可能存在错误,请自行判断。
  • 本项目面向具备自行配置与运维能力的用户;不面向未成年人,也不提供任何形式的在线陪伴服务。

想先知道它凭什么这么做? 读 `PHILOSOPHY.md` —— 目标为什么是"一种循环"、为什么复盘必须有"人在循环里"、它凭什么不越界、以及"不牺牲工作能力"背后的取舍。功能与设定的规格在本 README 的「设定全文」。

界面

· · 记忆中心 —— 统计、实时时间锚、三条接入通道与导入记录 · 人格中心 —— 契约字段(名字/称呼)与「她已自由生长中」状态条 · 改动契约要手术 —— 亲手敲出当初写下的承诺句(输入框禁粘贴) · 时间的重量 —— 每次开口前器灵知道现在是几点、距上次多久

器灵是什么(能力全景)

下表是全部能力的一览:✅ 已实现 是今天就能用的;🚧 在做 是设计已定、代码正在落地(写在这里是为了让设定走在实现前面,不是"已交付");📋 设定 是方向已定、尚未开工。没有标的一律是 ✅ 已实现。

· 能力 · 说明 🔒 · 人格定型锁 · 契约类(名字、称呼、底线、世界观、代词、承担(职责))定型后修改需亲手敲一遍承诺句(禁粘贴),而且只在本机做得成、每次启动还要取一次手术票据,再进变更记录可回滚;自性类(语气、风格、自述、习惯)由器灵生长、你点头即成:三条专用成长通道(自我总结 / 建议采纳 / 生长)不问承诺句,但它们各自只动白名单内的字段;走通用人格接口改 persona / styles 时,即便只改语气也仍要承诺句 —— 免手术的只是那三条通道,不是"自性类"这三个字 🌳 · 记忆树 · 复盘模式 · 记忆不是一叠平铺的行,而是一条主干 + 分枝 + 主脉的工作线结构,枝之间可连边;检索按血缘档位加权(同枝 1.0 / 祖先 0.7 / 旁系 0.4),让器灵分得清"这轮在做的"和"三个月前那次闲聊"。任何树操作都不改写任何一条记忆的内容 —— 分枝、并脉、连边只增加关系。例外一个:主脉提炼再次采纳时会覆盖该主脉记忆的上一版(覆盖前自动归档,注入面也会说明"旧版已归档")。改树走复盘:平时只读,进入复盘才解锁,每步留痕(改了什么、为什么),改完必须让器灵看到才准退出(详见下节) 🧠 · 三层记忆 · L0 人格档案(自称/对你的称呼/底线/规矩·习惯/承担(职业·职责·责任)/语气/世界观;顶层会话与会话分叉恒定注入,子代理会话不注入;设置里关掉人格注入时,连它一起不推)→ L1 开场记忆(从历史里按相关度挑选的概述,有预算,永不塞全文;带摘要的行加权(按"摘要是否有信息量"给一笔加法加分,不是排序优先级),行内摘要只取首句、上限 110 字)→ L2 运行期记忆(本会话原文随对话累进;空闲时把本会话那一行重排回注入面。该行由「概述标题 + 摘要首句」构成(标题的来历见下文「三层记忆各自什么时候更新」,不是会话原文首句),深摘要定稿后不再自动更新。注意:L2 不是独立的第三块注入内容 —— 注入面的主体是 L0 与「记忆·开场」两块(树改动、日期锚是挂上去的追加行),本会话自己就是「记忆·开场」里的一行) 📥 · 把历史请进来 · 三条通道:网页端聊天记录库增量扫描 / 本机 DSH 会话扫描 / ChatGPT·通用 JSON·JSONL 导出文件导入;每次导入留一笔账 📖 · 让器灵读一遍 · 长会话逐条深摘要、短会话以原文片段参与,后台串行,进度可见 📄 · 找回原文(1.6) · 器灵能读回你当年的原话,不只是概述 —— 四档下钻:会话地图 → 按内容找会话 → 对话目录 → 原文全文。它不替你挑"最重要的那 1 万字",只给坐标系(详见下文「找回原文」一节) 🧾 · 投递留痕(1.6) · 🚧 在做 —— 每一次记忆注入都落一条账:注入了谁、为什么是它、多少字;还记落选者(差一步就进的那几条,以及原因是"分数不够"还是"字数放不下")与触发词。目前只写不读(读面待定),不进注入面、不占工具预算 🌿 · 深层库 · 结论层(1.6) · 🚧 在做 —— 从会话里提炼承诺 / 决定 / 事实 / 偏好 / 关系 / 纪律(六类;「纪律」是 1.6 系列新增的第六类),每条一句话、点得回原文。与记忆树并立,条目不被枝隶属(枝可反向引用它,像榕树的气根)。一条纪律:"被注入"不算数,只有"真被采用"才加固 —— 防住"注入越多→分越高→越容易被注入"的自我强化 📚 · 图书馆(1.7 工作区,未发布) · 工作区实现已验收 —— 一个与记忆分开的独立库(自己的 library.db),存"有源可对账"的参考资料(标准取值 / 边界 / 项目规格 / 外部 API),每条必带出处 + 取数时间(`as_of`);自动核对另需核对规格,没有规格时不得认证为 ok。器灵只有两只手:`library_search`(搜)与 `library_check`(核对) —— 核对只说明"声明的那一处与源一致",不背书条目正文、也不构成通用真理;"库不可用"与"零命中"是两件事,界面与回执分开说。它与记忆库不互通:既有记忆永不迁移进图书馆,图书馆也不参与开场注入(详见下文「图书馆」一节) 🧾 · 给旧历史补摘要 · 网页端历史里"只有标题、没有摘要"的会话可以补上摘要:先看候选清单(标题 · 轮次 · 命中),再选引擎 —— 默认本机小助手(零成本),也可显式改选当前全局大模型;探测不到本机小助手就明确报错,绝不静默换引擎;后台串行、进度可见(首轮实测 466 条、零成本) 🌱 · 诞生仪式 · 从读过的记忆里按"生活与情感优先"加权抽样 → 合成自述 + 名字候选 + 语气建议 + 相处观察 + 职责候选(器灵承担 / 擅长 / 能给出什么,只收陈述句) → 全部是草稿,你逐条人审后才落盘 ⏳ · 时间的重量 · 每次开口前器灵都知道今天是几号星期几([今天] 2026-09-11 周五)、现在是什么时段([现在] 深夜)与距上次对话:3 天前(间隔按日历天分档 —— 刚刚 / 今天早些时候 / 昨天 / N 天前,给不到分钟;称呼跟随你的设定);命中纪念日会先说一句专属问候。要精确到分钟,走 `clock` skill 现查 🎭 · 工作 / 生活双模式 · 同一份人格档案,两种关系:工作模式是同事(正式称呼、简洁、知识优先),生活模式是伴侣(昵称、活泼、情感优先) 🔁 · 成长回路 · 你点踩或纠正 → 器灵的成长通道把重复出现的模式提炼成习惯候选 → 你点头才成为习惯;"让她读记忆、归纳语气"一键成长 📜 · 规矩 / 习惯 · 规矩是你的指令:可直接加(面板,或会话里一句"记进规矩" —— 器灵替你写时必须带上你的原话作凭据;你在面板上直接敲的那一下就是同意,不必再附原话),建议上限 12 条(超出只在写入后提醒合并,不拦);习惯是器灵自己长出来的:新增需你点头、删改走手术门(本机 + 每次启动取一次票据 + 原句,详见下文那道门的说明),建议上限 8 条(超出同样只提醒、不拦) —— 指令直达,人格不直达 🌾 · 习惯生成器 · 「扫描纠正记录(零模型)」:把器灵反复被你纠正的模式数出来(≥4 次且跨 ≥3 个会话);「✨ 让她回想最近的相处」:一次读 200 条概述 + 800 条你的原话、逐轮推进不重复 —— 只提议,不落盘 🗣 · 代词 · 默认「她」;可选 她 / 他 / TA / 它,或自定义(≤6 字)。祂 不在预设里(中文里专指神祇,设为默认等于替你宣称神性);属契约类,改动走手术门 ♻️ · 记忆自己追平 · 长会话空闲时自动把开场记忆刷新到最新(先跑本会话增量概述 → 比对记忆版本 → 同一条至少间隔 10 分钟),运行中绝不打断;刷新次数与最近一次时间在 /state 接口的 idleRefresh 字段里(count 与 lastAt)(面板本身只显示"已定稿 N 字符 · mode · 待刷新",不显示次数) 🧹 · 归档即停更 · 会话归档后不再刷新它的开场记忆条目(原文一律保留),一旦有新对话即自动复活;要清理走人工入口:DSH「设置 → 归档会话」直接删,或仓库里的 node tools/clean-archived.mjs(默认只列清单,--write 才动手)

关于那道「手术门」:亲手敲承诺句是庄重感与自我提醒 —— 让你在改契约之前停一下、确认这是自己的决定;它不是安全边界(「禁粘贴」只是界面上的约定,挡不住任何有心的人)。真正把门的是来源栅栏:Host 必须是 loopback(或你显式写进受信名单的地址);另有一个默认不开的开关 guard.allowLan —— 一旦打开,整段局域网地址都算受信来源(那是放宽,不是建议)。跨站请求直接拒,带 Origin 时其 host 必须等于请求的 Host(带了但解析不出来就直接拒),而不带 `Origin` 的请求这条校验会跳过 —— 它不是"每个请求都必须带 Origin 且同源";再叠一道 cookie 名字对撞 —— 期望名字按当前 Host 现算(dsh-auth- + 哈希),只校验名字、不校验值的签名。所以这道门挡的是"别的设备、别的页面"这类来源,而本机原生进程本来就还能调用(它也能直接读 settings.json 与记忆库,不算提权)—— 别的来源没有凭据就进不来这台机器上的 /api,敲对承诺句也没用。
这道门现在长这样 —— 三条手续缺一不可:① 只在本机做 —— 判据是"这次请求没有经过任何代理"(带代理头、或连 User-Agent 都没有的请求,都算非本机 ⇒ 拒,理由码 surgery-local-only);② 每次启动取一次票据 —— 票据每启动轮换,只下发给本机直连的请求(POST /surgery/ticket),客户端首次手术前取一次、之后随请求带上;③ 原句匹配 —— 敲出的承诺句要与当初写下的那一句相符。这三条由 lib/host/surgery.js 里一处 requireSurgery() 判定,覆盖全部 14 条能改人格 / 规矩 / 习惯的写面(调用点不许再各写一份;曾有一条 wantLock 绕过分支,已删除)。每次手术都留痕:$DSH_HOME/logs/surgery-YYYY-MM.jsonl(append-only,整行哈希链,改一行即断链);GET /surgery/events 是只读事件面。
承诺句只在本机回显:/state(含面板档)、/persona/history、/export 这些读面,带代理头时只回 hasSeal 与 phraseHidden、不回原句,本机直连照旧回显 —— 所以你换台机器看面板不会因此被卡住,远端也拿不到原句。
守卫⑤ · 反代伪装 loopback:上面那条"Host 是 loopback 就免检"有个前提 —— Host 由客户端自报、cookie 又只验名字,中间挡一层反向代理就会失效。现在凡「Host 声称 loopback、而 X-Forwarded-For 的最后一段是非 loopback 的代理对端」一律拒(spoofed-loopback-host);本机直连(无代理头)与经反向代理的正常远程访问都不受影响(后者根本不走 loopback 分支)。
⚠️ 边界如实写在这里:以上全部是入口侧的把关(来源、有没有经过代理、票据、原句) —— 它们不覆盖「本机执行权已经失守」的情形:能在这台机器上以同一个用户跑进程、读文件的人,本来就在信任域里面(票据文件与 settings.json 对同机同用户可读;承诺句是明文存储,那把锁只造庄重、不保密)。这些防护挡的是"别的设备、别的页面、公网一侧够到这排 API",不是本机上的对手。

三层记忆各自什么时候更新 —— 「我刚说的话,最快多久能看到」:

层 · 看得到的是什么 · 什么时候变 L0 常驻人格 · 自称 / 对你的称呼 / 底线 / 规矩·习惯 / 承担(职业·职责·责任) / 语气 / 世界观 · 你改它就变(没定型时直接生效;定型后走手术门)。会话运行中不打断 —— 改动排队,该会话回到空闲才落进注入面 L1 开场记忆 · 从全库概述里按相关度挑出的 Top-K(有预算,永不塞全文) · 新会话开场时定稿一次,之后随空闲边界刷新(与下面 L2 同一把闸);库里的概述本身由概述器持续追平 L2 运行期记忆 · 本会话那一行(格式就是「记忆·开场」里那条自己:- [标签] (YYYY-MM-DD) 标题 —— 摘要首句 (来源: DSH 会话/短id) —— 标题指概述标题,不是会话原文首句) · 本会话原文随对话累进;那一行只在空闲边界重排回注入面

行里的「标题」从哪来(按来源分,细节见括号里的实现):

  • DSH 会话:该会话第一条真人发言经启发式清洗后的要点句 —— 先剥掉「晚上好器灵,」这类问候与称呼前缀,「你好」「在吗」这类无实义的首句会被跳过、往后取前 6 条真人发言里第一条有实义的,再在句读处收尾(不切半句);都被剥空才退回首句截断。主人手改过的标题会上锁,机器不再覆盖(lib\host\retitle.js 的 retitleCandidates()、lib\host\summarizer.js 的 summarizeDsh()、lib\host\memory.js 的 upsertOverview() 与 renameTitle())。
  • 文件导入:导出文件里原本的 `title` / `name` 字段,缺了才退回上面那套启发式(lib\host\import-file.js:142,171-173)。
  • 历史会话(旧扫描):该会话第一条真人发言的前 46 字硬截,没有启发式(lib\host\backfill.js 的 overviewRowFromFile())。
  • 机制数字(默认值):① 概述器周期 —— 有新增才重建,否则跳过;每 15 分钟一拍(可配,下限 1 分钟);② 空闲刷新 —— 只有「会话空闲 + 记忆版本变了 + 距上次刷新 ≥ 10 分钟」三条同时成立才重建。
  • 所以「刚说的话」最快约 10 分钟、通常 15 分钟内会出现在注入面,最坏约 25 分钟(刚刷过一拍 + 再等下一拍概述器)。运行中绝不打断 —— 刷新只发生在 运行中 → 空闲 那一刻。
  • ⚠️ 一个例外(也是"感觉它失效了"的来源):深摘要定稿之后,那一行不再自动更新 —— 概述器遇到已深摘的会话会避开它,免得把厚摘要覆盖成浅版。要它重跑:记忆中心 →「记忆浏览」→ 找到那个会话 → 点「重摘」,几秒后那一行就会重排。
  • 健忘感还有第二个来源:越长的会话,只取"概述标题 + 摘要首句" —— 这一行天然比整场对话"薄",不是它没读到,而是它只被允许显示这么一行。
接口侧(给要接线的人):GET /state(?sessionId= 取某会话,?scope=global 取全局)返回 snapshot(builtAt / mode / stale / chars)、idleRefresh(count 与 lastAt)与 memory.l1(该会话当前选中的行、分数、被省略条数)。面板现在也渲染空闲刷新次数 / 最近一次与 L1 当前选择(含分数与省略条数) 两栏。

后台自动行为(默认全开)与它们的开关

五件事装好就自己在跑,默认全开。 它们的键都在设置文件的 auto 子树里 —— 设置文件 = /settings.json(` 默认 $DSH_HOME/cache/dsh-ling`),只存你改过的部分;面板上的开关写的就是它(面板里改完会明确提示"需要重启插件")。

键 · 默认 · 什么时候跑 · 关掉 / 改小会怎样 auto.deepSummary · true · 启动自检通过后挂定时器 · `false` = 只有你手动点「深摘」才调用模型;已深摘过的会话不受影响,不丢数据 auto.deepIntervalMin · 30(最小 10) · 每隔这么久跑一轮自动深摘 · 调大 = 更省;调小 = 记忆更早变厚 auto.deepMaxPerRun · 2(1–20) · 每轮最多挑几个会话 · 调小 = 每轮更克制,调大 = 单轮更重 auto.bootProbeDelayMs · 12000 · 启动 12 秒后做一次 LLM 自检探针(通过了才挂定时器) · 0 = 立即试探;探针只验证"模型通道通不通",空回复判失败 auto.feedbackAuto · true · 事件驱动:宿主推 feedback/committed 时就处理 · false = 完全不接反馈成长回路

「探针」是一次性自检(只回答"模型通道通不通"),「深摘」是定期跑批 —— 两个键各管各的。

改动面有三层,优先级从高到低(改了不生效,先看这一条):

  1. settings.json 的 auto.* —— 面板开关写的就是它;
  2. 插件配置里的旧键:feedbackAuto / deepSummaryAuto / deepIntervalMin / deepMaxPerRun;
  3. 代码默认值。

生效时机(如实写):auto.deepSummary / auto.bootProbeDelayMs / auto.feedbackAuto 是启动期读取 ⇒ 改完要重启插件才生效;auto.deepIntervalMin / auto.deepMaxPerRun 对已经挂上的定时器不回溯 —— 下一轮启动后按新值走。

另外两条既有事实(不要读宽):

  • 深摘每次喂给模型的单次输入上限 = 26,000 字(lib/host/deepsummary.js 的 DEEP_CFG.inputCap);
  • 循环外调用(深摘、诞生仪式、语气归纳、自述起草)一律跟随你当前的模型选择;只有读不到当前选择时,才退回兜底 deepseek-official/deepseek-v4-flash —— 那是兜底,不是替你指定模型;它不是上面那五个键之一,也没有面板开关。
⚠️ 状态如实写:五件动作默认全开;settings.json 的 auto.* 与面板上的这五个开关属 1.7 工作区(未发布)—— 更早的版本里,能改的只有插件配置里的那几个旧键。

记忆树与复盘模式

这是本插件里跟"器灵的人格"无关、但决定"器灵想得起什么"的那一层。

结构:主干(恒一条,主线工作流)→ 枝(一条并行工作线,比会话大,一条枝可含多个会话)→ 主脉(由多条枝归并出的抽象节点)→ 连边(枝与枝之间的横向关联,如 力学 ↔ 材料科学,可带关系类型:相关 / 前置 / 对照 / 例证 / 取代)。

为什么值得:平铺的记忆库没有"离我多近"这个概念 —— L1 是全库裸排序,不区分"这轮做的工作"与"三个月前的闲聊",分叉会话更是记忆真空。记忆树把"归属"变成可检索的信号:

  • 血缘加权:检索时按"当前枝 → 目标枝"的相对位置取档 —— 同枝 1.0 / 祖先 0.7 / 旁系 0.4,score = 基础分 × 血缘系数 × 矛盾系数(1.6 系列起再乘一项枝系数,见下条);分叉会话照常注入,走的是它的血缘链而不是主干大排档。
  • 不连乘:血缘系数是单值,不沿路径累乘 —— 否则五层之后 0.4^5 ≈ 0.01,深层枝等于从记忆里消失。
  • 内容零改写(硬性不变量):除了"主脉提炼再次采纳会覆盖上一版"这一个例外(覆盖前自动归档),任何树操作都不改写任何一条记忆的内容;分枝 / 并脉 / 连边只增加"关系",从不"融合内容"。
  • 矛盾只标记不改写:一对记忆被判重复/矛盾时先标记;未复盘时检索以较新的那条为准(旧的一条降权 ×0.3,不删除;被驳回的与被标成重复的那一对不参与降权)。
  • 枝系数会真的改变排序(1.6 系列起):复盘期可以调(0~2,夹紧后写入)。它按 raw × 血缘 × 矛盾 × 枝系数 参与检索打分 —— 调低某条枝的系数,从这条枝的会话里提炼出的结论就会在记忆开场里往后排(1.0 = 不偏不倚)。⚠️ `0` 是"清空"不是"轻压":分数被算成 0 的非置顶条目会被当"无信息量"整条排除、不进开场(判据在 lib/host/l1.js 的 score > 0),所以别用 0 表达"压一压"。

复盘 — 不是一个开关,是"左会话 + 右复盘栏"这个状态:

  • 树平时只读(记忆中心始终只读);点「🌱 一键复盘 / 一键进入复盘」才解锁编辑,并把复盘开场材料(当前树 + 待理矛盾)复制到剪贴板,开个新会话粘进去就能开始。
  • 解锁后可做:改名 / 拖动换归属 / Alt+拖动只连边 / 并脉(归类到主脉) / 调权重 / 裁定矛盾 / 断续边,以及快照保存·恢复(删枝不在面板上 —— 请让器灵用 branch_edit 删,见下文「器灵侧的手」);拖动 / 连边之后可 Ctrl+Z 撤回这一步(撤销只覆盖复盘栏的拖动与连边;记忆中心是只读的,它自己那份 Ctrl+Z 监听早已删掉)。
  • 每步留痕:写清"改了什么、为什么"(器灵改树时 reason 必填;你在面板上改可以留空);树改动一律记进 branch_log,复盘时可审计 —— 器灵侧工具的改动记在器灵名下(「器灵改动了记忆树」),不会伪装成你改的。
  • 未同步笔数守门:面板记着"自器灵上次开口以来累积了多少笔结构改动还没交代"。有欠账时顶部红字提示,并且拦住"退出复盘模式" —— 改完树必须让器灵有机会看到(「互相复盘不应该被我绕过」)。
  • 注入感知:树变了之后,branchLogDigest() 把器灵还没看过的结构改动转成中文短句,附在器灵下一次开口的注入面末尾 —— 不需要推送,一开口就知道树动过。
  • 快照与撤回:树的保存 / 恢复(tree_snapshot)是"大改之前的保险";恢复前会自动把当前状态另存一份,所以恢复本身也可再撤回来。

也不必全靠手拖:

  • 「一键生成树」分两阶段 —— ① 机械分段(按时序相邻性把全库概述切簇,零成本、确定、可复现)② 小助手/模型只做"命名"这一件语义活,③ 你审核后才落库(建枝/主脉 + 挂会话);它只读概述索引、只产出候选,从不改写任何记忆内容。
  • 「归并零散枝」幂等:把名字含「零散」的枝统一收进主脉「零散会话」。
  • 主脉也可以有自己的记忆:由子枝概述提炼共性,先出草稿,采纳才写入(采纳需先解锁)。
  • 器灵侧也有自己的手:tree_read(看整树 / 某枝成员 / 待复盘矛盾)、branch_edit(分枝 / 并脉 / 连边 / 调权重 / 删枝)、recall(读回会话原文,见上文)、memory_forget / memory_backfill(遗忘 / 回灌)、rule_add(记规矩)、habit_propose / habit_resolve(习惯提议与表态)。tree_read 与 branch_edit 的描述里写着一条纪律:改结构必须给 reason,而且"只在主人明确要求、或正在和他一起复盘时"才动手。
依据:DESIGN.md §6.3(记忆树结构、血缘加权与"不连乘"、内容零改写不变量)、lib/host/memory.js(LINEAGE_ANCESTOR = 0.7 / LINEAGE_SIDE = 0.4 / CONFLICT_DOWNWEIGHT = 0.3、branchLogPending() 守门、setBranchWeight 的 applied:true)、lib/host/l1.js(血缘档位 × 矛盾系数 × 枝系数进打分)、lib/host/autotree.js(两阶段生成)、lib/host/tools.js(tree_read / branch_edit)。

找回原文:器灵能读回你当年的原话

概述是别人替你挑的,原文才是现场。 在 1.6 之前,器灵能"想起"的只有一行行概述 —— 想知道你当年到底怎么说的,只能靠猜。

recall 给器灵一只档案馆查询台,逐级下钻、一次一层:

给什么 · 得到什么 recall() · 会话地图 —— 本机有原文的会话清单(最近活动在前) recall({q:"…"}) · 按内容找 —— 默认搜本机库内原文层(毫秒级;只含"真人消息 + 有正文的回复",我自己的长回复中段搜不到),直接给命中片段;要看全量(覆盖全部事件、含子代理会话)才传 hostSearch=true(走宿主,较慢)。想不起"这话是哪次说的"时用它 recall({conv}) · 该会话的对话目录 —— 每轮一行:序号 · 时间 · 角色 · 前 70 字,不给正文 recall({conv, from, to}) · 那几轮的原文全文

它不替你挑"最重要的那 1 万字" —— 工具只给坐标系,选哪一段是器灵自己的判断(它知道自己在找什么,排序器不知道)。单次正文有上限,但溢出必报"还剩多少轮、下一次从第几轮读起",绝不静默截断。

四个不显眼、但决定成败的设计:

  • 真人判据:只有"真人发的消息"才算一轮 —— 注入的运行时上下文、任务标记、技能目录一律折叠成一行占位,否则目录会被过程噪音淹掉(实测:一个 150 轮的会话里,真人发言只有 5 条)。
  • 读不到时回落查库:磁盘上没有该会话日志时(网页端导入的历史就是这样),回落到记忆库里那份,并明说材质不同:⚠️ 来源:库内原文(库那份是残的 —— 只含真人消息与有正文的回复)。
  • 会话文件名代次:两条路各有一套 —— ① 会话历史扫描 + 库内回退(backfill / deepsummary)按 session(.v\d+)?.jsonl.zstd 匹配、取版本最高一代并跳过 0 字节;② `recall` 读原文走的是 lib/host/rawlog.js 里三代硬编码名单(session.v4 / session.v3 / 无版本)⇒ 它不解析版本号(将来出现 v5 时它会看不见)。本机三代并存,只认 v4 会让老用户看不见自己的旧会话。
  • 范围默认收紧:按内容找时默认只搜主人自己的会话,越界要显式传参,且被挡掉的条数一律报出(静默少给等于骗人)。⚠️ 两条限定:① 这层"可见集"过滤只在宿主路(hostSearch=true)成立 —— 默认的本机库路是全量(含网页端导入的会话),它的回执会如实标注"本机库全量"而不是谎称"已限定";② 无论哪条路,都默认排除当前会话自己(要看自己刚说的传 includeSelf=true)。

只读,零副作用:recall 不写库、不改树、不进注入面。

底座:1.6 同期把网页端一年半的历史(1,523 个会话 / 726.7 万字原文 / 时间跨度 2025-01 → 2026-09)补进了记忆库 —— 在此之前,它们在本机只剩一行概述。

图书馆:另一个库,只搜与核对

它不是第二个记忆库。 记忆存的是"你和器灵之间发生过什么";图书馆存的是"有出处、可回头对账"的参考资料 —— 标准里的一个取值、一条边界、一份项目规格、一个外部 API 的字段。判据不同,所以分成两个库、两套表:

  • 独立库:图书馆有自己的 library.db(与 memory.db 并列,在同一个数据目录里)。记忆库里的东西永不迁进图书馆,图书馆也不参与开场注入、不进注入面。两者之间没有隐式通道:不互相导入,也不互相引用。
  • 只搜与核对两只手:器灵侧只有 `library_search`(按关键词搜条目)与 `library_check`(对一条条目做当次核对)。没有新建 / 修订 / 撤下 / 改配置的模型工具 —— 维护面只在界面与本地接口上;代码根目录、URL 白名单这类东西不接受模型参数(那是管理员登记,不是工具入参)。
  • 核对认证的是"声明的那一处",不是"这条是对的":每条条目可以声明一份核对规格(定位 + 期望值 + 比较方式:行段定位或 JSON 指针定位;比较方式只有两种:`exact` —— 类型严格的结构比较(JSON 对象成员顺序无关、数组顺序有关;文本按字面精确比);`text-lf` —— 先把 CRLF/CR 归一成 LF 再精确比。两者都不 trim、不折叠大小写、不模糊匹配)。核对按它去取源、抽取、比对,结论只有三态:`ok`(声明的那一处与源一致)/ `wrong`(不一致)/ `unknown`(核对跑完了却判不了:取不到源、定位不到、编码不对、超时……) —— ⚠️ 取消不在这三态里:取消是"没跑完",不是一次 `unknown` 认证(见下面「取消」那一条)。回执里写死了这条边界:它只认证声明的那个比较,不认证解释性正文,也不构成通用真理 —— 说人话:"核对通过"是"这一处对得上",不是"这段话可信"。
  • "不可用"与"零命中"分开说:库打不开 / 未启用 / 运行时不在场,回的是明确的不可用(接口层是 503 语义),绝不回落成"搜到 0 条" —— 静默的 0 会让人以为"库里没有",而真相是"根本没查成"。
  • 修订与撤下都留旧的:修订产生新版本 + 新 id,旧版留痕指向新版;撤下只标记状态(带原因),不物理删除;完成的核对各留一行账(带当时取的源摘要、观测值、结论与理由)—— "完成"包括结论是 `unknown` 的那种;被取消的核对不算一次核对、不留记录(见下面「取消」那一条)。所以"它当年是怎么写的""上次核对时源长什么样"都查得回来。
  • 写面永远带"我看到的版本":修订与撤下必须附上你读到的那份内容摘要(content_hash),对不上就拒 —— 防的是"你看着旧版、改的却是新版"。
  • 严格配置,默认什么都没登记:用于"我们自己代码"条目的代码根目录,与用于官方文档条目的 URL 白名单,都是管理员登记的绝对路径 / 精确 URL,默认都是空的 —— 不登记就核不了,不存在"它自己上网找一份来对"。限额(条目 64 KiB、取源 2 MiB、选中文本 16 KiB 且不超过 200 行、单次核对 10 s、并发 2 等)在激活时校验:写错了响亮报错,不静默夹取。
  • 取消 = 没跑完,不是一次 `unknown` 认证:核对在飞时断开、取消或卸载插件,本次核对不写任何核对记录 —— 既不留 ok / wrong,也不留 `unknown`,只回一个"已取消"(带 cancelled)。"取消了"与"核完了(哪怕结论是 unknown)"在留痕里必须分得开:只有跑完的核对才计入"认证过"。
  • 远程核对会主动去你登记的地方取:官方文档类源走 HTTPS GET 你白名单里的那个精确 URL(不许带凭据、不许跳转、私网地址拒 —— **还拒 localhost / *.localhost / *.local / *.internal,并对 DNS 解析结果与连接后的 remoteAddress 做二次校验(实现见 `lib/host/library-checker.js`:地址校验 + 解析结果钉住 + 连接后复核))。发出去的到底是什么,必须说准:这个 GET 会把你登记的那个 URL(连同它带的查询串)与一次普通 HTTPS 请求的元数据(例如对方可见的连接来源 IP,以及 Accept/Accept-Encoding 请求头)交给那台服务器 —— 换句话说,对方能看到你的 IP,也看得到这个 URL 本身;但它不会附带条目正文、记忆内容、人格档案或任何本机文件。⚠️ 因此:URL 里若放了不该外传的东西(比如把 token 写在查询串里),它就会被发出去** —— 白名单是你自己登记的,请当成"公开可见"来写。白名单默认为空 ⇒ 图书馆默认不发生外部取源访问。
  • 边界(如实写):这一版的包版本是 `1.7.2`(工作区候选);上一版已于 2026-10-17 发布上 npm(latest),1.7.2 尚未发布。而"没发布"不等于"没验过" —— 第五个标签(图书馆)已于 2026-10-07 在本机既有 3080 上真机验收走通(建档 / 核对 / 撤下留痕 / 撤销弹窗全过),实现与交互测试同时也在源码与离线测试里。两者别混读。另有一条边界要说清:"不把记忆库里的旧数据迁进图书馆"不等于"禁止网页端来源的资料" —— 一条资料能不能进图书馆,看的是它有没有具体、可追责的出处,与它从哪个渠道来无关。
机制依据与源码证据(表结构、核对三态与各条判据的实际位置、取消与错误码落在哪一行)集中在 DESIGN.md 的「图书馆:机制与源码证据」一节 —— 本节讲"用户能感觉到什么",那一节给出"代码里是哪一行"。

快速开始

前置:Node ≥ 22.15(用到内置 node:sqlite 与 node:zlib 的 zstdDecompressSync —— 两者都到位才加载得起来;23.x 线需 ≥ 23.8;精确范围见 package.json 的 engines)· 已安装 DeepSeek Harness 并至少启动过一次。

安装入口是官方 CLI 的 dsh plugin add(它在 profile 目录里转发给 pnpm,并自动把本包登记进 profile 的 dsh.profile.bundles 层列表)。两条路径按你的身份选一条,不要两条都走。

A · 从 npm 安装(推荐给普通用户)

# 1) 装进你要用的 profile(缺 --profile 时用当前默认 profile)
dsh plugin --profile web add @nightpainters/dsh-ling

# 2) 只验证配置层,先不启动
dsh --profile web --dump-config     # 应能看到一行 "# == @nightpainters/dsh-ling" 层

# 3) 给 DSH 核心打补丁(**两行** —— `PATCH_TARGETS` 里两个目标;可取证访问日志要用;幂等;这一页各步里**只有这一步**自带回滚)
node ~/.dsh/profiles/web/node_modules/@nightpainters/dsh-ling/tools/apply-access-log-patch.mjs
#    不打也一切照常 —— 只是访问日志为空;但**会提醒**:侧栏挂常驻 ⚠(判据 installed===false 且 reason 为 patch-missing;只有**有意关掉** config.accessLog.enabled=false ⇒ reason 为 disabled-by-config 才不染红)
#    DSH 升级/重装后需重跑一次(补丁随包被覆盖)
#    ⚠️ 从 npm 装的人跑不了 `npm test` —— 本包 `files` 白名单不含 tests/(见下)

# 4) 重启宿主(host),然后浏览器 Ctrl+Shift+R 硬刷
⚠️ 「可回滚 / 先备份」的确切范围(别读宽了 —— 2026-10-05 现场复核):两套步骤里只有打补丁这一步自带回滚 —— 脚本动手前先把被改的那个 core 文件另存一份 .e4-access-log.orig(同名备份已存在时不覆盖);要撤,就在上面那条命令后加 --revert(手动跑,详见下文「访问日志与核心补丁的总开关」)。⚠️ 撤法是"删掉插进去的那几行",不是把整份备份还原回去(备份只用来对拍:不一致时它会如实报 ⚠ 与备份不一致)。安装与升级本身既不先备份、也不会失败自动回滚:插件只读 profile 的 package.json 与 cordis.patch.yml 做自检(tools/check-install.mjs),从不改写、也不备份它们 —— 那两处改动是你手动做的,撤回也是手动(删掉那一行 + 撤依赖,见 DESIGN.md §10「回滚」)。想留后路,动手前自己把这两个文件复制一份。锚点漂移 / 歧义时脚本的行为是一个字都不改(安全失败),不是"改了再回滚"。
发布名是 `@nightpainters/dsh-ling`,不是 dsh-ling —— npm 上 dsh-ling 这个名字已被他人占用。
插件在配置里的注册 id 仍是 dsh-ling,两者不是一回事。

B · 从源码安装(给要改代码的人)

# 0) 取得代码
git clone https://github.com/NightPainters/DSH-Ling

# 1) 装进 DSH 的 profile(把 <路径> 换成你 clone 下来的实际路径)
cd ~/.dsh/profiles/web
pnpm add file:<路径>/dsh-ling      # 或 npm install file:<路径>/dsh-ling
#    若 dsh 没有自动登记这一层,在 profile 的 cordis.patch.yml 追加:
#    - insert:
#        - id: dsh-ling
#          name: '@nightpainters/dsh-ling'
#    (id 保持 dsh-ling;name 用包名,Node 才解析得到代码)

# 2) 给 DSH 核心打补丁(**两行** —— `PATCH_TARGETS` 里两个目标;可取证访问日志要用;幂等;这一页各步里**只有这一步**自带回滚)
cd <路径>/dsh-ling
node tools/apply-access-log-patch.mjs   # 只在 core 的 /api 栅栏处插一行可选调用
#    不打也一切照常 —— 只是访问日志为空;但**会提醒**:侧栏挂常驻 ⚠(判据 installed===false 且 reason 为 patch-missing;只有**有意关掉** config.accessLog.enabled=false ⇒ reason 为 disabled-by-config 才不染红)
#    DSH 升级/重装后需重跑一次(补丁随包被覆盖)

# 3) 重启宿主(host),然后浏览器 Ctrl+Shift+R 硬刷
备份与回滚的确切范围见上文路径 A 之后那段(要点:只有核心补丁这一步可回滚;安装本身既无自动备份、也无自动回滚)。

自检安装是否成功(下面四个脚本不在 npm 包内,只有源码路径能用:tools/check-install.mjs / tools/check-publish.mjs / tools/clean-archived.mjs / tools/check-peers.mjs;tools/apply-access-log-patch.mjs 则在包内,从 npm 装的人也能直接跑,见上面第 3 步):

node tools/check-install.mjs          # 只读自检:入口是否直连仓库、patch 是否启用、数据目录是否就绪
node tools/check-install.mjs --fix    # 若入口变成"复制副本"(DSH 升级/回退常见)则改名备份为 <入口>.bak-<时间戳> 后重挂链接;⚠️ 重挂若失败,备份留在原地但**不会自动改回**,按它打印的 mklink /J 手动修
node tools/apply-access-log-patch.mjs --check   # 访问日志核心补丁是否在位(未打/锚点漂移时退出码 2)
node tools/check-publish.mjs          # 上传/发布前跑一次:揪出记忆库、设置、密钥、绝对路径等不该公开的东西
node tools/check-peers.mjs            # 声明与宿主对不上?退出码三态:0 合规 / 1 有不合规(宿主会把本插件丢进 skippedBundles,不激活)/ 2 无法判定(2 不算通过)
tools/check-peers.mjs 只做一件事:复刻宿主对 peerDependencies 的那道闸门(判据照抄宿主语义,用的是宿主自己那份 semver),回答"这个插件在你的宿主版本上会不会被丢进 `skippedBundles`"。它存在的理由是宿主的行为方式:宿主对每个名字形如 @deepseek-ai/dsh / @deepseek-ai/dsh-* 的 peer 跑一次 semver.satisfies(宿主版本, 你声明的区间, { includePrerelease: true }) —— 判的是「宿主版本是否落在你声明的区间里」,不是"你的写法合规不合规";判不满足 ⇒ 整个 bundle 进 skippedBundles、不激活。线索分两条路径:从命令行启动会往 stderr 打一行 skipping profile bundle …(容易被忽略,但不完全静默);而应用自持 profile 的那条装载路径一个字都不打 —— 那才是真正难查的一种,表象常常只有"它突然不见了"。⚠️ 所以"改成 >= 就安全"是错的:开区间同样可能不满足(取决于宿主版本);^ / ~ / 钉死版本只是更容易被将来的一次宿主升级甩出去。

跑测试(不需要宿主,全部离线):

npm test        # 57 套:人格组装(含冻结门 · L1 选择)/ 归档清理(人工)/ **归档·遗忘·回灌** / 模式(只改档位不改模型)/ **跨插件挡位服务(dsh-ling/mode)** / 时间戳归一 / 空闲刷新 / 规矩·习惯 / 习惯生成器(A 数出来 + B 想出来)/ 会话内工具 / **接口守卫** / L1 行瘦身 / 记忆库 / 记忆标题与排序 / 导出合并 / 文件导入 / 导入记录 / **活行(那一行跟着会话走)** / **会话文件名代次(会话历史扫描)** / 网页端补摘要 / 深摘要 / 概述器 / 诞生仪式 / **职责字段(承担)** / 语气 / 时间锚 / **clock skill(时间查询)** / 捕获差分 / 反馈成长回路 / 语料建议 / **小助手地址配置** / **注入面中和** / **记忆分枝与血缘** / **记忆树(主脉·连边·矛盾)** / **一键生成树** / **可取证访问日志** / **补丁锚点(cohort 候选)** / **请求体上限(超限排空 → 413)** / **导入切批(按字节 × 条数双限)** / **投递留痕(每次注入落一条:注入了谁·为什么是它·多少字)** / **会话原文直读(三档下钻 + 库内回退 + 旧版会话代次)** / **深层库条目(结论层落点:读写·幂等·取代·加固)** / **检索档(按内容找会话:条目 + 会话同查·范围限制与降级)** / **图书馆存储层(独立库:生命周期·限额·检索·留旧·不可用≠零命中)** / **图书馆跨连接事务(陈旧修订拒绝·完成的核对留档·序号一致)** / **图书馆核对(三态判定·只认证声明的那个比较·限额与取消不写)** / **图书馆运行时(禁用/不可用降级·接线与关闭顺序)** / **图书馆接口与工具(端点形状·状态码映射·两个工具的参数边界)** / **图书馆界面(独立库的面:分页无伪总数·必填与 JSON 校验·推迟应答竞态)** / **图书馆导入引擎(料源登记与导入)** / **图书馆独立 SQLite(隔离库:只读打开·表必须点名)** / **图书馆浏览(分页与零命中)** / **热重载补绑(工具重挂载:存量 agent 补绑)** / **文档灌入通道(结论层 C 档:两道污染闸·三级去重·出处核验·累计预算)** / **后台自动行为开关(auto:五键默认值·三级优先级·白名单夹取·四条真 HTTP 回归)**
npm test 需要 tests/,而本包 files 白名单不含 `tests/` ⇒ 从 npm 装的人跑不了测试。要跑测试请走路径 B(clone 源码)。
计数口径:`npm test` 链里登记的段数(即 package.json 的 scripts.test 那一行按 && 切分后有多少段就是多少套,不再数 tests/ 目录)—— 实测 57 套(即链里 57 段),且 57 套全部已登记在 `npm test` 链里(tests/harness.mjs 与 tests/vm-light.mjs 是被复用的辅助模块,不计套数)。加了一套新测试记得同步登记,否则 npm test 会静默少跑一套 —— 这份登记是手写的(package.json 里 test 脚本那一行字面量清单),漏登记不会被自动发现,没有安全网兜底。
⚠️ 2026-10-15 实测订正:此前这里写着 49、而链里当时已经有 53 段 —— 也就是这段注释自己警告的坑真发生过:套件登记进去了,这个数字却没人回头改,四次都没被任何判据抓住。当天新增 deep-import-docs.test.mjs(C 档文档灌入)⇒ 54。这个数字没有判据替你看着,改链时必须顺手改它。
⚠️ 2026-10-16 再 +1:新增 tests/auto-config.test.mjs(后台自动行为开关:五键默认值 / 三级优先级 / 白名单与夹取 / 四条真 HTTP 回归)⇒ 计数从 54 变成 55。同日还抓到一条真红:修复批次把 tools/compliance-spec.mjs 的出站基线从 6 抬到 23 时,漏改了 `tests/tools.test.mjs` 里钉住的字面量 ⇒ 整链自那时起就是红的,直到当日修复。同一个坑两次(数字写死在多处、改一处忘另一处)—— 改链、改基线时,同批把所有钉死它的地方一起改。