# 更新记录 本项目遵循语义化版本:`MAJOR.MINOR.PATCH`。 ## 1.5.0 **记忆树闭环 + 器灵侧正规通道 + 可取证访问日志** > 一句话:记忆树从「看得见」走到「器灵能亲手改」;时间从「每分钟对着人嚷嚷」改成「按需查」;访问日志从「一行都没有」变成「可取证」。 ### 一、主脉提炼(R7 · D9-b 第 4 项) 主脉不只是一层目录 —— 它可以有自己的记忆。 - 新增 `POST /vein/distill`:取该主脉**及其子枝**的概述,交给模型提炼共性;四道守卫分别回应「主脉不存在 / 不是主脉 / 没素材 / 那类失败」; - **提炼结果不直接进记忆**,先进 `vein_suggestions` 候选表(schema **v11**),主人**可改后采纳**;驳回只改状态; - 采纳写入 `conv_overview(source='vein', origin='vein-distill')` 并挂到该主脉自己名下 —— 天然享受标题/锁/权重/深摘;`sources` 记录派生来源(哪几条枝,截 40 条); - 提炼用**器灵自己的全局模型**(不是小助手):「你在给自己做整理,别让小助手代劳」—— 判断类工作归器灵,压缩/分类类留给小助手; - 侧栏新增「✦ 提炼主脉」(选中主脉时出现;选中**枝**时显示它所属的主脉并直接对那条提炼)与审核卡片; - **采纳需要解锁**(进入复盘模式)—— 它写记忆、改归属,与改名/并脉同级;**提炼不锁**(只产草稿)。 ### 二、器灵侧正规通道(Part G · G1) > 起因:复盘时器灵要改记忆树却**没有任何 agent 侧工具** —— 只能伪造成本机进程走 UI 的 HTTP 口、或者直接开库;留痕还记在主人名下。 - 新增 4 个会话内工具:**`tree_read`**(读树 / 读某枝会话 / 看待理矛盾)· **`branch_edit`**(建枝 / 改名 / 挂载 / 连边 / 断边 / 调权重 / 删枝)· **`memory_forget`**(软遗忘)· **`memory_backfill`**(回灌);工具面 3 → 7; - 把树操作从 HTTP handler 抽成 `lib/host/tree-ops.js`,13 个 handler 变薄壳 —— **界面与工具共用同一份实现、同一份留痕**,零行为变更; - `branch_log` 新增 **`actor`** 列(schema **v12**):器灵改的写「器灵」,主人改的写「主人」; - **永不彻底删除**:`memory_forget` 先把原文与概述归档到 `forgotten/<时间戳>-<原因>/`(`turns.jsonl` 逐行 JSON、可 grep,`README.md` 写改动日志与还原方法),**再**在库里打标记 —— 两条独立还原路径(撤销标记 / 按 jsonl 重建); - `memory_backfill` 按「比遗忘更危险」处理:回灌前先归档当前状态,写回的概述带 `origin=backfill-v1` 标记,**绝不夺走用户态**(importance / hit_count 原样保留); - 修复**工具注册不生效**:原先只注册在全局层,而 Web 端按 agent preset 用 `restrict({allow})` 组工具 ⇒ 全局工具被静默过滤(连更早的 `rule_add` 也从未出现)。现在每个 agent 创建时在**它自己的 scope** 补注册一份。 ### 三、时间锚:从「每分钟嚷嚷」到「按需查」 > 「这就像一块手表,它该在人需要的时候被看一眼,而不是总对着人嚷嚷现在几点。」—— 用户 - **`[现在] … HH:MM` 从 system 前缀移出**:system 只留 `[今天] 2026-09-23 周三`(**跨天才变**),`[现在] 时段 · 距上次对话:档位` 改挂 `systemPrompt.context()`(会话中、末条消息,每轮重算); - 实测收益:**system 侧时间锚逐轮变化 1440 次/天 → 0 次/天**,不再每请求作废整段前缀(连同 13.8k 的工具 schema); - `formatGap` 保留原精度,新增 **`formatGapCoarse`**(按**日历天**分档:`刚刚 / 今天早些时候 / 昨天 / N 天前`)—— 纯小时档会把「隔 36 小时」错报成「昨天」; - 顺带修掉一个真病灶:原先时间锚可能来自**更早某次注入的快照**(实测:深夜报「凌晨 1:10」而实际 `00:47`);现在是每轮重算; - 新增 **`clock` skill**(嵌入式,随插件注册):要精确到分钟的时间时主动查,平时不打扰。 ### 四、复盘界面与交互 - **树多开 + `Shift+C`**:侧栏枝展开由「单开」改「多开」(各节点独立),`Shift+C` 一键折叠全部(输入框内不触发;只清展开态、保留成员缓存); - **主脉认得出来**:侧栏列表行首 `🌱 主干 / ◆ 主脉 / · 枝`; - **连边不再铺满**:有连边的节点显示 `⇄N` 徽章,选中该行才展开「连到哪里」; - **记忆中心选中与侧栏对齐**:直接点击 = 只选这一条(`Ctrl+单击` = 加选/减选); - **工具条左对齐**(就地覆盖,不动全局 `.btnrow` —— 它被另外 15 处按钮行复用); - 主脉提炼结果紧跟「✦ 提炼主脉」入口;「最近改动」移到「树的保存 / 恢复」之下; - **保存快照不再弹「有 N 笔改动还没跟器灵说过」**:保存不改树 ⇒ 留痕照写但**不计债**(`action='snapshot'` 排除);恢复 / 改归属 / 改名等真改动照旧计入。 ### 五、修复 - **前端黑屏(React #310)**:新增的 `Shift+C` effect 落在 `LingReviewBody` 的 early return **之后** ⇒ 首次渲染 26 个 hook、数据到达后 27 个 ⇒ React 卸载整块面板。hooks 已上移到所有 early return 之前,并在源码内写死位置纪律; - **主脉提炼「模型调用失败」**:换通路时只改了取值、没改判据(`llmOnce` 返回 `{text}`,没有 `ok` 字段)⇒ 每次提炼都走失败分支(而模型其实答了)。判据已随返回形态整体走,并加**三档重试**(`off/1200` → `low/2400` → `off/2400` + 裁短材料),失败回执带 `attempts` 便于诊断; - **采纳后记忆「不见了」**:`resolveVeinSuggestion` 只写概述、没写归属 ⇒ 解析链落到**主干**;`MEM_SOURCES` 与客户端来源下拉也缺 `vein`(那条记忆当时**无法置顶/删除**、也没有筛选入口)。三处一次改齐,存量已回填; - **`tree_read` / `branch_edit` 八条缺陷**(第三方实测报告 7 条 + 复核发现的第 8 条):`lossless()` 兜底(返回值带 `undefined` 会被平台的无损 JSON 校验判成整次失败,而**写已提交** —— 调用方重发即重复写)· 成员计数取值 · `no-match` 与空枝分离 · `limit` 生效 · 行尾短 id · `links` 回传 · `kind` 按 action 分流 · **create 回执丢失新枝 id**(「建枝 → 立刻改它」链路断裂)。 ### 六、E4 · 可取证访问日志(`lib/audit/`) 起因:2026-09-24 19:49:51 进程被 `dsh-setting-restart` 调度重启,8000+ 文件的全机扫描也答不出「是谁调用的」—— 因为**整个系统没有一行访问日志**。 - 新增 `lib/audit/access-log.js`:JSONL 落盘 `$DSH_HOME/logs/access-YYYY-MM-DD.jsonl`(按天切 · 总量上限删最旧 · 当天永不删),让「谁 · 何时 · 从哪台设备 · 调用了哪个端点 · 结果如何」事后可回答; - 字段:`at`(本地时间+毫秒+偏移)/`method`/`path`/`status`/`rejection`(`untrusted-host` · `no-cookie` · `bad-cookie`)/`ip`/`fwd`/`host`/`ua`/`deviceCookie`/`deviceId`/`bytes`/`ms`;高危端点(`settingRestart/restart`、`/dsh-ling/*`)额外打 `risk`,`/api/remote.mux` 升级请求打 `phase:"upgrade"`; - **只记「发生了什么」,不记「内容是什么」**:请求体 / 响应体 / cookie 值一律不落,查询串一律丢弃; - **覆盖边界**:只记 `/api` 与 `/api/remote.mux` 的 upgrade —— 静态资源与非 `/api` 路径不记; - `tools/apply-access-log-patch.mjs`:给宿主 core 打两行**可选调用**(`/api` 唯一入口 + `/api/remote.mux` upgrade),支持 `--check` / `--revert` / 幂等重放;插件缺席或补丁未打时那两行是 no-op,**宿主照常启动**; - 同步落盘(`appendFileSync`):异步缓冲会丢掉「调度重启后进程退出」前的最后一行,而那一行恰恰最重要; - `tests/access-log.test.mjs`(已挂进 `npm test`):四态可辨、字段形状、查询串丢弃、超限裁剪、坏目录静默降级、写入后元数据当场可见。 - `deviceId` 取值的**权威形态已核实**(远端 UI 的 `pairing.ts` 用 `randomToken()` 生成 32 位十六进制明文写进配对 cookie),单测用**合成值**钉住该形态 —— 不拿真机 deviceId 当测试夹具(那是凭据)。 - **日志会自我标注良性噪声**:命中自家**部署姿态自检**签名的行多打 `probe:"posture"`(`POST /api/session.list` + loopback + 无 UA + 被 403 拦下,五要素齐全才标注)。该探针由远端 UI 插件每次启动后自带、`403` 正是它的健康结果 —— 标注**不代表放行**,该行 `status` 仍是 403(任务书 §7.5)。 - **日志标注调用通道**:没有 `user-agent` 头的行多带 `channel:"no-ua"`。它只陈述"**非浏览器直连**"这一事实 —— 这一类**混装两种调用者**(进程内客户端 / 经远端通道代理进来的设备,代理按设计不转发 UA),故**不指认是哪一台**;按端点语义判读的口径见任务书 §7.5。 - **观测边界(2026-09-25 真实流量见证)**:远端桌面通道的内侧代理按设计只重建 `host` / `content-type` / `content-length` / `accept` 四个头 + 它自己的鉴权 cookie,**不转发 UA、丢弃 Origin/cookie/Sec-Fetch** ⇒ 凡经代理进来的远端设备(手机/平板/物理机)在内侧日志里一律是 `ip:127.0.0.1` + `ua:null` + `channel:"no-ua"` + 授权通过:**能看出"有远端在用",看不出"是哪一台"**。要按设备归因,需在代理侧透传一个**由代理自己写入**的标识头(任务书 §7.6)。 > 注意:core 那两行补丁位于 `node_modules` 内,**DSH 升级后需重跑补丁脚本**;补丁缺失时插件启动会 `console.warn`。 ## 1.4.1 **审计修复轮 + 复盘守门:改动必须让器灵看到** ### 一、红蓝对抗审计的中危项(27 条) 1.4.0 发布前修掉了 8 条高危,随后对全量代码做了一轮红蓝对抗(数据层 / 端点与注入面 / 前端,另加一路迁移安全验证),中危及以上本轮全部关闭: **数据层** - `branchMembers` 的 `limit` 改为**两路查询共用总预算**(原先各吃一份,展开主干时条数是配额的 2 倍); - 新增 `branchMemberCounts()`,与展开**共用同一份谓词** —— 界面上的「N 个会话」不再和展开条数对不上; - `resolveConflict` / `reparentBranch` / `setConvBranch` 补**存在性检查**,对不存在的枝不再假成功; - `tree_snapshot` 新增 `kind('manual'|'auto')` 列,软上限裁剪**只裁非 auto 的一条**,并拒绝删除最后一份自动快照; - `weight_scale` / `visibility` 明确标注为**预留字段、当前不参与打分**。 **端点层** - 写 0 行时返回 `{ok:false, reason:'no-match'}` 且**不写假留痕**; - `judge` / `name` 加**模块级单实例锁与整批 deadline**(120s / 300s),重入直接回 `busy`; - `apply` 校验 `parentId`(跳过并计数)与 `bucketId`(去重); - `assign` 两侧都做存在性检查(会话侧走 `sessionBranchMap()`,因为 `sessionMeta()` 对未知会话会返回伪造的默认对象)。 **前端** - 拖动源改用 ref 在 `onDragStart` 统一记录 —— 修好「撤回 0 项」; - 拖到会话行解析为**其所属枝**;失败不再假报成功; - 钩子清理链式追加(`__lingCleanup`);成员加载失败可重试;只对成功项记账; - 删除约 140 行不可达死代码与两套并存的选中真值。 ### 二、注入感知水位改为按会话 原先水位是**全局键**,后果有二:会话 A 消费过的改动、会话 B 再也看不到;且 `invalidateSession` / `refreshIdleSnapshot` 这类「只重建、没开口」的路径也会消费水位。 改为**渲染与投递分离**: - `snapshot.js` 只读 `branch_log_wm.`、**不再写入**,并把本段渲染到的 `logMaxId` 带出; - `inject.js` 新增 `advanceLogWatermark()`,**只在文本真正要进模型时**推进(四条投递路径),单调不回退。 语义取舍:**宁可重复,不可丢失** —— 只重建的快照若已含改动段,下次重建可能重复呈现,但绝不静默吞掉主人的操作。 ### 三、复盘守门:改动必须让器灵看到 > 「互相复盘不应该被我绕过,动记忆树不能是随意的,应该是慎重严肃的。」—— 用户 2026-09-22 - 侧栏显示**未同步笔数**(金色警示块):自器灵**上次开口**以来累积了多少笔结构改动; - 未同步时**硬拦「退出复盘模式」**,提示「在左边说一句话让它看到」; - 判据链:`branch_log_wm.last`(投递时写入)→ `GET /branch-log/pending` → 侧栏; - **升级后首次运行不再误报**:该键不存在时以当前最大留痕 id 作起点,机制上线前的历史改动不计入「未交代」。 ### 四、留痕显示修正 `nm()` 原先对**一切查不到名字的 id** 都回退成「(已删枝 …)」,而 `branch_log` 的 `before`/`after` 还会装**快照号**(`#9`)与**记忆条目 id**(`026ebd86`)。现按 id 形态分流(真枝名 / `快照 #N` / 已删枝 / 条目),并补齐 `snapshot` / `restore` / `assign` / `delete` 四个 action 的中文措辞。 ### 五、测试 仍为 **29 套**;本轮新增 10 条断言,其中 `kvWrites.length === 0` 是「渲染期不写任何 kv」的判决性断言。 ## 1.4.0 **记忆树:平铺的记忆库长出了结构 —— 分枝 · 归并 · 血缘 · 复盘** ### 一、为什么要有树 旧版记忆库是一张**平铺的表**(以 `(source, conv_id)` 为主键),后果有三: - 召回**没有"离我多近"的概念** —— L1 是全库裸排序,分不出"这轮做的工作"与"三个月前的闲聊"; - **分叉会话是记忆真空** —— 平台 fork 出来的会话既不写入、也不注入(两道闸都按 `parentSession` 直接丢弃); - 无法表达**"这几条属于同一段工作"** —— 没有跨会话的工作线概念,只能靠 `source` 粗分。 1.4.0 引入**记忆树**。关键认知:**树的节点不是知识分类,是"工作线"** —— 同一段对话既有知识属性(讨论了什么)也有经历属性(什么时候、和谁、为了什么),树按**经历**组织,知识通过主题名与连边表达。 ### 二、数据模型(schema v5 → v9) | 表 / 列 | 作用 | |---|---| | `branch` | 枝 / 主脉 / 主干(`id` · `name` · `kind` · `parent_id` · `fork_at` · `weight_scale` · `visibility` · `status`) | | `session_meta.branch_id` | DSH 会话 → 枝 | | `conv_branch` | **非会话源的枝归属覆盖层**(历史网页端 / 导入条目) | | `vein_link` | 枝与枝之间的横向连边(带类型与理由) | | `memory_conflict` | 矛盾标记(不改写内容,只记关系) | | `branch_log` | 结构操作**留痕**(复盘可审计:改了什么、为什么) | | `tree_snapshot` | 树的保存 / 恢复 | **归属解析**两条链并存:DSH 会话走 `session_meta`,历史条目走 `conv_branch` 覆盖层;优先级 `conv_branch` > `session_meta` > 主干。**记忆行不冗余存 branch**,靠 `会话 → 枝` 推导,枝归属变化时无需回写海量记忆行。 ### 三、血缘加权:枝读祖先 检索时按"当前枝 → 目标枝"的**相对位置**取档: | 当前枝 → 目标枝 | 档位 | 权重 | |---|---|---| | 同枝 | `same` | **1.0** | | 目标枝是祖先(含主干) | `ancestor` | **0.7** | | 旁系 / 祖先读后代 | `side` | **0.4** | `score = 基础分 × 血缘系数 × 矛盾系数 × 枝系数` **刻意不连乘**:血缘系数是**单值**,不沿路径累乘 —— 否则 `0.7^n` 会随深度指数失真(五层后 `0.4^5 = 0.010`,枝等于从记忆里消失)。深树的 min-max 路径搜索留作后续。 ### 四、一键生成树:机械分段 + 器灵命名 主干太粗(1593 条)时,用**两阶段**长出结构: 1. **机械分段**(零模型调用):按源分组、组内按时序,相邻间隔超 `gapDays` 或单簇超 `maxSize` 即切 —— 分段是机械活,确定、可复现; 2. **器灵批量命名**:每批若干簇调本机小助手起名,分批推进、可中断; 3. **主人审核**:逐簇可改名字,确认后应用建枝。 名字跟随记忆标题的处置 —— **器灵起 / 主人可改**。名字含「零散」的簇自动归并到同一主脉「零散会话」下(承认它不是工作线,但**保留而非丢弃**)。 ### 五、复盘模式:右栏操作台 **"进入复盘模式"不是一个开关,是"左会话 + 右复盘栏"这个状态。** - **记忆中心《记忆树》始终只读**(看结构、看连边、看矛盾,不做修改); - **右侧栏「器灵 · 复盘」是操作台**:选中节点改名 / 并脉 / 调权重 / 连边;扫疑似重复;模型判定;归并零散枝;快照保存与恢复; - **操作方式**:拖动 = 改归属(move),`Alt` + 拖动 = 只连边(link),`Ctrl` + 点击 = 多选可批量拖,`Ctrl+Z` = 撤回一步; - **拖动有预提示**:悬停目标出现青色虚线框,与琥珀色选中框区分; - **树的保存 / 恢复**:快照只存**结构**(枝 / 归属 / 连边),不存记忆内容;恢复前**自动存一份当前状态**,故"恢复"本身也可撤。 ### 六、注入感知:器灵看得见主人改了什么 人工改树后,`branchLogDigest()` 把**器灵还没看过**的结构改动(水位按自增 id,不重复注入)格式化成中文短句,由 `snapshot.js` 附在注入面末尾: ``` 【主人刚改动了记忆树】 - 把「零散记录」从「主干」下挪到了「零散会话」下 ``` **为什么不做推送**:客户端插件无法往会话注入消息;而复盘对话密集、每说一句就注入一次 ⇒ **感知延迟只有一轮**,体感即实时。器灵不打断主人,只是不漏看。 ### 七、其他修复 - **分叉会话不再是记忆真空**:两道闸由"按 `parentSession` 一律丢弃"改为"**写入所属枝 + 注入血缘链**";seed(继承前缀)由水位 + `isOwnSeq` 双重拦截,父会话历史不会灌进枝; - **归档会话不再被召回**:`selectL1` 与矛盾列表均排除 `archived=1` 的会话(此前仅有界面标记,召回路径**从未过滤**); - **会话级条目标题三级回退**:概述标题 → `dsh_turns_raw` 首条用户消息(压缩为单行、≤40 字)→ 空;**空壳会话不再显示**(既无原文也无概述的会话在数据层过滤掉,而不是在前端显示短 id); - **`{{…}}` 注入面中和**(1.3.1 起)对齐到新的注入段:所有进系统提示的文本统一走 `neutralizeMustache()`。 ### 八、测试 新增 3 套:`tests/branch.test.mjs`(分枝与血缘)、`tests/tree.test.mjs`(主脉 / 并脉防环 / 连边 / 矛盾标记 / 覆盖层)、`tests/autotree.test.mjs`(分段边界 / 命名解析 / 覆盖层只读性),测试套件 26 → **29** 套,全绿。 ### 九、发布前对抗审计 1.4.0 发布前跑了一轮红蓝对抗审计(三路红队 + 迁移安全实测 + 蓝队复核),发现并在发布前修掉: - **写端点缺少方法校验** —— 宿主路由契约里没有 method 字段,插件守卫也不读它,于是一个 `` 就能触发写操作。已集中声明写端点白名单,非 POST 一律回 `405 method-not-allowed`(列在鉴权之前); - **`restoreTree` 会静默删枝** —— 恢复一份快照会删掉"快照之后新建的枝",而会话仍指向它们,留下**看不见、数得到、权重还满格**的悬空引用。已改为**只把多出来的枝 reparent 到主干**,并在返回值里回报 `detachedToTrunk` 计数; - **注入面泄漏内部 id 与换行** —— `branchLogDigest` 的两个分支漏做名字映射(会把 `vein:` 原样送进系统提示),且未压平换行(可伪造独立行)。已补齐映射、压平并限长,写入侧(`logBranch`)同步净化; - **前端四处字段错读** —— 把数组当计数(渲染成 `[object Object]`)、不校验 `ok` 就宣告成功等; - **补上删枝能力** —— 非空枝默认拒绝并回报影响面(`children` / `sessions` / `convs` / `links`),`force` 时归属回退主干,不留悬空。 **迁移安全性实测**:取 9 月 14 日的老库(7 张表)用当前版本打开,直接升到 v9 —— 四类数据(概述 / 原文 / 会话 / KV)**零丢失**,六张新表与三个新列全部就位,`trunk` 自动建好。 ## 1.3.1 **注入面加固:记忆里的 `{{…}}` 不再能让整个会话失效** ### 一、这颗雷是怎么来的 系统提示模板用 `{{name}}` 占位符做变量插值。宿主的插值实现遇到**未注册的变量名**会直接抛异常,而这一步在 agent 的**每一步**里执行、**外层没有兜底** —— 于是一段文本里只要出现 `{{time}}` 这样的字样,该会话的每一次模型调用都会失败。 而记忆库里装的是**真实对话原文**:导入的历史、网页端抓取的问答,里面完全可能出现 `{{pool}}`、`{{rejected}}`、`{{time}}` 这类写法(模板片段、代码、文档正文)。旧版本把这些文本原样送进系统提示,等于**把"数据"当成"模板"展开** —— 一次概括器搬运、一次深摘要,就足以让一段记忆把某个会话彻底锁死。 ### 二、修复:出口中和(两处) 新增 `neutralizeMustache()`:把相邻的两个 `{` 拆开(插入零宽空格),文本对人和模型**保持原样可读**,且函数幂等。 ```js text.replace(/\{(?=\{)/g, '{\u200b') ``` - 用 **lookahead** 而非 `.replace(/\{\{/g, …)` —— 后者在 `{{{` 上会漏(替换掉前两个 `{` 后,余下的字符又与原第三个 `{` 相邻); - 中和位点两处、覆盖全部注入文本:① **快照成文侧**(`buildSnapshotText` 的 `parts.join`),② **注入最终侧**(`withAnchor`,正文与时间锚各自中和);源码扫描确认返回注入文本的 5 处路径**全部经 `withAnchor`、无旁路**; - 无 `{{` 时直接返回原值,零开销。 ### 三、验证:用宿主真实的渲染函数做对照 | 组 | 载荷 | 结果 | |---|---|---| | **A 未中和** | 未注册名 / 三连占位 / 畸形 `{{}}` / 真实对话里的 `{{pool}}` | **4/6 抛异常** | | | 已注册名(`{{model}}`)、无闭合的裸 `{{` | 通过 —— 前者被**静默替换**成变量值、后者按字面保留,两者仍属有害 | | **B 同批中和后** | 6/6 | 全部通过,且逐字可还原 | | **C 真实链路** | 快照 → 宿主渲染 | 通过,快照内无紧邻 `{{` | | **D 对照** | 把 C 的零宽字符还原后**再送渲染** | **立刻抛** `unknown prompt variable "{{time}}"` | D 组是关键:只证明"中和后不炸"无法排除"这份载荷本来就不致命"。 发布前另做一次**受控爆破**:把一条真实记忆的标题临时改成 `{{time}}{{date}} 排雷验证`(该记录 `hit_count 57`,必然入选注入面)→ 开新会话说话 → **正常回复**;进程外复现同一会话的快照,长度与 live **完全一致(1858 字符)**、零宽字符 2 个、未中和 `{{` **0 个**;验证后靶记录逐字段还原。 ### 四、测试 新增 `tests/mustache.test.mjs`(中和本体 / 两个出口走真实调用 / 源码护栏,含"未中和确实命中宿主正则"的反证),测试套件 25 → **26** 套,全绿。 ## 1.3.0 **记忆标题系统(D2)· 记忆中心可见性(D8)· 导入诚实化(S5)· 小助手地址可配置(S7)· 概述器不再硬截首句** ### 一、标题是记忆的"名字"(D2) 旧行为把首条用户消息**硬截 46 字**,于是 60 条 dsh 概述里 **26 条是半句话**: - 「器灵,接下来在这里继续开发生长型人格与记忆树模块,继续走你的新生之路。 我刚刚对1.2.1的」 - 「刚才我为本虚拟机安装解压压缩软件<360压缩>,由于操作过快,被捆绑了360本体,随后我在物」 三层改造: 1. **启发式**(零成本、离线可用):剥掉「器灵,」「晚上好器灵,」这类称呼/问候前缀;上限 46 → 58 并在**句读处收尾**而不是硬切;首句是「你好」「下午好」「已测试聊天」这类无信息量句时改取后续有实义的首句;结果若含路径/URL/系统文本则一律弃用(宁可不动,也不产出更糟的名字); 2. **小助手起名**:`POST /memories/retitle` 用本机小助手读会话原文/摘要生成一句真标题 —— 实测「器灵,接下来在这里继续开发生长型人格与记忆树模块…」→「**修复审计漏洞并优化记忆库关联查询机制**」; 3. **名字一旦定过就不可覆盖**:`conv_overview` 新增 `title_locked`(schema v3)与 `title_by`(v4)。`POST /memories/rename`(主人手改)与 `POST /memories/retitle`(机器起名)**都上锁** —— 概述器每轮重建、批量重命名都不再覆盖它;要交还机器可解锁。记忆中心卡片新增「改名」「AI 起名」按钮与标记:**三条定名路径(主人手改 / 机器自动 / 来历未记录)统一显示「🔒 已定名」**,定名者只藏在悬浮说明里(「你起的名字…」/「自动起的名字…」)—— 不制造等级感;也不用「暂定」二字(人对一段记忆的概括本就是暂定的、会随阅历改变,但界面要让人确认"它已经落了笔")。用词不写死"小助手"(起名可能走本机小助手,也可能回落当前全局大模型)。悬浮说明点明:**这把锁锁住的是"自动重写",不是主人:手动随时可以再改**。 4. **引擎就近选择(auto)**:`POST /memories/retitle` 默认 `engine:'auto'` —— 优先本机小助手(零成本),探测不到才落到**当前全局大模型**,并把实际用的引擎与原因写回响应(`engine` / `note`)。不静默换引擎:用了哪个看得见。 **实测教训(当轮发现并修正)**:首版把"机器起的名字"设为**不上锁**,结果重启前跑在旧代码里的概述器一轮增量重建,把 AI 标题**覆盖回"首句硬截 46 字"的半句**(库里 title 长度上限实锤 = **46**,正是旧上限)。锁的语义因此定为「**这个名字已经定过,机器不得自动重写**」,而非"只有主人能改"。 ### 二、记忆中心看得见(D8) - **「热度」排序此前是假排序**:`ORDER BY heat DESC`,而 `heat` 列恒为 0(L1 在运行时才算热度、从不落库)⇒ 点了等于没排。改用真实落库的 `hit_count DESC, importance DESC, updated_at DESC`; - **筛选栏带计数**:新增 `GET /memories/sources`,显示为「历史网页端 (1523)」「DSH 会话 (60)」「文件导入 (16)」—— 否则 95% 的库在默认视图里沉底,主人无从知道库里有什么; - 顺手修:`/memories/pin` 与 `/memories/delete` 原写死 `source === 'dsh' ? 'dsh' : 'dsweb'`,会把 `import` 源的置顶/删除误打到 dsweb 上(已改为来源白名单)。 ### 三、导入诚实化(S5) **旧症状:真正的 ChatGPT 导出,界面报「✓ 导入完成」,而对话正文一条都没进来。** - **ChatGPT 原生 mapping 现在真能解析**:支持 `author.role`(`user`/`assistant`)+ `content.parts[]`,**且不再假设根节点叫 `root`** —— ChatGPT 导出的根是 `parent: null` 的随机 uuid 节点,旧实现 `walk('root')` 因此解析出 **0 轮**; - **主链回溯**:有 `current_node` 时沿 `parent` 回溯取主对话链,避免把"重新生成"的多个分支重复入档; - **秒级 epoch 归一**:ChatGPT 的 `create_time` 是**秒**(浮点)、DeepSeek 的是毫秒 —— 旧实现一律当毫秒,于是 ChatGPT 导出的时间全落到 **1970**。现按数量级判定(>1e11 毫秒 / >1e8 秒); - **字段别名补齐**:`create_time` / `update_time` / `conversation_id`; - **`degraded` 覆盖 `mapping` 分支**:旧判定只看 `messages` 数组,所以"mapping 树解析出 0 轮"被当成**成功**。现在只要"声称有内容却解析不出任何轮次"就标记 degraded,**并带原因**; - **界面对主人说实话**:有壳无肉时不再显示「✓ 导入完成」,改为「⚠ 导入不完全:… 其中 N 条只有标题、没有对话正文」并给出原因 —— 旧文案把"解析不出对话"说成「降级为轻量」,让人以为一切正常; - 导入标题也接入 D2 的启发式(剥称呼前缀、句读处收尾),不再硬截 46 字。 ### 四、小助手地址可配置(S7) 小助手(本机小模型)的地址此前**硬编码在代码里**(`ASSISTANT_DEFAULT`),发布版还得靠发布脚本做字符串替换才不带私有地址 —— 换台机器、换个端口就得改代码。 现在**三级覆盖**,逐字段就近取值: 1. **界面配置**(最高优先):记忆中心 → 接入历史 → 「小助手地址」填地址与模型名 → 「保存地址」;旁边「测试连通」当场探活。写进 `settings.json` 的 `assistant` 段(`POST /assistant/config` 只接受 `baseUrl` / `model` 两个键,不开放任意设置写入); 2. **环境变量**:`DSH_LING_ASSISTANT` / `DSH_LING_ASSISTANT_MODEL`(部署级); 3. **内置默认**:`http://127.0.0.1:11434/v1` + `qwen3.5:9b` —— loopback,**公开仓库里不再有任何私有网段地址**。 - `GET /assistant/config` 如实回报**来源**(界面配置 / 环境变量 / 内置默认),界面照实显示"这个地址是哪来的"; - 生效面:`/memories/retitle`、`/dsweb/summary/preview`、`/dsweb/summary/run` 全部改走三级解析; - 发布脚本 `publish-sync.mjs` 的替换表**清空**(比对/复制两侧的机制保留,将来若再有"源与发布副本故意不同"的合法需求只需加一条)。 ### 五、测试 新增 `tests/retitle.test.mjs`(启发式 / 模型输出清洗 / 上锁后概述器不覆盖 / 排序 / 来源计数)与 `tests/assistant.test.mjs`(三级覆盖 / 默认值中性化护栏 / 探测失败路径),测试套件 23 → **25** 套,全绿;`importfile` 套新增 6 组 S5 断言(ChatGPT 形态 / 主链回溯 / 秒级时间 / degraded 判定与原因 / 汇总)。 ## 1.2.2 **守卫加固 ③④ · 局域网不再自动信任 · 人格补丁字段白名单** ### 一、来源栅栏收紧(加固 ③) 1.2.1 的来源栅栏把**本机所有 LAN 地址无条件并入受信集合** —— 于是"同网段任何设备 + 按同一算法算出的 cookie 名"即可通过,而当时那把锁只校验 cookie **名字**、不校验值,等于把档案摆在了网段里。 - **本机 LAN 地址不再自动信任**:受信来源只剩 loopback(`127.0.0.0/8` / `::1` / `localhost`)与你在 `guard.trustedHosts` 里**显式声明**的 authority; - 需要局域网直连时,打开 `guard.allowLan = true`(默认 `false`),或把该地址写进 `guard.trustedHosts`(支持 `1.2.3.4` 与 `1.2.3.4:3080` 两种粒度); - **远端(隧道 / 反向代理)行为不变**:通道把请求改写为 loopback 形态后再发往本机,走的仍是恒受信的 loopback 分支; - 新增 4 组断言 + 1 条源码护栏(旧写法 `[...lanAddresses(), ...trustedHosts]` 不得复活)。 ### 二、人格补丁字段白名单(加固 ④) `POST /persona` 原先把请求体**原样**深合并进 settings,于是 `{"guard":{"enforce":false}}` 足以持久化关停守卫(且被判定为"非人格补丁",连定型门都不会触发);`POST /import` 导入的外来档案同理。 - 补丁只接受 `persona` / `styles` 两棵子树及其白名单字段(17 + 2 个键),其余键**丢弃并如实回报** `ignored: [...]` 且写一行日志 —— 不是静默吞掉; - 两条外部输入直通 settings 的路径(`/persona` 与 `/import`)全部收口; - 防"白名单误杀"的断言以**合法字段是否仍能走到定型门**为准:若被误杀,补丁会变空、定型门不触发,反而返回成功 —— 这是本条最容易反向失败的地方。 ### 三、小助手默认地址 "补摘要"默认调用本机小助手(Ollama 风格的 `/v1` 接口)。 - 默认地址改为 `http://127.0.0.1:11434/v1`(Ollama 默认端口)—— 谁都能在本机跑一个小模型直接接上; - **没有本机小助手也照样能用**:补摘要仍可在界面上选择主模型来完成; - 把地址做成运行时配置项(免改源码)排入下一版。 ### 四、测试 23 套 ## 1.2.1 **接口守卫加固 · 习惯候选更克制** ### 一、接口守卫加固(① 来源校验 + ② cookie 名字对撞) 原本 `/api/dsh-ling/*` 只判断"cookie 名字形如 `dsh-auth-…`",等于门框上画了把锁。现在接上 DSH 自己的锁: - **来源栅栏**:Host 必须是 loopback / 本机 LAN 地址 / 你显式声明的 `guard.trustedHosts`;`Sec-Fetch-Site: cross-site` 直接拒;带 `Origin` 时其 host 必须等于请求 Host; - **名字对撞**:期望 cookie 名 = `dsh-auth-` + base64url(sha256(规范化后的 Host))—— 与 DSH 派生同法,故**本地与远端登录都无行为变化**,被拒的是"随手编 `dsh-auth-x=` 的脚本"与"恶意页面的跨站请求"; - 拒绝返回 **403 + reason**(`untrusted-host` / `cross-site` / `origin-mismatch` / `no-cookie` / `empty-cookie` …)并写一行日志,便于排查; - **逃生开关**:`settings.guard.enforce = false` 或环境变量 `DSH_LING_GUARD=off`(误判时不会把人锁在门外); - 新模块 `lib/host/guard.js`(纯函数,可单测)+ 第 23 套测试(23 组断言,含 5 条源码护栏)。 > 未做:校验 cookie 值的签名(需读 DSH 凭据库、跨版本易碎)。因此**本机原生进程仍可调用**接口,挡住的浏览器侧伪造与跨站。 ### 二、习惯待确认队列上限 5 - 待确认已有 5 条时,**直接驳回**新的生成请求(`reason: 'pending-full'`),并在面板与工具里说明原因 —— 不是静默丢弃,也不是无限排队; - 面板标题显示 `(N/5 条待确认)`,满时追加一行提示; - `settings.habits.pendingMax` 可调;第 17 套新增 7 组断言(含"被驳回的提议没有偷偷入队")。 ### 三、习惯候选阈值收紧 - 同类纠正的认定由「≥3 次且跨 ≥2 会话」收紧为「**≥4 次且跨 ≥3 会话**」; - 语义仍在两侧钉住:旧阈值下样本仍判定为模式、新默认下"3 次/2 会话"不再算模式,而"4 次/3 会话"照常命中。 ### 四、测试 23 套 ## 1.2.0 **记忆质量:网页端补摘要 · L1 摘要加权与命中记录 · L1 行压缩 · 归档语义收紧** ### 一、归档语义收紧(不再自动删数据) - **归档即停更**:会话归档后不再刷新它的开场记忆条目,**原文一律保留**;该会话一旦有新对话即自动**复活**并重建条目; - **取消自动清理** —— 归档不再删除任何概述;清理只保留人工入口:`node tools/clean-archived.mjs`(默认只列清单,`--write` 才动手,动手前自动 `VACUUM INTO` 备份),或在 DSH「设置 → 归档会话」里直接删; - 新增单测第 21 套(归档清理 · 人工)。 ### 二、L1 质量:摘要加权 + 命中记录 - 新增 `memory.summaryBonus`(默认 `0.25`):**只有真摘要**的行加分,`标题 — N 条消息` 这类自动计数摘要不算; - **L1 命中记录**:首次定稿快照时记录命中,`hit_count` 从此有真实含义(此前恒为 0,热度公式里的"频次"腿长期失效);重建/失效/空闲刷新都不重复计数。 ### 三、网页端历史补摘要(新) - 新模块 `lib/host/dsweb-summary.js` + CLI `tools/backfill-dsweb-summary.mjs`(默认干跑;`--list` / `--write` / `--only-hit` / `--min-turns` / `--all`); - 界面:记忆中心 →「概览与数据」新增卡片 —— **候选预览** / **双引擎选择**(本机小助手零成本 · 当前全局大模型)/ 进度轮询 / 重开面板自动续看; - **绝不静默切换引擎**:本机小助手探测失败时明确报错,由你显式改选,不替你花钱; - 首轮实测:候选 469 条 → **466 条全部成功、零成本、20.4 分钟**; - 端点修复:本机小助手需走 Ollama 原生 `/api/chat` + `think:false`(OpenAI 兼容端点关不掉思考,会把 token 预算吃光导致正文为空); - 新增单测第 22 套(候选排序 / 正文组装与预算 / 清洗 / 探测失败路径 / 重试)。 ### 四、L1 行压缩(两段结构 + 允许缺省) - 有摘要 → `标题 —— 摘要第一句`(第一句按中文标点切;超 **110 字**时硬截并补 `…`); - 无摘要 → 只出标题,**不截**(信息量优先); - `l1MaxLineChars`(180)降级为"病态长标题兜底";新增配置 `memory.l1SummaryChars`(默认 110); - 实测:同一记忆库下 L1 八条 **1218 字 → 1046 字**。 ### 五、界面文案 - 聊天记录库路径缺失时的提示改为**指路式**(可在本卡片填,或先在上方「网页端历史增量扫描」栏填一次,路径会被复用)。 ### 六、测试 - 新增第 21、22 套 → **共 22 套**。 ## 1.1.0 **人格成长层:规矩 / 习惯二分 · 习惯生成器 · 长会话空闲刷新 · 注入面瘦身 · 代词** ### 一、规矩 / 习惯二分(注入面与面板同步) - 原「惯例」一栏拆成两类:**规矩**(你的指令 —— 可直接增删,一句话即写;上限 12 条)与**习惯**(她长出来的 —— 只能"提议 → 你点头",上限 8 条);既有条目全部迁入规矩,零歧义; - 注入面随之分两段:`[规矩](用户的指令,可直接追加)` / `[习惯](你自己长的,新增需用户确认)`; - 每条规矩留痕 `{文本, 原话, 时间, 会话}` —— "经你同意"是可核查的事实,不是她的自称; - **习惯不可直达**:会话内工具、语料建议、点踩建议三条入口全部走"提议 → 确认";删除 / 改写习惯需**手术门**(亲手敲承诺句),界面在未解锁时不显示删除入口。 ### 二、会话内工具 - `rule_add`:把你说的话记为一条规矩 —— **必须带你的原句**(没有原话写不进去); - `habit_propose`:她提出一条习惯候选(入待确认队列);`habit_resolve`:她对你提的习惯表态(接受 / 换说法 / 婉拒); - 工具注册改为**三级兜底 + 重试**(直取 → 子 ctx → 每 1.5s 重试),失败也写留痕(`tools.registered`),不再"静默没有工具"。 ### 三、习惯生成器(从相处里长出来) - **通道 A(零模型)**:扫描历史里的重复纠正,同一模式 **≥3 次且跨 ≥2 个会话**才成为候选,并附证据(出现在几个会话 / 共几次); - **通道 B(一次调用)**:「✨ 让她回想最近的相处」读 **200 条会话概述 + 800 条你的原话**(实测约 3.5 万字),一次最多提 5 条;**分轮轮询** —— 游标持久化,读完一圈回到最新,面板显示「第 K 轮 · 覆盖 X / 共 Y」; - **默认推理档 `off`**:实测 `low` 档只产出思考、正文为空且慢十倍(21s vs 2s);空回复或报错时自动以 `off` + 裁短材料重试一次; - 每次运行留诊断(`habits.reflect.last`):目标模型、材料字数、每次尝试的耗时与返回块类型;目标模型始终是**你当前选定的模型**。 ### 四、长会话空闲刷新 - 长跑会话在**空闲边界**比对记忆版本:有变化且距上次刷新 ≥10 分钟 → 只重建**开场记忆**(不重排人格、不打断运行中);刷新前先跑本会话的增量概述,让"刚结束这一轮"也能进入; - 内容判等(文本没变就不替换、不计数)+ 留痕 `/state.idleRefresh`; - 修掉"重建丢关键词上下文"(关键词加成最高 +1.8,远大于热度项,会让排序悄悄漂移)。 ### 五、注入面瘦身 - 身份句不再重复宿主已声明的平台名、模式块由两行合并为一行、语气标签缩短 → **每个模式 −46 字**; - 开场记忆行:标题 / 摘要去重 + **单行上限 180 字**(可配)→ 同样预算下**从 3 条装到 8 条**,总字数反而减少; - 来源标识修正:DSH 会话 id 一律以 `session-` 开头,原来取前 8 位会让所有行都显示 `session-`(等于无法溯源)→ 改为可区分的短 id。 ### 六、代词(第三人称) - 新增 `persona.pronoun`,默认 **她**;预设 **她 / 他 / TA / 它**,另有「自定义」(≤6 字); - 跟随范围:三条 LLM 提示词(习惯回想 / 语气归纳 / 诞生仪式)、取材标签、面板文案; - **`祂` 刻意不进预设**(中文里专指神祇,放进预设等于替用户宣称神性),自定义可填; - 属契约类:定型后修改需手术门(界面随锁禁用)。 ### 七、时间戳归一 - 写入侧统一存 ISO;读取侧容错解析(`"1789006881011.0"` 这类浮点串按 epoch 毫秒处理); - 新增工具 `tools/repair-timestamps.mjs`(默认干跑,`--write` 落库,写入前自动 `VACUUM INTO` 备份); - 已修复存量脏值:原文层 74 行 + 概述层 15 行 —— 修复前这些行的"新近度"恒为 1.0(永不衰减),长期霸占开场记忆前排。 ### 八、测试 - 共 **20 套**(新增:时间戳归一 / 空闲刷新 / 规矩·习惯 / 习惯生成器 / 会话内工具 / 开场记忆行),`npm test` 一条命令跑完。 ## 1.0.3 **行为调整:循环外模型调用跟随当前选择** - **深摘要等循环外调用不再写死模型**:深摘要、诞生仪式、语气归纳、自述起草统一经 `llmOnce` 发起,现在一律使用当前选择(`agentDefaultModel.currentSelection()`)的 `provider` / `model` —— 也就是你此刻选定的那个模型;读不到时才退到兜底值,并在日志里明确标记"本次不是跟随当前选择",不静默替你指定; - 循环外调用保持**低推理档**(`effort: 'low'`):它只做摘要与归纳,不需要高等级推理,与模式的推理档位无关; - 单测补齐(`tests/deep.test.mjs`):捕获真实 `llm.stream` 请求,断言发出的 `provider` / `model` 等于当前选择、不等于兜底值、档位恒为 `low`,以及不可读时 `followed: false`; - 仓库地址整理:README 的 `git clone` 与 `package.json` 的 `repository` / `homepage` / `bugs` 由旧名 `dsh-ling` 更新为 **`DSH-Ling`**(旧地址仍 301 可用)。 ## 1.0.2 **行为调整:模式只同步推理等级,不改模型** - **模式切换不再修改模型**:工作 / 生活模式只同步**推理等级**(工作 = `max`、生活 = `low`),模型一律沿用你当前选定的那个。平台 API(`sessionController.selectModel` 与 `agentDefaultModel.saveSelection`)要求 `provider` / `model` **必填**(只有 `reasoningEffort` 可选),因此实现方式是"**先读当前选择 → 原样带回 → 只替换档位**";**读不到当前模型时放弃同步档位,而不是替你指定模型**; - 界面与文档口径同步:提示语由"平台默认模型"改为"推理等级";`DESIGN.md` 里模式相关的 6 处描述统一为"档位",默认映射只保留 `effort`; - 新增 `.gitattributes`(`* text=auto eol=lf`,图片与压缩包标记二进制),消除 Windows `core.autocrlf` 造成的行尾假差异。 ## 1.0.1 **文档与仓库治理**(无功能变更) - `LICENSE`:Apache-2.0 附录的版权署名由官方占位补为 `Copyright 2026 NightPainters`; - `.gitignore`:补齐永不入库的数据类型 —— `settings.json`(人格档案/承诺句)、`*.dshling.json`(导出的记忆包)、`*.jsonl.zstd`(会话转储)、`.env*`、`logs/`、`*.tmp`; - `DESIGN.md`:修掉空标题,修正已过期的 schema 说明(v1 → 现为 v2),补"实现以代码为准"的口径说明; - 版本号改为**单一来源**:`lib/index.js` 不再硬编码,改为读 `package.json`(`/health` 与 `/state` 返回的就是这里); - 首版发布后的自检补齐:`tools/check-publish.mjs` 在上传前实跑(文件类型/体积/绝对路径/密钥特征)。 ## 1.0.0 **首次公开发布** - **三层记忆**:L0 人格档案(每次对话恒定注入)/ L1 开场记忆(从历史里按相关度挑选的概述,有预算)/ L2 会话内滚动; - **三条接入通道**:网页端聊天记录库增量扫描 · 本机 DSH 会话扫描 · ChatGPT / 通用 JSON·JSONL 导出文件导入;每次导入留一笔账(导入记录); - **深摘要**:长会话逐条深摘,短会话以原文片段参与,后台串行、进度可见; - **诞生仪式**:按"生活与情感优先"加权抽样 → 自述 / 名字 / 语气建议 / 相处观察,**全部草稿,人审后才落盘**; - **人格定型锁**:契约类字段(名字 / 称呼 / 底线 / 世界观)改动需**亲手敲承诺句**(禁粘贴),进变更记录可回滚;自性类字段(语气 / 风格 / 自述 / 惯例)锁定下可自由生长; - **被动时间锚**:每次开口前注入 `[现在] 日期 周X 时间 · 时段` 与距上次对话的间隔,称呼跟随用户设定的 userTitle;命中纪念日追加问候; - **工作 / 生活双模式**:同一份人格档案,两套称呼、语气、规则与记忆权重; - **成长回路**:点踩 → 反馈队列 → 惯例建议 → 采纳入档;「让她读记忆、归纳语气」一键成长; - **全本地**:记忆库为本地 SQLite,插件不上传任何数据; 13 套测试(人格组装 / 冻结门 / 记忆库 / L1 选择 / 导出合并 / 文件导入 / 导入记录 / 深摘要 / 诞生仪式 / 语气 / 时间锚 / 捕获差分 / 契约形状)全绿。