# 0.9.1 更新说明 2026-11-06。这一版是**修 bug 版**,没有新功能,不改配置键、不改事件字段、不升事件格式版本——装上去旧数据照读。 修的是社区报上来的十条(issue #32–#38、#41–#43,@BBleae),加几条用户直接反馈的问题(换房问话每轮回弹、B 站评论看不到、艾特不生效、cookie 观感),并合入四个社区 PR(#39、#40、#44、#45)。都在本地复现过,**都是我们这边的错**,不是用户配置问题。按「你会看到什么」排: - **面板上「说 N」不再被聊天撑大,「记下 N 条」旁边多了口径说明**(#32)。 - **当前会话被归档后,认回来的是最近那条,不再是库里最旧那条**(#33)。 - **群聊房里她写东西不再算「你来过」,冷落检测恢复正常**(#34)。 - **卡片里手打的答案能认出来了;你手打的私事不再落到日志里**(#35)。 - **排队叫醒不会被上一轮的收尾清掉**(#36)。 - **Windows 上建会话偶尔失败(EPERM)不再一次就放弃**(#37)。 - **带 BOM 的 JSON 不再被当成坏文件改名、内容「凭空消失」**(#38)。 - **「想给下一个房间留一句话吗」不会每回一句都弹了**;换会话失败时也不再问、并冷却 5 分钟再试。 - **「多久看一次有没有人回复」真的按间隔去问了;扫到的回复会主动投给她**;`bili_look` 也能读自己的评论区了。 - **@ 艾特按昵称原文匹配,不再被改成下划线**;只粘 cookie 保存也不再清空 uid / 昵称。 - **cookie 输入框空着是设计**,文案补了说明;「看不到评论」与 cookie 无关,是 B 站按 IP 的间歇风控。 - **`her_say` 真的按「一天最多几次」拦住了**;它写下的成长坐标也不再是写死的 1/1(#41)。 - **回执从「说出去了」改成「记下了」**——那一步本来就只记流水,不会让她的话出现在你的对话里(#41)。 - **关掉「兴趣」之后,不会再每 6 小时因为一句旧句子把她叫醒**(#42)。 - **用 `plugin add` 升级留下的旧包名能清干净了**;「检查更新」在 pnpm 布局下也认得出装在哪(#43)。 --- ## 一、面板上的数字(#32) ### 你会看到什么 时间线里不再出现几十行「说了:…」。她一次醒来跟你连聊很多句时,那一段会折成**一行「聊了 N 句」**。面板上原来的 `说 N` 会把聊天行减掉,聊天另起一个 `聊天 N`。 「她记下 N 条」这个数字本身**没变**,但鼠标停上去现在会说清楚它数的是什么。 ### 为什么会这样 两件事。 **第一件:聊天行混进了「她做的事」。** 你在侧栏跟她说话,她每回一句,那一轮结束时都会在事件流里落一条 `utterance`(标着 `uncommitted`,意思是「这句是对话里回的,不是她自己认下的」)。可面板读事件流时没看这个标:它跟「她自己认下的判断」一样算进 `said`,也一样往时间线上推一行「说了:…」。 于是两个症状: - 你连说四十几句,时间线 40 条的上限全被聊天占掉,那一轮真正做了什么反而看不见; - 那一块本来「醒了但没干事」,因为塞了聊天行,被算成「干了事」——`idleWakes` 直接掉到 0。 现在带 `uncommitted` 的行单列成新的 `chat` 计数:不进 `said`、不算这一轮做了什么、时间线上同块内连续多句折成一行。**事件流一个字节没动**,只是读法变了。 **第二件:「她记下 N 条」对不上。** 那个数字来自 `countEvents().real`,它数的是「所有不是系统记账的事件」——包括**你**说过的话(`heard`)、她读到并记下的别人说法(`voice`)、开局那份起点(`seed`)。文案只写「她记下」,看起来就像「她写了 N 条」,也就难怪一直对不上。 `real` 的定义这次**不改**:动它会牵一大串派生数字(`/state`、`/events`、设置页三处都读它),而且有测试逐字段钉着它的返回形状。改的是把话说清楚——两处文案各加一句鼠标悬停说明,讲明这个数包含什么、不包含什么(系统后台记账另计)。 ## 二、归档后认错了会话(#33) ### 你会看到什么 你在侧栏把当前那条会话归档了,而她家同屋还有两条以上活会话时,以前她可能认回**最旧**那条继续住。现在认最新那条。 ### 为什么会这样 宿主在 workspace 记录里写会话 id 是**往前插**的:`sessionIds: [新id, ...旧的]`。我们的 `pickLiveSession` 取的是 `live[live.length - 1]`——末尾,也就是最旧那条,注释还写着「宿主按创建顺序往追加,末尾即最近那条」。方向反了。 现在改成取 `live[0]`,注释一起改,测试里的假宿主也从 `push` 改成了 `unshift`,跟真宿主对齐。 **还有一件事没解决**:你在侧栏拖动过会话顺序时,`live[0]` 是列表最上面那条,不一定最新。要严格按时间排得问宿主的 `sessionController.list()` 拿真实时间戳。这次只修明显反了的方向。 ## 三、群聊让冷落检测失灵(#34) ### 你会看到什么 她不再因为你没出现而装作没发现。以前群一热闹,她就以为「他今天来过」,那条「那个人有 N 天没出现」的记账永远不会写下;现在会正常写。 ### 为什么会这样 插件判断「你来过」不靠聊天内容,靠一条专门的记账行 `meta seen:<时间>`。冷落检测(`absence.js`)只看最近一道 `seen:` 是多久以前;默认超过 2 天(配置项 `absenceAfterDays`)就落一条 absence。 写 `seen:` 的地方有三处:你在侧栏打字(`turns.js`,主路)、插件替你把外面渠道的话递进来(`say.js`)、以及**她当场写东西时顺手补一条**(`capture.js`)。第三处是兜底:有些渠道进来的消息,`turns.js` 分不清是不是人(比如带 `kind:user` 却没带时区,判成「不知道」→ 不记 `seen:`),这时如果她这一轮刚好写了东西,就补一条,免得漏记。 问题就在这第三处:它只问了一句「现在在不在叫醒窗口里」,**没问这句话是在哪间房说的**。群房里群友说一百句,她每回一句都调 `growth_commit` 记下,每次都落在窗口外 → 每次都刻一道 `seen:` → 冷落检测永远不触发。 现在写之前先翻 `channels.json`(房间登记表):这条会话登记成 `group` 就跳过,不是就照写。 ### 为什么不干脆删掉这段 试过。删掉会红 4 条测试(`wake-spacing`、`p1` 里都钉着「窗口外写的要记 `seen:`」),而且真会漏记——上面说的「带 `kind:user` 但没时区」那类消息全靠这段兜底。 ### 只跳群房,不跳另外两处 另外两种「窗口外、但其实是插件自己把她推醒」的轮:换房前问她上一间还有没有话(`home.js:753`)、定时任务与 goal。这两处**照旧记** `seen:`,这是 2026-11-05 用户确认过的口径: - 换房前那句通常是在应你上一轮的话,算你来过的证据; - 定时任务分不出「她自言自语」和「她在回你」,要分开得先给每种叫醒打标记,不在这一版做。 ## 四、手打的答案与日志(#35) ### 你会看到什么 在卡片里直接**打字**回答(而不是点按钮),她现在能认出来并照做。以前一律当成「没答」,白白等到超时。 另外,你手打的私事不会再原样落到 `logs/dsh-fish.log` 里。 ### 为什么会这样 宿主返回的答案形状是 `{ id, selected: [], custom: '你打的字' }`——选了自由文本时,`selected` 是**空数组**,内容全在 `custom` 里。我们以前只读 `selected[0]`,空数组于是取到 `undefined`,一律判成 `NO_MATCH`。 `askinchat.js` 里早就有一份写对的 `picked()`(非空 `selected` 优先,否则退回 `custom`),`ask.js` 没用它。现在改用同一个函数。 日志那半:认不出的时候,原来会把整份回执 `JSON.stringify(...)` 塞进 warn 日志。你手打的那段话就这么落进了日志文件。现在换成形状摘要——只留 id、选了几个、自定义文本多长。 ## 五、排队叫醒被上一轮清掉(#36) ### 你会看到什么 你点「叫她一次」时,如果她正在别的轮次里忙,这条叫醒以前可能**排进下一轮却什么都没收到**——处境段被上一轮的收尾清空了,她醒过来一脸空白。现在会等到真正属于她那轮结束。 ### 为什么会这样 `watchTurnEnd` 认**会话**不认**回合**:只要这条会话上出现 `turn/end`,就当成「她这一轮结束了」,把处境段收走。 而投递走的是宿主的 `followup`:宿主忙时会把它放进 next-turn 队列,当前那一轮先 `turn/end`,然后才开下一轮。于是那个 `turn/end` 一到,处境还没送达就被清了。 `waitIdle` 能挡住大部分情况,但有两处绕过它:手动「叫她一次」的 `wakeNow` 不传 `waitIdle`,`/wake` 路由也不看忙闲;手动这条路还有 90 秒兜底,到点就清。 现在要求「监听挂上之后**先见到本条会话的 `turn/start`**,之后的 `turn/end` 才算」。拿不到事件总线时(单测、老宿主)退回老行为,靠 `waitIdle` 兜。 这条回归**已验证能复现**:把判据改回去,新加的那条测试立刻红。 ## 六、Windows 上的 EPERM(#37) ### 你会看到什么 Windows 上偶尔看到「建会话的锁没拿到:EPERM…」,然后这一次换会话就失败。现在这种暂时性的失败会重试,不再一次就放弃。 ### 为什么会这样 `withFileLock` 用 `fs.open(file, 'wx')` 抢锁。Windows 上文件**正在被删除**的那一刻去打开,返回的是 `EPERM` 而不是预期的 `EEXIST`。我们的代码只把 `EEXIST` 当「锁被占着」,其余一律 `throw`——于是 EPERM 直接被抛到上层。 (这也是 `test/tenant.test.mjs` 那条用例时好时坏的原因:单跑 25 次,23 过 2 红。) 现在 EPERM 走独立的重试分支,**受 `timeout` 约束**,且只认 win32。 ### 为什么不能并进 EEXIST 分支 EEXIST 那条路里,`stat` 失败就判「锁陈旧」然后 `continue` 重试——但它**不看超时**。真正的权限错误在那条路里会变成死循环。必须单独一条路,每一步都检查 `Date.now() - started > timeout`。 (issue 作者压测过:12 并发 × 400 次,改前 4113 成功 / 687 抛,改后 4800 成功 / 0 抛。) ## 七、带 BOM 的 JSON(#38) ### 你会看到什么 用 PowerShell 5.1 的 `Set-Content -Encoding UTF8` 或记事本改过配置文件的人,以前会发现**改的东西全没了**——配置退回默认、主人信息变 null、她家在侧栏新建了一条会话。现在照常读出来。 ### 为什么会这样 这几个文件都是 UTF-8 **带 BOM** 的。`readJson` 直接 `JSON.parse(file)`,开头的 `\uFEFF` 让解析抛 `SyntaxError`,而 SyntaxError 走的是「坏文件」分支:把**内容完好**的文件改名成 `<文件名>.corrupt.<时间戳>`,返回兜底值。 具体后果: - `config.json` → 全默认(`settings.js` 的 `fresh: true`); - `owner.json` → null; - `home.json` → 新建一条会话。 现在读之前先剥掉开头那一个 `\uFEFF`。`lib/upgrade.js` 里自己那份 `readJson` 是同样的坑(带 BOM 时读成 null,升级自检当成没装),一起修了。 --- ## 八、换房那句问话每回一句都弹(用户反馈,2026-11-06) ### 你会看到什么 聊天聊到该换会话的时候,她那边每次说完话、你这边就冒出一条: > (你在这个房间里已经说了不少话。屋子要换一间新的了——还是同一间屋子,只是对话重新开始。) > 想给下一个房间留一句话吗?留什么都可以。不想留就说「不用」。 而且**答了也没用**——回「不用」之后,下一次她说完话,这条还会来。 ### 为什么会这样 两个错叠在一起。 **第一件:问话问得太早了。** 换会话的动作分几步:问一句留话 → 旧会话改名留档 → 建新会话 → 把原话和轮数搬过去。原来「问一句」排在**第一步**,而它用的 `sc.prompt({ mode: 'queue' })` 投递的是**她此刻正在用的那条旧会话**——也就是你眼前的对话窗口。所以这条问话会像一条普通消息一样直接出现在你俩的对话里。 **第二件:换不成就无限重试。** 只要「建新会话」这一步抛错(宿主没起来、工作区 id 丢了、Windows 上那个 EPERM,见第七条),函数当场返回,`turns` 也不会清零。下一轮你说完话、她答完,结账时又发现「到点了」,于是又进来、又先问一遍。你看到的就是每回一句都弹。 这也解释了为什么它「答了也没用」:那句「不用」根本没有让流程往前走,卡住的是建会话那一步。 ### 现在怎么改的 - **问话挪到新会话建好之后。** 换成功了才问她留不留话;换不成就一个字都不发——不在你的对话里制造噪音。 - **换失败记一笔时间,5 分钟内不再试。** 以前是每说完一句就去撞一次宿主、每次都留一条问话;现在 5 分钟内只试一次。真修好了(宿主恢复)下一次到点自然就换成了。 - **顺手补了个分类遗漏。** `carry-ask-*` 这条问话的 `rpcId` 前缀没写进 `PLUGIN_RPC`,于是它一直被当成「认不出来源的消息」,每换一次会话就在日志里多一行「这条消息认不出来源,没算进轮数」。不进轮数是对的,日志噪音不该有。 - **设置页那句「上下文占满后会自动换」是空话。** 代码里没有这个功能。设「0 = 永不换」的人会一直聊到宿主上下文撑不住、那一轮直接失败。文案改成说实话。 ### 顺带说明:这跟「轮数设 0」没关系 「永不换会话」这个设置**一直是生效的**:两个换会话入口(`lib/turns.js`、`lib/say.js`)前面都有 `rotateAfter > 0` 的守卫,设成 0 根本进不到换会话的流程里,也就不会问那句话。所以反馈里猜的「没适配永不换」不是真因,真因是上面那个「换失败后无限重试」。不过既然提到了,就顺手把其他叫醒路径都过了一遍: | 叫醒/投递 | 出处 | 要适配 `rotateAfter = 0` 吗 | | --- | --- | --- | | 新会话第一次叫醒 | `lib/home.js` `wakeIfBlank` → `wake-fresh-*` | 不用。它跟轮数无关,只在「屋子是空的」时候说一句,用 `wokeFor` 去重。 | | 出门第一轮 | `lib/observe.js` → `outing-*` | 不用。由出门开关控制,不看轮数。 | | 定时任务/主动叫醒 | `lib/deliver.js` `deliverWake` → `agent.followup` | 不用。走的是自建来源 `fish-wake`,不经过 `sc.prompt`。 | | 排队的驳回话术 | `lib/sessionnote.js` `queueNote` / `deliverQueued` | 不用。只在她家会话开着时投递,与轮数无关。 | | 群房按天换 | `lib/channels.js` `channelExpired` | 已经对了。`!(d > 0)` 直接返回 false,设 0 就是不换。 | --- ## 九、B 站评论看不到、艾特不出来、cookie「填完就消失」(用户反馈,2026-11-06) 这三件事是一起报上来的,查下去是**三个独立的毛病**,另外还牵连一个认知误区(以为跟 cookie 有关)。 ### 你会看到什么 - **评论**:开了「允许看评论」、也把「多久看一次」设成 5 分钟,仍然一条回复都收不到;等她被叫醒时偶尔又冒出来一条。现在到点就会去问,收到就直接投一条到她那儿。 - **她主动看评论**:以前她只会说「我只能拿热评,翻不到自己的评论区」。现在 `bili_look` 多了 `what=replies`,能读到「谁在你哪条评论下面说了什么」,还带着回楼中楼要用的编号。 - **艾特**:设置里写了「发评论时在最前面 @ 你」,实际评论里只有 `@` 或者根本没 @。 - **cookie**:粘进去点保存,输入框立刻空掉,让人以为没存上。 ### 为什么会这样 **一、「多久看一次」是个空旋钮。** `comment.scanEveryMin`(默认 30,范围 5–720)从加进设置页那天起就没人读:全仓库只有设置页读写和 `/comment` 回显,调度器一次都没用过;配套的 `comment.lastScanAt` 更是零读写。真正的扫描只发生在「她下次被叫醒、拼消息那一轮里顺便扫一句」——她不醒就永远不扫。设 5 分钟和设 720 分钟,实际表现完全一样。 修法:`tick` 每分钟看一眼「到点没有」(`repliesScanDue`),到点就扫,**扫完无论有没有都记一笔 `lastScanAt`**(不记的话每 30 秒就去敲一次 B 站);扫到新回复就用 `queueNote` + `deliverQueued` 投一条笔记进她的会话——和驳回话术那套投递是同一个通道。设 0 仍然表示「不主动问」。 **二、她醒着的时候没有「看我自己的评论区」这个动作。** `bili_look` 只有首页 / 搜索 / 接着看 / 看某一条四个动作,没有任何一个是「我刚才发的那条下面有没有人回」。`bili_speak` 回楼中楼又必须要 `parentRpid`,而那个编号她手上根本拿不到——于是「知道有人回了,但回不了」。 新增 `what=replies`:读她最近发过的评论下面的楼中楼,**不受「已读过」标记限制**(她主动看就该看全),每行末尾给出 `[reply=… parent=…]`,回楼中楼用哪个编号一眼可见。 **三、@ 艾特被自己改坏了。** `mentionPrefix` 原来会把昵称里的空格换成下划线再拼 `@`。真机抓到的真实 @ 评论里,`content.members` 里的 `uname` 和正文里的写法一字不差——B 站按昵称**原文**匹配,换了下划线就是 @ 一个不存在的人。 另外还有个更常见的前提:**昵称为空就根本不加 @**(这是有意为之,空的 `@` 只是一串没人认领的字符)。所以「设置了能艾特却艾特不出来」,先看那两格填没填。 **四、只粘 cookie 保存一次,会把 uid / 昵称清空。** 保存按钮原来无条件提交 `{mid, nickname}`,空串也照发;后端对空串是「写入空值」。用户只粘 cookie、那两格没填,存一次就把 `@` 的对象抹掉了——这跟第三条叠在一起,就是「艾特完全不生效」。现在三格各自只在非空时提交,全空直接提示「没东西可存」。 **五、cookie 显示空白是设计。** 接口从来不下发 cookie 原文(只给「存没存」、「有没有 bili_jct」两个布尔),输入框保存后清空是为了防窥屏。原本文案只写「不再回显」,容易读成「没存上」,补一句「空着不代表没存上,上面那行『已存…』才是凭据」。 ### 顺带说清:看不到评论跟 cookie 没关系 `scanReplies` 用的是**匿名**客户端,本来就不带 cookie。本机真机实测:同一份 cookie,`popular` 能读,`/x/v2/reply` 和 `/x/v2/reply/reply` 一律 `-412 request was banned`;同一台机器早些时候又能读通——这是 B 站按 IP 的间歇风控,插件这边只做冷却退避(读接口 60 秒起、翻倍、上限 10 分钟)。 所以「看不到评论」排查顺序应该是:① 设置里那个开关开了没;② 日志里有没有 `-412`(有就是撞上风控,等一会儿或换网络);③ 有没有真的发过评论——没有发过,就没有「自己的评论区」可扫,这是设计,不是故障。 --- ## 十、主动开口的账、旧句子空醒、旧包名升级(issue #41–#43,2026-11-06) ### 你会看到什么 **一、`her_say` 说好的上限是真的。** 设置里写「一天最多几次」,以前这句话只在说明文书里成立:连调 8 次全成功,开关关着、上限设 0 也照写。现在到上限就记不进去,回一句「今天已经主动开口 N 次,到上限了。明天再说;这不是工具坏了」。 **二、她主动开口时记下的成长坐标不再是写死的 1/1。** 别的写入点(日记)都用 `deriveSelf` 算出的 `{day, stage}`,只有这里硬编码成第 1 天第 1 阶段,于是成长一致性检查会跟她早期的行错配对——阶段 1 少算一对、阶段 4 多算一对。改成跟日记同一套算法。 **三、回执改名:`[说出去了]` → `[记下了]`。** 那一步只是往她自己的流水里记一行,你的对话里看不看得见,取决于她这一轮有没有直接写出来。原来那个名字会让她(和你)误以为消息已经发出去了。 **四、关掉「兴趣」之后,桌面不再摆旧句子。** 以前只要桌上有旧句子、距上次醒来超过 6 小时,就叫醒——而且这一轮**不摆事件卡、不写 `fired:`、不想一想、不递规则申请、「今天到这里」也不给**。结果是卡池永远空着、想一想永远轮不到。作者用 14 天合成数据跑出来:叫醒 14 次、14 次全是旧句子、事件卡 0、想一想 0。现在「兴趣」关掉就不摆旧句子,这一轮回到正常的空醒判断。 **五、三句说死了的话松开了。** 「这次隔了 N 个小时」其实只数 `wake:`(她主动开口、你跟她聊的那些都不算),改成「距你上次醒来,隔了 N 个小时」;「这些天再没被提起过」的判据只是「之后有没有事件 `refs` 指回它」,改成「还摊在那儿」;桌上的「还有一件没合上的事」暗示她还没得到答案,其实只是她反复问过,改成「还有一件你反复问起的事」。设置页兴趣卡里那句「问过还没答案的问题」一起改。 **六、用 `plugin add` 升级之后,旧包名能清干净了。** 按更新说明执行 `dsh plugin add "@ssamurias/dsh-fish@0.9.0"` 的人,profile 里留下的是**版本号**依赖;而清理脚本原来只认 `link:` / `file:` 两种写法,一句「没有老依赖,跳过」就退出 0——旧键 `dsh-fish` 与旧 `bundles` 条目原封不动,下次 pnpm 清理时两条 fish 打架。现在三种写法(源码目录 / tgz / 版本号)都收拾,写入前先备份 `package.json.bak-fishrename-<时间戳>`。 **七、「检查更新」在 pnpm 的 isolated 布局下也能显示下载按钮了。** 从模块路径反推 profile 根时,遇到 `node_modules/.pnpm/<包>@<版本>/node_modules/<包>/` 这种真身路径,以前会截到 `.pnpm/...` 那一层——那里没有 `package.json`,于是判成「没装」,`managed:false`,按钮永远不出现。现在往回退到 profile 根。 **八、两个社区 PR 已合入**(@BBleae):#40 主动搭话关着时不再每轮同步算一遍 `deriveSelf`(8000 条事件实测从 7.6 秒降到 50 毫秒级);#39 「只留证」的事件(聊天行、带 `selfmod` 的说法)不再被当成她问过的 / 总说到的摆回桌上,也不算她「自己动过」。 **九、又是一对社区 PR(#44、#45)已合入**(@BBleae): - **配置偶尔整个回默认,是它自己撞坏的。** `writeAtomic` 写文件时先写临时文件再改名,而临时文件名只有「进程号 + 毫秒」。同一毫秒里两次写(设置页自动保存、调度器记账撞在一起)会共用一个临时文件,内容交错,改名后就是半个 JSON。坏文件进 `readJson` 的坏档分支被收走并返回 `null`,`updateConfig` 按默认值处理再写回去——`window`、`wakesPerDay`、`proactive.enabled` 这些就都回到默认了。现在临时文件名带 4 字节随机后缀。 - **「她靠工具发话」的部署现在记对了账。** 有的部署里她的正文不会自动转发,要靠一件发送工具才发得出去。这种部署上主动回合只认正文,于是她明明发出去了却被记成「没开口」:不占额度、不起冷却,够长的还被折成一行墓碑,模型侧之后看不到自己发过什么。新增 `proactive.sendTools`(填发送工具名,工具调成功才算开口)、`proactive.idleCountsSeen`(`seen:` 也算打断安静,不然「你刚聊完」它以为「好久没人理你」)、`proactive.foldSilent`(上下文很大、靠缓存省钱时关掉折叠)。处境段与身份段也会照实写「用这个工具发、正文发不到聊天里」。 - **主动回合投递前也看她在不在忙了。** 定时叫醒一直有这个判断,主动回合没有——她正开在别的一轮里时投递,处境段挂在旧轮上、旧轮一结束就被清掉,真醒的那一轮只收到一个空格。现在两边都看本进程的忙碌表,也认别的插件发来的 `dsh-fish/busy` 事件(谁在忙谁自己说,不用互相知道包名)。 ### 需要你动手的一步(只影响从 0.9.0 以前升级的人) 如果你当初是用 `plugin add` 装的,profile 里可能还留着旧的 `dsh-fish` 键。手动跑一次: ```bash node scripts/fix-legacy-link.mjs --profile desktop --write ``` 不加 `--write` 是预览,只告诉你它会改什么。它会把旧键、旧 `bundles` 条目清掉,老链接改名成 `.disabled-<时间戳>` 而不是删除。 --- ## 迁移 **无必改项。** 这一版不改事件字段、不升 `EVENT_SCHEMA_VERSION`;旧数据照读,行为只朝「更少误判」的方向变。 新增三个**可选**配置(`proactive.sendTools` / `foldSilent` / `idleCountsSeen`),默认值等于旧行为——不填、不配置的部署一处不变。只有「她写的正文不会被自动转发、要靠工具才发得出去」的部署才需要填 `sendTools`。 一处**观感上会变**:`her_say` 的回执从 `[说出去了]` 改成 `[记下了]`,桌上一句从「还有一件没合上的事」改成「还有一件你反复问起的事」。都是措辞,不动数据。 一件要说明的:**已经被误改名成 `.corrupt.<时间>` 的老文件不会自动恢复**。内容本身是完好的,只是名字被改了;需要的话手动改回原名即可(这次决定不自动扫、不自动改回)。 ## 测试 全量 `node --test` = **1512 pass / 0 fail**(0.9.0 是 1441,本版新增 70 条,其中四个社区 PR 带来 29 条)。 新增的:#33 认最新会话;#34 群房不记 `seen:` / 家里那间照记;#35 手打答案能认出来、日志不落原文;#36 前一轮的 `turn/end` 收不走处境;#37 EPERM 重试 + 一直失败时受超时约束;#38 带 BOM 照读且不改名;#32 聊天折行与 idle、判断与聊天的分界、聊天之后的状态;换房失败不问话、失败后冷却 5 分钟、冷却过了能再试并把失败记录清掉;`carry-ask-*` 判成 plugin;`repliesScanDue` 按间隔生效且 0 = 不主动问;`readMyReplies` 不受已读标记限制、带 bvid 只看一条;`renderMyReplies` 带编号与素材声明;`tick` 真的按「多久看一次」去问并记下时间;`what=replies` 能读出回复且不写「看过」;`mentionPrefix` 保留昵称原文。还有:#41 `her_say` 按上限拦、成长坐标不是 1/1;#42 兴趣关掉不再摆旧句子;#43 版本号依赖也能清账、pnpm isolated 布局认得 profile 根。另有 #44 的 20 条(靠工具发话、空闲时钟、忙闲检查、处境与身份段说实话)与 #45 的 1 条(同一毫秒同时写同一个文件不出半截内容)。**#36、换房、#41 那几条已验证能复现**——把判据改回去就红。