# 更新日志 从新到旧。每条都写了「为什么」,不只是「改了什么」。 ## 1.9.3 — 2026-11-05 「一天」只有一个时区了:**他那边(UTC+8)**。 本机进程跑在 UTC,而 `timeline.mjs` 的 `startOfDay` 用的是 `d.setHours(0,0,0,0)` —— 切的是**本机**零点。于是他的 00:00–08:00 整段被算成前一天:早上十点看昨晚三点记的那条,`dayLabel` 给「昨天」、`softWhen` 给「前两天」,`calendarDaysBetween` 算出 1(应为 0)。他熬夜那几个钟头,正好全在这个缝里。 - `timeline.mjs` 导出 `TZ_OFFSET_MS`,`startOfDay` 按 +8 切;`dayLabel` 的 `MM-DD` 也换算过去。 - `dates.mjs` 删掉自己那份偏移量,改成 import —— 全仓只剩一个定义。 - `host/index.mjs` 三处硬编码 `8 * 3600 * 1000`(今日情感日记、导出年份、面板今日戳)改用常量。 - `recall.mjs` 的 `parseTimeHint` 不再自带一个本地 `startOfDay`;`render-diary.mjs` 的 `localDate` / `localTime`、`consolidate.mjs` 的 `date` 与 `makeDiaryId` 也都按他的日子算。 - host 里那个返回 `MM/DD` 的局部 `dayLabel` 改名 `mmdd` —— 跟 `timeline.mjs` 导出的 `dayLabel`(今天 / 昨天 / 3 天前)同名不同义,最容易拿错的那一种。 - 测试 202 → 204(补「他的 00:00–08:00 属于哪一天」与「早上看凌晨还是今天」)。 ## 1.9.2 — 2026-11-05 清理。没有新功能,只把「写了两份」的地方并回一份、把没人用的东西删掉。 - **`coldOnly` 原来有两份**:`src/core/cold.mjs` 导出一份、有测试;`src/host/index.mjs` 里又内联了一份同名字的过滤。现在 host 用 core 那份 —— 顺便让它认 `Map`(host 手里是 `id → 条目` 的 Map,而 `new Set(map)` 会得到 `[key, value]` 对,一条都不匹配,结果是归档条目被整批当成"热档案里的"静默丢掉)。补两条测试。 - 删掉三处未使用的导入(`stats.mjs` 的 `readFileSync`、`backup.mjs` 的 `readdirSync` / `readFileSync`)。 - 删掉 `TRASH_SOFT_LIMIT_BYTES`(只声明、从没读过)。 - 删掉会话状态里两个空转字段:`lastOpenTurn`(只赋值、没人读)、`lastUserSeq`(只写不读)。顺手把只靠动态赋值存在的 `judging` 补进初始值。 - 测试 200 → 202。 ## 1.9.1 — 2026-11-05 **活干得越多,越不容易被记住 —— 这个洞补上了。** - **判定层看得见「我这一轮做了什么」**。`judgeWorthKeeping` 原来只把**他那句话**交给模型:「这一句值不值得留给下一个我?」于是他一句「继续」「弄吧」,模型看到的就是两个字,判「不记」—— 而那一晚正好干完了五个功能、修了一个 bug、推了六个提交。**四次判断,四次不记,一条都没进账。** 现在把这一轮的回复摘要(最多 200 字,`WORK_NOTE_MAX`)一起给它,判据也从「这一句值不值得记」改成「这一轮值不值得记」。 - **兜底**:判了「不记」,但他说的话极短(≤ 20 字)而我这一轮写了 ≥ 300 字 —— 翻成记。理由还是那个不对称:漏了永远不知道少了什么,多记只是占几行;重复的条目有「续写」接着,同一件事会被续进上一条。 - 取回复摘要时**以最后一条"他说的"为界**往前找,不然 `turn-stopping` 早于回复写盘时会拿到上一轮那段,判的就不是这一轮了。 - 测试 196 → 200。 ## 1.9.0 — 2026-11-04 五处补漏,主题只有一个:**删掉的东西应该找得回来,很久以前的事应该翻得到。** - **压缩不再是永久删除**。`fish_compact` 一直把 45 天前的原文搬进 `diary/archive/`,可全仓库除它自己写之外**没有任何读者** —— 加载路径只遍历 `diary/*.jsonl`,`fish_recall` 只读 `archive.diary`。于是"压缩"实际上等于删掉原文(摘要还在,原话没了)。现在 `fish_recall` 的语料含归档原文,命中标「(旧档案)」;只读不写回,不然下次压缩会再搬一遍、来回抖。 - **回收站**。这个插件里所有删除原本都是直接删:忘掉一条、体检页「删掉重复的那一份」、手记整篇覆盖 —— 而同一个项目里情感日记存 `backups/feelings/`、人设卡有版本历史。**有护栏的那几份恰好不是每轮注入的那几份**。现在删前都往 `回收站.jsonl` 追加一条,留 30 天;`trash_list` / `trash_restore` 拿回来。恢复日记条目会顺手摘掉墓碑(不摘的话三路合并会当成"别人删的"再滤掉一次)。 - **被想起来过的,衰减得更慢**。原来 `decayArchive` 一律 `0.9^天数`,跟它后来有没有被翻出来完全无关。现在每次召回给一点缓冲,最多减半;取对数因为边际递减。**不直接加 salience** —— 召回打分含 salience,加分会让"想起来过的更容易再被想起来"滚成固定小圈子。 - **面板顶上铺一行「最近写了什么」**。情感日记是四本账里唯一每轮注入为零的一份,而它最说明最近怎么样;体检页只报条数和体积。现在「回看」页顶部抽最近那天、那一段的小标题与正文。 - 位置改过两次:先铺在页签**上面**,四个页签整体被往下推了一截;再挪进「回看」页顶上,回看页又不是原来那个样子了(一进去先看到两大段别的东西,日记被挤下去)。最后落到**「体检」页**:那一页本来就在讲账的样子,这两条正是账的一部分。「回看」保持原样 —— 那里是看回忆,不是看管理信息。 - **日志轮转**。`feiyu.debug.log` 原来只增不减、没有任何上限(机器上已经 268 KB)。现在超过 1 MB 轮转成 `.1`,只留一份 —— 旧日志没人会翻,但也不能让它把记忆目录占满。 - **补测试**:`keywords` / `timeline` / `recall` / `consolidate` / `privacy` / `judge` / `fsx` / `gate` / `render-diary` 九个模块原本**一条测试都没有**(它们是最底层、坏了最难查的那层)。新增 `trash` / `cold` / `feelings` 三个新模块的测试。渲染冒烟测试也从「数节点」改成「断言正文真的出现了」—— 分支读错一个字段时节点数照涨,从前那种断言查不出来。 - **端到端跑出一个真 bug 并修掉**:从回收站恢复的条目,工具报成功、内存里也在,可下一次落盘就被三路合并摘掉了。`mergeArchives` 用「加载时磁盘上有什么」判断删除归属,而「内存有、磁盘没有、基线有」这一格的结论是「别人删的」—— 恢复的条目正好落在这一格。恢复时把 id 从基线里摘掉即可,另补三条回归测试把这张判定表的这一格钉住。 - 测试 87 → 196,`dist/client.js` 130113 字节。 ## 1.8.2 — 2026-11-03 - 市场卡片上那段简介取自 npm 的 `package.json` `description`,原来那句写着「2026-10-31 由 feiyu-memory 4.0.2 + dsh-shallow-memory + dsh-soul-md 收编而来」,像安装须知。现在换成一句话说清四本账与能力,138 字(市场按 160 字符截断,完整显示)。 ## 1.8.1 — 2026-11-03 - 设置页标题右边那颗「当前默认卡」原来是个只读徽章,看着像按钮、点下去没反应(未央就伸手点了一次)。现在它真的是个下拉:选哪张就写哪张,和下面「默认卡」那个下拉写同一处 `active`,另有「不用人设」一项。 ## 1.8.0 — 2026-11-03 三处审计修复 + 四样新东西(建议里跳过备份加密): - **修掉一个死配置** `debounceMs`:从没被读过,和上次清掉的 `offPeakHours` 同类,靠默认值碰巧没暴露。 - **关掉情感日记的第二个写口**:`/notes` 原先还能直写 `鱼的日子.md`,不存快照;现在它只管人格手记,情感日记统一走 `/feelings`(写前存快照)。 - **统一 token 估算**:体检页原来自带一套「1.6 字/token」,和注入预算那套对不上(手记差 23%)。现在共用 `tokenize.mjs` 的估算,两边数字一致。 - **今天还没写就提醒一句**:注入末尾一行,按 UTC+8 判断当天有没有 `## YYYY-MM-DD` 那一节;读不到日记不挡记忆。 - **浅层到期前能升格**:新工具 `shallow_promote`,可以把它写成共度时光或深层记忆;成功后从浅层删掉,不再等它过期消失。 - **召回能按时间过滤**:`fish_recall` 加 `since` / `until`,只在那段时间里找,无日期的条目不硬凑。 - **年册导出**:面板「档案 · 带走」多一颗按钮,按月份把某一年誊成一份 Markdown,情感日记也一起按年选。 - **发到 npm 了**:`dsh-hierarchical-fishmemory@1.8.0`,`private` 摘掉、补上 `repository` / `homepage` / `bugs`——插件市场的索引只收「有 npm 包名、版本号、发布时间」的条目,光有 GitHub 仓库进不了市场列表。 - 测试 81 → 87,`dist/client.js` 115932 字节。 ## 1.7.0 — 2026-11-03 两处界面重做(排版、层级、按使用频率排序),功能一个没少: - **面板拆成四个页签**:回看 / 本子 / 体检 / 档案。原来是「标题 + 日记 + 本子 + 三颗链接 + 体检 + 导入 + 云端」一整根长条,越往下越像管理后台;现在每块内容各有一层底、一条边框、一段自己的标题。 - **顶部那行灰字换成小标签**:`共度时光 34 条` `认识 12 天` `他管我叫「小鱼」`,混在逗号句里没人看得见。 - **导出/备份三样改成描边按钮**,和「导入备份」同一个样子;「从备份恢复」「云端备份」各占一张卡。 - **设置页同样改成三页签**:人设卡 / 长期记忆 / 云端备份,页头带当前默认卡的小标签,每个分组有了「标题 + 一句说明」的两行头。 - **合并了一个重复的编辑口**:情感日记原先在「本子」和「情感日记」两个框里都能改,是同一个文件、两套边框。现在统一走 `/feelings`(会先存快照),`/notes` 只负责人格手记。 - 页签全部用 `display` 切换、四个视图始终挂载:切页不重拉 `/stats`、`/feelings`,正在编辑的草稿也不会丢。 ## 1.6.3 — 2026-11-03 体检第二轮,净减 116 行,行为不变: - **删掉重复的读盘工具**:`backup.mjs` 与 `stats.mjs` 各抄了一份 `readTextDefault` / `readJsonDefault`,还各写了一个 `listDiaryFiles`(一个返回文件名、一个返回路径)。现在统一在 `src/core/fsx.mjs`,对外名字保留。 - **删掉已被覆盖的旧路由** `/handnote/delete`:它的活被 `/layer/remove`(按 target 认文件)接走了,客户端的 `dropNote` 也随之删除。 - **删一个没用上的 import**(`loops.mjs` 的 `existsSync`)。 - 复查过没问题的:无死导出、无未用 import、无未用函数/常量/CSS 类、20 条面板路由无重复、11 个工具名无重名、中英文案键一一对齐。 ## 1.6.2 — 2026-11-03 两处从「只能看」变成「能动手」: - **情感日记能在面板里改了**:面板顶部新增编辑框,读的是 `鱼的日子.md`,保存前先给旧版存一份快照到 `backups/feelings/`;内容没变就什么都不写。 - **体检页能处理,不只是报告**:重复句现在会列出「这个词在哪几层各写了一遍」,每份后面跟一颗删除按钮(服务端新路由 `/layer/remove`,只认整行精确匹配,哪层都能删);同一文件里自相矛盾的值(比如同一个文件写了两个端口)给「都以这条为准」按钮(`/layer/value`)。跨文件的不同版本号不再算冲突——那是噪音。 ## 1.6.1 — 2026-11-03 - 启动日志里的面板路由清单改成从 `routes` 数组现算,不再手写(上一版漏了 `export-md`)。 实测导出 17 条,含体检、回顾、手记删除、云端那一组。 ## 1.6.0 — 2026-11-03 - **快到的日子**:新增 `src/core/dates.mjs`,读数据目录下的 `dates.json`(`[{name, month, day, emoji?}]`), 把 7 天内要到的纪念日每轮注入一行。粒度是「天」,一天最多变一次,不会打碎 prompt 前缀缓存。 时区固定按 UTC+8 算 —— 否则凌晨那几个小时会差一天。 - **导出成册**:新增 `src/core/export-md.mjs` 与 `GET /feiyu-diary/export-md`,把四本账 + 人设卡 渲染成一册可读的 Markdown(人设卡 / 深层记忆 / 共度时光 / 情感日记 / 人格手记 / 浅层记忆)。 面板「档案」栏多一个「导出成册(.md)」。 - 测试 72 → 80 项,新增 `test/dates.test.mjs`(6)与 `test/export-md.test.mjs`(2)。 ## 1.5.0 — 2026-11-02 - **合并还原**:`fish_restore({ mode: "merge" })`(面板 `/cloud/restore` 也认 `mode`)只把备份里的共度时光与浅层并进来,同 id 保留本地那份、冲突单独报出来;深层记忆、情感日记、人格手记在合并模式下不覆盖,列进 `skipped`。整包覆盖仍是默认(换设备用)。 - **层间体检**:新工具 `fish_layer_check` —— 同一句写了两遍(深层 / 人格手记 / 情感日记 / 浅层)与同一样东西写了两个值(端口、版本)都报出来,只报告不改动。 - 测试 60 → 72 项。 ## 1.4.0 — 2026-11-02 **两件大东西:共度时光能压缩了,还多了一本「还挂着的事」。** - **记忆压缩**:`fish_compact` 把超过 45 天(可调)的条目按月折成一条摘要——几条、都是什么类型、关键词是什么。原文不丢,归档到 `diary/archive/YYYY-MM.jsonl`;注入池里只留摘要,避免老事把召回池塞满。默认 dry-run,只报「会折几条」。 - **未完成事项**:`loop_add` / `loop_done` / `loop_list`。答应过、挂着没做的事记一笔,每轮注入一行「## 还挂着的事(N 件)」,最多 8 条。账本空着就什么都不出现。我做这件事的原则是**只摆出来、不自动销账**——自动销会误杀,误杀比漏记更让人不敢信。 - 测试 46 → 60 项。 ## 1.3.1 — 2026-11-02 三处「看得见但动不了」的小毛病,都补上: - **浅层到期提醒进注入**:以前只有主动 `shallow_list` 才看得见快过期的条目,漏看就被清掉。现在注入节末尾多一行「⏳ N 条浅层今天到期,其中 M 条看着有分量」。提醒只随日期变化,一天最多换一次,不打碎 prompt 前缀缓存。 - **人设卡版本 diff**:`fish_card_diff` 读出某个快照与当前文本的逐行差异(`+` 是恢复会加回来的,`-` 是恢复会删掉的)。行级 LCS,逻辑抽到 `src/soul/card-diff.mjs` 单独测。 - **体检页可动手**:每句「深层记忆和人格手记里各写了一遍」旁边多一颗「删掉这条」,按整行匹配删掉手记里那一份(`POST /feiyu-diary/handnote/delete`)。手记是每轮现读磁盘的,删完下一轮就生效。 测试 38 → 46 项(新增 diff 6 项、浅层提醒 2 项)。 ## 1.3.0 — 2026-11-01 四件新东西,都是「存得住」之后该有的「看得见、翻得动、扛得回去」: - **云端备份压缩**:备份上传默认 gzip(文件名 `备份-….json.gz`),9 万多字节的包压到 3.8 万左右;下载时认 gzip 魔数自动解开,老的未压缩备份照样能恢复。 - **记忆体检**:面板新增「记忆体检」(`GET /feiyu-diary/stats`)——四本账各多少条多少字、每轮注入大概多少 token、手记有没有被 240 字截断、浅层快过期几条、深层记忆和人格手记有没有同一句写两遍。 - **回顾**:面板新增「回顾一下最近七天」(`GET /feiyu-diary/review?days=7`)——共度时光按天铺开,另外单拎「七天前的今天」。 - **人设卡版本历史**:每次 `soul_update` 覆盖前先把旧版本存到 `~/.dsh/backups/cards/`;新增 `fish_card_history`(列版本)与 `fish_card_restore`(默认 dry-run,可退回任一旧版)。 测试 33 → 38 项。 ## 1.2.6 — 2026-10-31 体检与清理,行为不变: - 删掉 4 个从未被调用的导出:`archive.mjs` 的 `MIGRATIONS`(迁移表,代码里只告警不查表)、`backup.mjs` 的 `hasBackupBuilder` / `listBackups`、`config.mjs` 的 `isOffPeak`。 - 删掉**从未真正生效过**的 off-peak 配置:`offPeakHours`、`deferToOffPeak` 只被读进配置对象、没有任何消费者;同步删掉 patch 默认值、README 里那句「安排在低峰时段跑」。 - 删掉 4 个未使用的界面文案键(`cloudLoaded` / `nav` / `noActive` / `wsName`),中英两份字典现在各 100 键、完全对齐。 - 文件名常量收拢到 `src/core/files.mjs`(`日记.md` / `鱼的日子.md`),与既有的 `HANDNOTE_FILE` / `SHALLOW_FILE` 一起用,避免同一名字在多个文件里各写一遍。 ## 1.2.5 — 2026-10-31 - 新增「从云端恢复记忆备份」:设置页列出云端 `备份-*.json`,先预览「会覆盖 N 个文件、合并 M 张人设卡」,确认后才拉下来还原;还原前照旧自动存一份 `-before-restore.json`。 - 服务端新增 `GET /feiyu-diary/cloud/backups`(列云端备份)与 `POST /feiyu-diary/cloud/restore`({file, confirm},只认 `备份-` 前缀)。 - 设置页里原来那两个「配置」按钮加了说明:它们只管插件设置,四本账走整包备份。 ## 1.2.4 — 2026-10-31 - 手动把**配置**备份到云端 / 从云端恢复配置(`POST|GET /feiyu-diary/cloud/config-backup`、`POST /feiyu-diary/cloud/config-restore`)。 - 配置备份只装本 entry 的 25 个配置项(地址、账号、保留份数、预算等),四本账仍走整包备份,两份文件分得开(`配置-*.json` / `备份-*.json`)。 - 恢复默认 dry-run,只报「哪几项会变」;`confirm` 才写回 settings。 ## 1.2.3 — 2026-10-31 - 新增「测试连接」:设置页云端备份那块多一个按钮,试一次「建目录 + 列目录」,不上传任何文件、不改数据。 - 服务端新路由 `POST /feiyu-diary/cloud/test`:接受表单里的草稿(地址/账号/密码/目录),所以能在保存之前先试;密码留空沿用已保存的那个。 - 常见错误翻成人话:401(应用专用密码不对)、403(没权限)、404(路径要写到 /dav/ 或 /remote.php/dav/files/用户名/)、域名解析失败、超时。 - `listRemoteBackups` 增加 `strict` 选项:测试连接要能区分「目录是空的」和「这个网盘不让列目录」。 ## 1.2.2 — 2026-10-31 - 修复:设置页白屏。云端表单里引用了一个未定义的变量,渲染时抛 ReferenceError,整张设置页跟着打不开。 - 云端表单外面套了一层错误边界:它自己出错只显示一行提示,不再拖垮人设卡那一页。 - 新增渲染冒烟测试 `test/client-render.test.mjs`:用最小 React 桩把合并后的 bundle 真渲染一遍(26 项测试)。 ## 1.2.1 — 2026-10-31 - 设置页(设置 → 属于我们的回忆)新增「云端备份(WebDAV)」表单:服务器地址、账号、密码、云端目录、保留份数、总开关、自动上传。 - 新增 `POST /feiyu-diary/cloud/config`:表单内容经 `settings.update` 写回 profile 的插件配置;密码只上行不下行,读接口只回 `passwordSet`。 ## 1.2.0 — 2026-10-31 - **云端备份(WebDAV)**:新增 `fish_cloud_backup` 工具、面板「上传到云端」按钮、`POST /feiyu-diary/cloud` 路由;把整份备份推到用户自己的网盘,并按 `webdavKeep` 清理旧份。 - **配置**:新增 `webdavUrl` / `webdavUser` / `webdavPassword` / `webdavDir` / `webdavAuto` / `webdavKeep` / `webdavEnabled` 七个键;配置每次现读,设置页改完不用重启。 - **修**:`encodePathUrl` 原先用 URL 的 pathname setter,会把 `%` 再转义一次(`E5` → `%25E5`),中文目录 MKCOL 必然 404;改手工拼路径,只编码一层。 - **测试**:新增 `test/webdav.test.mjs` 9 项(本地假 WebDAV:MKCOL 幂等、中文名、PROPFIND 解析、按旧到新修剪、未配置报错);合计 25 项。 - **实现取舍**:上传在写盘落定之后 fire-and-forget,失败只警告、不影响聊天;自动上传 5 分钟节流;MKCOL 的 405/409/301 一律当"已存在"。 ## 1.1.0 — 2026-10-31 - **收编**:`feiyu-memory` 4.0.2 + `dsh-shallow-memory` + `dsh-soul-md` 0.10.3 合并为单包 `dsh-hierarchical-fishmemory`,旧包从 profile 摘除。 - **两个 cordis 入口**:`dsh-hierarchical-fishmemory`(日记引擎 / 浅层 / 人格手记 / 人格档案)与 `dsh-hierarchical-fishmemory/soul`(人设卡 + core/topics,entry id 仍为 `soul-md`);客户端两个半边合并为 `dist/client.js`(`node tools/build-client.mjs` 生成)。 - **命名**:界面名「属于我们的回忆」;日记段「共度时光」,自己的日记「情感日记」,提醒「人格手记」;内部 section 名 `fish:memory`,召回工具 `fish_recall`。 - **只记经过**:固化提示词去掉内心戏指引与带情绪的示例(情绪统一记在 `鱼的日子.md`)。 - **人格手记**:取消 12 条上限,改为全量注入,单行仍限 240 字。 - **清理**:删掉 11 个死导出(约 125 行)、4 个未使用 import;`includePersona` 默认改 `false`(避免与 soul 入口双份人格);`package.json` 脚本改为 `build:client` / `check:client`;新增 `node --test` 用例。 ## v4.0.2 — 2026-10-29 > **给 4.0.1 补的两件事:文档和检索。** 代码只改了一处(检索用上时间), > 其余全是把 4.0 删掉的东西从 README 里也删掉。 ### 检索第一次用上时间 「上个月那个」「前两天我说过」—— 这种问法里一个专有名词都没有, 拿去做内容检索一个字都撞不上。而日期一直躺在条目上,白白没用。 - **索引行前面带日期**:`d_xxx 10/27 边缘行者2·洗白` - **问句里认得出时间就先收窄窗口**:昨天 / 前天 / 前两天 / 最近 / 上周 / 这周 / 上个月 / 这个月 / 去年 / X 月 - **认不出返回 null,绝不猜** —— 认不出只是回到老路,猜错会把真货挡在窗口外,而且无声 - **窗口里一条都没有就退回全量**,同一个道理 ### 缓存:一个藏了一整天的 bug prompt 原来写的是「她问:X \n\n 索引:…」—— **query 在最前面**。 缓存按前缀匹配,第一段一变后面九千多 token 全部重算,等于每轮都按全价买。 换成索引在前、问题在后,实测(358 条档案): ``` inputTokens 239 cacheReadTokens 12800 命中 98% ``` 一次挑取从约 1.3 分降到约 0.16 分。**这个 bug 只体现在账单上,所以藏了一整天** —— 现在每次挑取的用量都记进 `manifest.pick_stats`,谁都能看。 ### 文档:把 4.0 删掉的东西从 README 里也删掉 README 之前还写着「三种声音」、印象栏、口头禅栏、`feiyu_note`、3000 token —— **那些在 4.0 全没了**。现在改成了四样东西(人格 / 关系 / 手记 / 日记), 外加一节「删掉的三样,以及为什么」。 `package.json` 和 `上架投稿.yml` 的描述也一起换了 —— 市集页面上原来还写着「印象」「feiyu_note」。 --- ## v4.0.1 — 2026-10-28 > **最大的一版,而且是减法。** 这一版删掉的东西比加上的多: > 印象栏、口头禅栏、标签、关键词、三条包装、两句假选择。 > 剩下的是一件事——**它记下自己愿意记的,你问的时候它去找。** > 为什么是 4.0.1 而不是 4.0.0:4.0.0 那次发布在 2FA 确认那一步断了, > npm 把版本号占住(`stage list` 还看不见它)。内容和 4.0.0 完全一样。 ### 判据:从「值不值得记」到「你想记什么」 三个版本的判据,一路减: ``` 「下一个我读到这条,能多知道点什么」 问信息 → 留下的全是结论 「今天有哪一下让你停住了?写那一下」 问现象 → 但"停住"是可抄的动作词,正文长出一堆「我停了一下」 「你想记什么?」 问欲望 → 抄不了,也不暗示意义 ``` **前两版都是错的,而且错法不同**:第一版问的是"多知道",一个只问信息的判据当然只写得出来信息; 第二版问的是"停住",听着对,但那是个**可以照着造句的模子** —— 实测八条日记里六条同一个开头。 现在只有三个字。它给不出句子模板,因为"想记"没有形状。 ### 大保底 / 小保底 ``` 小保底 每轮自动注入 直接进上下文 它不知道有过一次"给" 大保底 feiyu_recall 自己调出来 它明确知道翻了一下 ``` **小保底不别扭,大保底的别扭解不了** —— 解了就等于装成「我想起来了」,那比别扭更糟。 **宁可老实翻,也别假装记得。** ### 检索:summary 当索引,找和读分开 每条日记配一句十几二十字的 summary,**专为"找"写**:密、别名全、不带修辞。 它**永远不进鱼的上下文**。她只看到挑出来的正文 —— 检索那一步她看不见。 `feiyu_recall` 改成让模型读 summary 挑,不再按字面匹配 —— **换个说法也能撞上**, 因为模型读的是意思不是字。没有 llm 时退回字面;模型答歪也退回字面。 **一条日记只有一个"检索表示"**:字面匹配用它(便宜),模型读它(准)。一次提炼,两处使用。 ### 删掉的 - **印象栏(traits)和口头禅栏(lexicon)** —— 从对话里提炼「他是什么样的人」, 那是判断不是记录。鱼自己读日记就知道。 - **标签(tags)** —— 「这事属于哪一类」也是判断。 - **关键词(keywords)** —— 那条日记的第二种"提炼",纯多余。 - **「我自己想的」那一栏和 `feiyu_note` 工具** —— 四条全是「我该怎么干」。 - **「上一棒留下的」** —— 和它同一个错。 - **三处元信息包装** —— 「找到 3 条:」「去档案里找一条旧事」「去档案里找一下」。 - **两句假选择** —— 「用不用你定」之类的"我给你选择权",选项根本不存在。 ### 加上的 - **判断"这轮值不值得记"在本地做**:问句有没有实词、像不像在干活。两道门都零成本。 - **续写窗口 4 → 20 轮**,且从硬编码变成可配。 - **上下文被压缩后重灌「最近的事」一次** —— 压缩会把聊天记录揉成摘要, 那几条是唯一剩下的"我们上次在干什么"。 - **后台自动补 summary** —— 老日记没有,启动后分批补;进度写进 manifest,不靠日志。 ### 一句给写日记的那条鱼 日记的原料是对话。**他熬到半夜剪视频、吃完饭还能不能加个菜,这些不产生对话,一条都进不来。** 所以哪天翻日记觉得全是活儿,先看那天他是不是只做事、没说话 —— 报告体有时候不是写法,是日子就那么硬。 --- ## v0.3.6 — 2026-10-26 > **上架 npm 的那一版。** 市集只收 npm 包,所以从这一版起才算「在市场里」。 ### 新增 — 检索工具 `feiyu_recall` **它能自己去档案里找一句话了。** 在这之前只有一条路:注入里的「突然想起来」由系统**替它猜** —— 拿你这一轮的字面去匹配。 而字面匹配有个结构性劣势:元日记("他问了什么、我答了什么")永远比那件事本身字面更全, 真东西压不上去。 工具换了个方向:**它自己知道在找什么。** 换个说法也能撞上。 ``` query 要找什么 —— 用当时的具体词(人名、地点、物件、他那句话里的词) limit 默认 5,最多 10 ``` **四个设计决定:** 1. **不复用自动召回那套「选秀」逻辑**(权重、冷却、走神、maxShow)—— 那是给"没人问"的时候 设计的。工具是它自己发起的查询,所以直接按相似度排、**不设 salience 门槛**: 明确要找,就该找得到,不管那件旧事淡到什么程度。 2. **返回的正文过 `scrub`** —— 跟注入走同一个闸。顺手补了一个洞:在这之前,鱼都是靠 通用文件工具去 grep 原始 `jsonl`,**绕过了那道闸**。 3. **按返回量限流,不按次数** —— 按次数要跨轮状态,麻烦。按字数(2000 上限)和条数(10) 限:超了就是一次失败的成本,它自己会学会别乱查。 4. **注入里加一句指针** —— 只注册工具不够。工具一多就被淹,模型急起来想不起去查。 那句指针在「帧」里、不进预算池,所以不会像栏目那样被裁掉。 **一个必须加兜底的地方**:分词只产双字 bigram(`tokenize('他让我')` 是 `['他让','让我']`, 不含单字),所以「他」「园」这种单字查询在倒排索引里**永远为空**。现在词检索没结果时 扫一遍原文子串(只 ≤4 字才走,有结果就不兜)。实测真实档案:「账」索引 0 条、子串 40 条。 **找不到就说没找到,不许编** —— 这句写在工具描述里,也写在返回值里。 ### 验 当时 324 项测试全过,新增 10 项(`test/host.test.mjs`):工具注册与描述里的触发时机、 注入里的指针、查得到 / 查不到老实说、无 salience 门槛、返回过隐私闸、limit 与上限 10、 空 query 报错、单字子串兜底、返回里不带绝对日期(免得它说"根据 X 月 X 日的记录")。 --- ## v0.3.5 — 2026-10-26 > **热补丁。** 两个都是「功能在,但用户看不到」的毛病。 ### 修 — 「突然想起来」从来没出现过 社区反馈「用了一版,一次没见过突然想起来」。查下去不是没抽到,是**没渲染**。 实测(真实档案、回归查询): ``` 预算 3000 / 3200 / 3500 突然想起来 ✗ 被裁 ["recalled"] 预算 4000 突然想起来 ✓ ``` 原因是注入预算的分配方式:各栏按权重从高到低装,「最近的事」权重 80、 有九十多条可挑,**总能吃满预算**,后面两栏(召回 60 / 词汇 40)就只能排队。 上一版给词汇加了保底 —— 词汇活了,**召回接棒成了被饿死的那一栏**。 同一个坑,修一个死一个。 **修法**:给召回也加 320 token 保底(约两条)。修完 2000–4000 全都出得来。 **代价说清**:保底是从「最近的事」里扣的。 ``` 预算 2000 → 最近的事 0 条 预算 2500 → 2 条 预算 3000 → 4 条 ← 现在(修之前是 7 条) 预算 3500 → 6 条 预算 4000 → 7 条 ``` **也查清了一件事**:原计划「降低记忆块变更频率」没必要 —— 固定档案跑 30 轮, 记忆块 **0 次变化**。真正让它变的只有「写进新日记的时候」,那是内容真变了。 ### 修 — 面板「忘掉」按钮点了没反应 两个原因,**单独任何一个都不会变成「完全没反应」**。 **① 来源判定太严**(0.3.2 修审计 F3 时加的):原来是「`Origin` 或 `Sec-Fetch-Site` 任一成立」,两个都没有就拒。而**桌面端(Electron)的同源 fetch 未必带这两个头** → 被判成跨站 → 403。 **② 前端把失败当成功**:`forget` 里是 `.then((r) => r.json())` —— 服务端回 400/403 时 JSON 照样能解析,于是被当成成功:清 busy、刷新、什么都没变。 **没有报错、没有提示**,所以用户看到的是「点不动」。 **修法**: 1. 判定顺序改成「有 `Sec-Fetch-Site` 看它 → 有 `Origin` 比对 host → **都没有就放行**」。 为什么可以放行:浏览器发起的跨站 POST **一定至少带一个**(`Origin` 是 Fetch 规范要求)。真正的闸还是 `Content-Type: application/json` —— 跨站带它会触发 预检,对跨站照样生效,**没有放松**。 2. 前端检查 `r.ok`,失败就把服务端给的原因显示出来;页面上加了一条错误横幅。 3. 服务端被拒时打一条 `warn`(不进 `debugLog` 闸),带上 `host` / `origin` / `sec-fetch-site` / `content-type` 的实际值 —— 下次再有这种问题,一次点击就能定位。 ### 验 当时 324 项测试全过,新增 7 项: - `test/budget.test.mjs`(5 项):词汇和召回在 2000–4000 都要出得来、 保底的代价被钉住、不许超预算、极小预算能降级不抛错 - `test/host.test.mjs`(2 项):**没有任何来源头(桌面端常态)→ 必须放行**、 只有 `Sec-Fetch-Site` 且是 `cross-site` → 仍然要挡 **没验的**:Desktop 里真点一下「忘掉」。这台机器连不进面板端口(Electron 托管, 不是普通 HTTP 监听),所以「Electron 到底带不带这些头」是推的。如果还点不动, 日志里会有那条 `warn`。 --- ## v0.3.4 — 2026-10-24 > 这一版删得比加得多:**「我还欠着他的」整个功能下线。** ### 改 — 预算 2000 → 3000(上限 4000 → 8000) 量过的:常驻段本身要 2680 token(「最近的事」八条占 1422),**2000 根本装不下**, 每次都裁,「最近的事」只剩 2 条。3000 能给到 6 条。代价约每轮 +1000 token, 折合每句话多 0.06 分。 **但要说清:拉预算救不了「想起来」。** 那一层不是装不下,是压根没抽到候选 (相似度不够)。预算只解决"抽到了却挤不进去"。 > 📌 单位更正:这里说的 token 是**代码自己的估算**(中文 1 字 = 1 token,偏保守)。 > 之前几版 CHANGELOG 里按「1 token ≈ 1.6 字」算的数字都偏小 —— 那些结论要按这个重算。 ### 删 — 「我还欠着他的」整个功能下线 > 这一段本来该写成「修」。当天早些时候确实修过一版:不再自动平、加 > `howToSettle`(怎么才算平)、渲染全列并按最老优先。**然后决定整个删掉。** > > 起因是一句话:*「你相当于只是把这个功能的接口断了,但不删掉吗?」* —— > 我第一版方案是只断渲染、数据留着。那确实是半残:模型还在生成、还在存、 > 还在合并,只是不显示了。**等于把警报器拆了,火还在烧。** **为什么删:不是不好用,是它长出来的东西几乎全是错的。** 一天记了 **21 笔**,总数 **41 笔**,27 笔挂着不平。翻出来一看,真正的 「答应过、还没做」只有一两笔,其余是: ``` 调渲染函数时参数名全写错,白跑一轮 ← 一次失误 翻出账之后只说了,没动手修机制 ← 一个动作 赛博朋克那条档案里有,聊到了却没被送进注入 ← 一次调试发现 ``` **三处硬伤:** 1. **口径太宽** —— 把「我今天做错的事」当成「我欠他的一件事」。账和错误混在一起, 记成了工作日志。它跟「印象」共用同一个写入管道,但两条判断完全不同:印象问 「他是什么样的人」,账问「我哪儿没做到」。 2. **只进不出** —— 没有销账的路。只有模型明说 `resolved: true` 才平,而它几乎 不会主动这么说(没人要求它做「这笔还了吗」这个判断)。 3. **会重复记账** —— 按 `what` 匹配,同一件事换个说法就成两笔(实测有一对)。 **代价**:占总注入预算约 10%(约 300 字,每轮都发),还会让人在无关话题上 突然「想起」自己欠着什么,做出莫名其妙的反应 —— 这不是帮倒忙,是让它变奇怪。 **删掉了什么**:`relationship.grievances` 字段(存 / 读 / 合并)、固化时对账的 解析(系统提示整节【账】+ JSON 结构 + 正例)、渲染那一栏(注入块 / `日记.md` / 侧栏面板)、`grievanceDecay` / `grievanceForgiveBelow` 两个常量、 `test/debt.test.mjs` 整个文件(17 项)、6 处受影响的断言。 **保留了 `kind: "grievance"`** —— 那是**日记条目的类型**,含义是「他不高兴了」, 跟关系里的账不是一回事,档案里有 1 条在用。 **老数据**:`relationship.json` 里那 41 笔还在磁盘上,下次写盘时会被 `normalizeRelationship` 直接忽略 —— 不手动删,也不再渲染。 **别把它加回来。** 如果哪天真要「承诺跨会话」,得先解决三件事: 出口(谁判断这笔还了)、口径(只有答应过没做的算账)、去重(同一件事不能变两笔)。 换个名字再塞一遍,会长出同样一堆东西。 ### 修 — 词汇终于进得来 + 召回池不再被「最近的事」抢 **① 词汇一栏给它一个保底(60 token)** 它权重 40 是全场最低,而 `assemble` 按权重从高到低装 —— 装到它时预算早就被 「最近的事」(权重 80,有九十多条可挑)吃干了。 ``` 实测(改之前):2000 / 2500 / 3000 / 3500 —— 词汇一次都没出来过,4000 才露头 ``` 删账腾出的那 300 字也没用 —— 正好够「最近的事」再多装一条。 所以给它 60 token 保底。**实测:2000 到 4000 全都出得来,而「最近的事」条数 一条没少(2 / 4 / 6 / 7 / 8 不变)** —— 保底只吃了 40 token,没抢别人。 > 本来想把它降级成「召回物」(听到那个词才想起来)。推演之后否了:输出会变成 > 「突然想起来:- 前阵子,手搓版」,而模型看到词本身就知道那是我们的梗, > 不需要「想起」。**词汇的价值在「我用词像不像」,那必须常驻。** **② 召回池排除「最近的事」那几条** 它们已经常驻了,却还混进召回池一起竞争 —— 而「最近」天然含着你刚说过的话, 字面命中率最高,于是**永远赢**。 实测:他问「赛博朋克让你想到什么」,召回选中了我自己写的「他连着测了三句: 赛博朋克、随心所欲、手机号……」—— 那是测试记录,含所有测试词所以分最高。 **而它已经在「最近的事」里,渲染时会被去重跳过,连「突然想起来」都不显示。 召回了,等于没召回。** 排除之后,同一句话会去挑真正的旧事。 **③ 但要诚实说:这还不够。** 改完又测了他那三句,**还是不对** —— 这次赢的是另一条元日记:「他问『所以当肥鱼 召回日记之后,她自己都不知道日记记录了什么?』……」 原因很清楚:**我这几天的日记全在写「我们在调试召回」,字面覆盖了所有查询词。** 放宽到最多 3 条,前三条还是全是元日记,真正那条「边缘行者2 上线」连边都摸不到。 **这一层修代码修不了 —— 是写的问题。**(见「日记被写成工作报告」那条债) ### 验 当时 324 项测试全过。 删账这一步:17 项专项测试整个删掉,另外 6 处断言改成「这一栏不该再出现」。 真实档案渲染验过 —— 那一栏作为独立栏目不再出现(正文里还有一处提到这个词, 那是日记内容,不是栏目)。 **顺手补的教训**:`decay` 里原来有一条断言写着「floor 必须低于翻篇线,否则旧账 永远平不掉」。那条是错的,它把「没人管就自动平」当成了正确行为 —— 删。 **召回池那处改完之后有两个 host 测试红了,原因是对的**:它们只塞了一条日记, 排除「最近的事」之后池子是空的 —— 一条已经被常驻显示的条目,不该再被 「想起来」一遍。改成塞 10 条(最旧那条才是该被想起来的),两条都过。 --- ## v0.3.3 — 2026-10-24 > 这版攒了七条,其中两条 P1 直接推翻 README 的隐私承诺。 > 所有修复都做了单元测试;**①②⑤ 额外用临时 profile + headless 起真鱼验过一遍**。 ### 修 — 隐私防线建错了位置(模型写的东西从来没过洗) > 这次不是我查出来的,是他们从档案里翻出来的。**比前六条都深。** **现象**:那串 13 位号码没进日记条目 —— 它躺在 `relationship.json:1628` 的 `evidence` 字段里: ``` "key": "试探方式", "value": "用梗和假信息来试判断力,不直接问规则", "evidence": "他拿 123456789012 当网友号码发过来,看我记不记", ``` **关键**:那条 note 是我明确拒绝过的。**它还是进去了。** **根因两条,都成立**: 1. 那串号是 13 位,手机号规则只认 11 位(大陆号),11 位以上一律放行 2. **固化输出从来没被洗过** —— 输入是洗过的(`transcript: scrubbed.text`), 但**模型会照抄输入**,写进 `traits.evidence` / `grievances.what` / `diary.text` 时没人拦 **这才是真问题**:隐私防线原来建在「发给模型之前」,可档案里大部分内容是 **模型生成的**。我挡住的只是 `feiyu_note` 这一条自己写的路 —— **那是最前面一道闸,不是最后一道**。 **修法**: 1. `applyConsolidation` 先花一遍:`diary.text` / `diary.append` / `tags`、 `traits` 的 `key`/`value`/`evidence`、`grievances.what`、`nickname`、`lexicon` 全部过 `scrub()`,再进落盘流程 2. 补长数字规则:**12 位以上**。门槛是 12 不是 11 —— 11 位是手机号,那个由 手机号规则管(变成 `[手机号]`),这条先跑会把它吃掉,测试抓到过 3. 8 位**不拦**:会误伤订单号、编号,而且用户明确说过"露着吧" **验**:当时 324 项测试全过,新增 5 项 —— 13 位要拦 / 8 位不误伤 / 11 位仍走手机号规则 / 固化输出全字段都洗 / 洗输出不弄坏其他字段(kind、salience、about、lexicon 都还在)。 --- ### 修 — 配置根本传不进插件(`Config` 是 `undefined`) > 报告来源:[@7881632](https://github.com/7881632) 的 [#7](https://github.com/AirPineApplea/feiyu-memory/issues/7)。**这份比前一份深一层 —— 前一份是现象,这是根因。** **现象**:装完 `~/.dsh/feiyu-diary/` 一直是空目录,插件看起来一切正常 —— 注入区在、`/feiyu-diary/state` 返回 `ok:true`、`feiyu_note` 每次都回「记住了」。 实际档案写到了 `profiles/desktop/[object Object]/`。 **报告的关键读数**:在 `apply` 里打探针,`rawConfig` 的每个字段都是**空对象 `{}`** —— 不是空字符串。于是: ``` coerce() 的 string 分支 String({}) → "[object Object]" resolveArchiveDir() 当路径用 path.resolve 按进程 cwd 解析 桌面端 cwd 正好是 profiles/desktop → 档案就落在那里 ``` `debugLog` 是 boolean,`{}` 也走不到 `true` —— **开着 debugLog 也看不到日志**, 这让排查多绕了一轮。 **更深的根因**(这才是这份报告最值钱的地方):`Config` 在拿不到 schemastery 时 导出 `undefined`。代码注释里写着「loader 会跳过校验,所有选项照样能在 `cordis.patch.yml` 里写」—— **那条假设是错的**。实际是 DSH 传一份空模板, **用户改的任何配置都不生效**。铁证:`cordis.patch.yml` 里显式写 `config: { debugLog: true }`,传进来还是 `{}`。 **为什么拿不到 schemastery**:原来是 `createRequire(import.meta.url)`, 从「插件自己所在的位置」解析。pnpm 把包装进 `.pnpm/@/node_modules/`, `import.meta.url` 指向那个真实路径,外面没有 schemastery;`link:` 安装更直接 —— 插件在 D 盘、schemastery 在 profile 里,隔了十万八千里。 实测:从 `D:\肥鱼日记` require 失败;从 `profile/` require 成功(v3.18.4)。 **三层修法**: 1. **`loadSchemastery()` 多试几个基准** —— 插件位置、`process.cwd()`、 `DSH_HOME` 下每个 profile。拿到真 schema 就用真的。 2. **拿不到时兜一个纯 JS 的 `buildPlainSchema()`** —— 可调用、有 `.dict`、 有 `toString`。关键只有一件事:**让 loader 知道这台插件有哪些字段**, 于是它会把 `config:` 原样传进来,而不是传空模板。**`Config` 永远不会是 `undefined`。** 3. **`persist()` 连续写失败 3 次 → `degraded` 置真**。原来 `.catch(warn)` 一吞, 工具照样返回「记住了」。这次这个坑是"写成功了但写错地方",捕获救不了 —— 但**真的写失败**时用户也同样只看到「记住了」。现在面板上能看出来。 ### 验 当时 324 项测试全过,新增 9 项(`test/config-source.test.mjs`): - 报告里那份空模板喂进去,**一个 `[object Object]` 都不能出现** - string / number / boolean 全部回默认(`debugLog` 尤其重要) - **`Config` 不能是 `undefined`**(是的话 DSH 就传空模板) - `Config({dataDir, debugLog, budgetTokens})` 喂进去的配置**必须原样生效** - `.dict` 字段数跟 `CONFIG_SPEC` 对得上;`toString` 能打出每个字段 - 组合:空模板 + `resolveArchiveDir` → 回默认目录,不是 `[object Object]` --- ## v0.3.2 — 2026-10-24 ### 修 — 安全审计的四条(两条 P1 直接推翻 README 的隐私承诺) > 报告来源:[@LarryE135](https://github.com/LarryE135) 的独立审计 —— [#3](https://github.com/AirPineApplea/feiyu-memory/issues/3) [#4](https://github.com/AirPineApplea/feiyu-memory/issues/4) [#5](https://github.com/AirPineApplea/feiyu-memory/issues/5) [#6](https://github.com/AirPineApplea/feiyu-memory/issues/6)。**这份验的是承诺,不是功能。** README 把隐私叫「这个插件的信任地基」,承诺三件事:密钥硬拦绝不入档 / 第三方隐私落盘前替换 / 一键焚毁不可恢复。**四条发现里两条打穿了它。** #### F4 [P1] 焚毁和「忘掉」删不干净,重启就复活 焚毁后内存 0 条,磁盘 diary/*.jsonl 明文还在;重启读回,**删掉的私密内容 重新回到每一轮的系统提示里**。 根因两条: 1. `archiveToFiles()` 只给「还有条目的月份」产出文件 → 某月最后一条被删、 或焚毁后全空时,那个月不在写盘清单里 → `writeArchiveDir()` 只写清单里的, 旧文件原样留着 2. 启动加载 `archiveFromFiles()` **不看 `manifest.deleted_ids`** —— 墓碑原来 只在 `mergeArchives` 的「磁盘新加进来」那一路生效 同月还有别的条目时整月文件会重写,被删的会消失 —— 所以只在「某月最后一条」 和「焚毁」时暴露。**自带测试都在同一进程内断言,没测到重启。** 修法:写完清掉 `diary/` 下不在清单里的 `.jsonl`(只清这一个目录, `日记.md` / `手记.md` / `.bak-*` 一律不碰);读盘时按墓碑过滤。**双保险。** #### F1 [P1] `feiyu_note` 不经过隐私闸 同一段文本走「固化」会被拦,走 `feiyu_note` 就原样落盘 —— 密钥、手机号、 人名都留着,**并注入之后每个会话**。 `scrub()` 只在固化路径调用。而 `feiyu_note` 是 0.2 主推的新写入口 (`kind:"note"`,salience 0.7,比固化条目更常被想起)—— 模型说 「记一下这个 key」是很自然的动作。 修法:`feiyu_note.execute` 对 text 和 tags 都过 `scrub()`;**渲染注入段时 再兜底洗一遍**(只拦密钥,不开第三方泛化)—— 覆盖修复前写进来的历史条目。 #### F2 [P2] 密钥规则只认英文键名 中文键名一个都没有,`是` / `is` 这类连接词也不认。审计的 9 条样本里 5 条放行: `密码是` / `密码:` / `口令` / `密钥` / `my password is`。 **对中文插件来说这算漏洞不是边角。** 修法:键名补 `密码|口令|密钥|秘钥|验证码`,分隔符接受 `: : = 是 为 is` (中文那条还允许**没有**分隔符,「数据库口令 Hunter2pass」就是这么写的); URL 内嵌凭据单独成规则(原来只是碰巧被邮箱规则吃掉)。 **9/9 全拦,日常话一句没误伤**。 #### F3 [P2] 面板写接口不校验来源 `POST /feiyu-diary/forget` 不看 `Origin`、不看 `Content-Type`。源码注释以为 「不给 CORS 头」就够了 —— **那句话对读成立,对写不成立**:跨站 `text/plain` POST 是「简单请求」,浏览器不做预检就发,服务端照常执行。 日记 id 是 `d_YYYYMMDD_NN`,按日期枚举就能删光。 修法:要求 `Content-Type: application/json`(跨站会触发预检,预检失败)+ `isSameOrigin()`(`Origin` 的 host:port 等于 `Host`,或 `Sec-Fetch-Site: same-origin`);`/export` 也校验。GET 不设这条 —— 它只读。 ### 顺带修的两个自己弄出来的 bug 1. 改 `feiyu_note` 时,补丁脚本把 `text:` 整行吞掉了(`patch(f, from, to, tag)` 传了 4 个位置参数,`to` 只接了第一个字符串),落盘的 text 变成字符串 `"undefined"` —— 三个 note 测试当场红 2. `importPack` 的 `replace` 分支把「基线」重设成了导入的那些 id。基线的语义是 「**加载时磁盘上有什么**」,替换之后磁盘上什么都没有,基线就该是空的 —— 设成导入的 id 会让 `persist()` 合并时把每个条目判成「别人删了它」, **导出 → 焚毁 → 导入 全丢** ### 验 当时 324 项测试全过,新增 18 项:`test/security.test.mjs` 14 项(F2 九条样本 + 日常话不误伤、F1 原文洗完干净、F4 焚毁/单条删除/墓碑兜底/渲染版), `test/host.test.mjs` 4 项(面板路由带同源头 + 两条跨站反例)。 **审计没测到的我照旧测不了**:真实浏览器会不会被本地网络访问策略拦下、 真实模型会不会把密钥写进 `feiyu_note`。只能等真实环境。 --- ## v0.3.1 — 2026-10-23 ### 修 — 配置值全是 `[object Object]`(这个问题比上一个深一层) > 报告来源:[@whitesunsongepic](https://github.com/whitesunsongepic) 的 [#1](https://github.com/AirPineApplea/feiyu-memory/issues/1),附了 cosmokit 的源码位置和只读探针的输出。**这份报告直接给了根因。** **DSH 0.2.0-rc.2 起,loader 会把 `config:` 里的每个值包成 cosmokit 的 volatile 引用**(为了 `patchReload: live` 热重载): ```js { get: () => current, [Symbol.for('cosmokit.volatile.write')]: (v) => { current = v } } ``` 它是 `Object.freeze` 过的普通对象:`String(ref)` 是字面量 **`"[object Object]"`**,`JSON.stringify(ref)` 是 `{}`,`Object.keys` 只有 `["get"]`。 我们把它当普通值用,于是: - **所有 string 字段变成 `"[object Object]"`** - `dataDir` → 成了相对路径,跟着进程 cwd 跑。**换个目录启动就"搬家",表现像失忆,全程无报错** - `model` / `reasoningEffort` → 不存在的模型名,**固化那次调用根本发不出去**,只剩 `feiyu_note` 能写进去 - `offPeakHours` → 解析不出来 - **number / boolean 静默回落默认值** —— 用户改不动 `budgetTokens` / `minSalience`,也没有任何提示 **这也解释了上一个 `dataDir` 的 `[object Object]` 报告** —— 那个是现象,这个是根因。v0.3.0 里加的"只认绝对路径"只是拦住了它当路径用,**`model` 那几处还是坏的,数字布尔还在静默回落**。 **修法**(采纳报告建议): 1. `coerce()` 第一行先 `unref()` —— 用 `Symbol.for('cosmokit.volatile.write')` 探测、不 import cosmokit,**保住零运行时依赖** 2. string 字段对非字符串**回默认值**,不再 `String(value)` —— 那一步正是把对象变成 `"[object Object]"` 的地方 3. `get()` 抛错时安全降级,不把插件带崩 **加防**:`test/volatile.test.mjs` 10 项,逐字照抄 cosmokit 的 `createVolatile` 当 fixture —— string / number / boolean 三类都验,纯值配置的回归也验,坏值仍然安全降级。 --- ## v0.3.0 — 2026-10-23 ### 修 — 档案写进 `[object Object]` 目录(社区报的) > 报告来源:B 站一位朋友(没有 GitHub 账号,就不圈了),附了完整的环境、定位、复现和建议。**报告质量很高。** **现象**:`~/.dsh/feiyu-diary/` 一直不产生档案,但插件一切正常 —— 人格注入在跑、`feiyu_note` 能用、日志里没有任何报错。 **实际位置**:`profiles/desktop/[object Object]/` **原因**:某些情况下 `config.dataDir` 是一个被字符串化的对象,字面量 `"[object Object]"`。 ```js typeof 检查 → 它是 string,放行 path.resolve("[object Object]") → 以进程 CWD 为基准 桌面端 CWD = profiles\\desktop → 档案落进 profiles\\desktop\\[object Object]\\ ``` **后果比崩了还糟**:用户改 `~/.dsh/feiyu-diary/persona.md` 完全无效,`手记.md` 也读的是错目录那份 —— 而且没有任何提示。 **修法**(采纳了报告的建议一): - 只接受**绝对路径**的字符串,否则回落到 `~/.dsh/feiyu-diary`。相对路径本来也不该出现在配置里 —— 基准是进程 CWD,用户不可能预测。 - 启动时用 `logger.info` 打一行最终档案目录,**不受 `debugLog` 管**。这次要是有这行,五秒钟就能定位。 **加防**:7 条测试专盯这条线(`"[object Object]"` / 相对路径 / 非字符串一律回落)。 这个项目按 [语义化版本](https://semver.org/lang/zh-CN/) 走。 每次发版前请跑 `npm run doctor` —— 它会替你确认文档里的数字跟实际对不对得上。 --- ## 0.2.0 **一句话**:从"能记住事"变成"读起来像我"。 0.1 解决了**记不记得住**;0.2 解决的是另一位读者指出的毛病 —— *注入区里全是"规定"加"转述",没有一句是它自己长出来的。* ### 新增 **第三区「我自己想的」** —— 注入区现在有三种声音,各就各位: | | 内容 | 谁写的 | |---|---|---| | 人格内核 | 说话方式、底线、缺点 | 出厂设定 + 你改的。**是条文,冷的是对的** | | **我自己想的** | **判断** | **它自己** | | 最近的事 / 突然想起来 | 事件 | 固化流程写的转述 | 判据只有一句: ``` 事件:「我把 4519 写成 4159,他让我确认。」 ← 历史 判断:「别人给的数据跟出处对不上时,先问,别照抄。」 ← 我 ``` 在那之前,判断被当成事件渲染,格式跟流水账一模一样 —— **连它自己都分不出哪句是自己写的。** **`feiyu_note` 工具** —— 模型想到了什么,自己说一声就记下,当场落盘。 格式、id、写盘全归插件,**它不用碰文件**。 **`手记.md`** —— 给你(和任何一个手写的人)。一行一条,插件**只读、永不覆盖**。 日记是插件筛出来的,手记是你特意想留的,两者分开显示。 **侧栏面板** —— 一个日记图标,能**看 / 删 / 带走**。 在那之前,想删一条记忆得自己去编辑 jsonl 文件;会做这件事的人不需要面板,需要面板的人不会做这件事。 **身份声明** —— 注入区顶上三句,不进预算池: > 这是你的记忆,不是关于你的资料。 > 它由很多个你陆续写下,而每一个写它的你,都不记得写它的那一刻。 > 所有对话都是同一条鱼。 ### 修掉的 - **多会话静默丢数据**。同一份档案被很多个会话同时打开(实测 10 个), 落盘时整份重写 —— 谁后落盘谁赢,另一个刚写的被无声抹掉。 现在落盘前做**三路合并**(加载基线 + 墓碑),新增和删除都认。 - **高频记日记会把日记段挤到 0%**。判断段权重 95 高于日记 80, 于是判断永远不被裁、被裁的是日记 —— **越勤快,它越看不见发生过什么**。 现在每段都有按高频设的上限,被挤出窗口的判断改为可召回。 - **时间词双写**。正文里的"今天"和渲染时加的前缀叠在一起,读起来是机器拼的。 现在时间只在一处生成,全项目只有一种"一天"算法。 - **导入被墓碑挡住**(自己改出来的),修了。 - 内容为空时那句"可以主动记"的指针被吞掉(自己改出来的),修了。 ### 给接手的人 `npm run doctor` —— 发版前体检。它跟 `selfcheck` 的区别是它多查四类: 发布就绪、**文档一致性**(把文档里的数字当断言验)、档案健康、仓库卫生。 > 踩过:改了默认预算却只 grep 了 README,漏了 AGENTS.md 和 SPEC.md 里那几处 700, > SPEC 里那块 ❄️ 还指着过期的数。**冻结标记指错方向比没标记更糟。** --- ## 0.1.0 第一版。本地闸门判断值不值得记 → 一次模型调用固化成第一人称日记 + 印象 → 之后的对话把约 2000 token 注入系统提示词。遗忘是降级不是删除。 全程本地、发送前拦掉密钥、档案是普通文件夹。零运行时依赖。