@ssamurias/dsh-fish 0.9.0 → 0.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +35 -4
- package/CHANGELOG.md +46 -1
- package/README.md +11 -6
- package/RELEASE-NOTES-0.9.1.md +296 -0
- package/RELEASE.md +151 -0
- package/lib/ask.js +18 -5
- package/lib/browse.js +1 -0
- package/lib/capture.js +16 -1
- package/lib/client.js +50 -11
- package/lib/comment.js +74 -0
- package/lib/compact.js +45 -1
- package/lib/curiosity.js +6 -2
- package/lib/derive.js +11 -2
- package/lib/desk.js +3 -1
- package/lib/dreamtable.js +13 -2
- package/lib/drive.js +14 -2
- package/lib/freewake.js +4 -1
- package/lib/home.js +87 -22
- package/lib/index.js +1 -1
- package/lib/interest.js +3 -1
- package/lib/look.js +23 -3
- package/lib/observe.js +9 -0
- package/lib/owner.js +5 -2
- package/lib/persona.js +16 -2
- package/lib/proactive.js +67 -12
- package/lib/scheduler.js +99 -27
- package/lib/selfdoc.js +5 -3
- package/lib/selfupdate.js +8 -1
- package/lib/settings.js +5 -0
- package/lib/status.js +31 -3
- package/lib/store.js +14 -2
- package/lib/tenant.js +12 -0
- package/lib/turns.js +5 -1
- package/lib/upgrade.js +4 -1
- package/package.json +2 -1
- package/scripts/fix-legacy-link.mjs +197 -90
package/AGENTS.md
CHANGED
|
@@ -69,7 +69,7 @@
|
|
|
69
69
|
node --test
|
|
70
70
|
```
|
|
71
71
|
|
|
72
|
-
当前基线 **
|
|
72
|
+
当前基线 **1512 个**,独立进程跑 `node --test` 应全绿。任何一条红都不许提交——包括看起来"不是我弄的"那条。
|
|
73
73
|
|
|
74
74
|
按环境挑跑法:
|
|
75
75
|
|
|
@@ -144,7 +144,24 @@ GET /dsh-fish/whoami
|
|
|
144
144
|
3. **外来场次必须带说话人前缀**。`lib/say.js` 对 `group` 与 `guest` 都用 `speaker` 拼 `某某在群里说:…` / `某某说:…`。不拼的话,群里三个人的话在她看来跟一个人连说三句一模一样,她会把三个人的观点拼成一个"他"。外来文本统一过 `lib/guard.js` 的 `sanitizeForeign`。
|
|
145
145
|
4. **桥不要用宿主的 `source.form`**。`snapshot`、`notice`、`catalog`、`instructions`、`recall` 是宿主前端的保留词表,`classifyMessage` 见到就当上下文,不算人(`MACHINE_FORMS`)。转述人说的话,要么不带 `form`,要么用 `relay`。
|
|
146
146
|
|
|
147
|
-
### 7.3
|
|
147
|
+
### 7.3 对话靠工具发出去的部署
|
|
148
|
+
|
|
149
|
+
先问一句:**她写的正文会被插件自动发出去吗?** 会,就什么都不用填,只认正文的默认行为是对的。不会,也就是正文只留在会话里、要调工具才发得出去,才填下面这几项。都不认具体插件,工具名由配置给。
|
|
150
|
+
|
|
151
|
+
- `proactive.sendTools`:发送工具的名字,也要放进 `tools.allow`(工具面收窄开着时,没放行她调不到)。填了之后,只有这些工具调成功才算她开了口(记 `proactive-turn:`,占额度,不折叠),代码模式(run_code)里调的也算;只写正文算没开口。处境和身份段会照实写用这个工具、正文发不到聊天里。工具自己返回「没发出去」但没有报错的,这里认不出来。
|
|
152
|
+
- `proactive.idleCountsSeen`:`true` 时 `seen:` 也算打断安静、也算「他接了」。她靠工具回话、正文不进事件流时要开。它要有 `seen:` 可认:走 `/say`,或者 `source.kind` 不是 `user`,或者带 `clientTimeZone`;`{kind:'user'}` 不带时区的桥要自己写。代价是他在这台机器上和别的 agent 说话也算,所以默认关。
|
|
153
|
+
|
|
154
|
+
还有一项和工具无关:`proactive.foldSilent` 是 `false` 时,没开口的一轮只记账、不折。折叠会改历史,上下文很大、靠缓存省钱时可以关。
|
|
155
|
+
|
|
156
|
+
插件知道她在忙时(比如在打一局游戏),挂一个事件就行,不用知道 dsh-fish 的包名和路径:
|
|
157
|
+
|
|
158
|
+
```js
|
|
159
|
+
ctx.on('dsh-fish/busy', (sessionId) => stillPlaying(sessionId) ? true : undefined)
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
定时叫醒和主动回合投递前都会问一声。监听要同步返回,只有 `true` 算忙;不忙返回 `undefined`、`null` 或 `false`,返回别的值会挡住后面的插件,返回 Promise 按不忙算。忙的标记卡住时她就一直不被叫醒,所以结束时记得放开。不挂也没事,调度器自己知道家里那条会话正在跑的那一轮。
|
|
163
|
+
|
|
164
|
+
### 7.4 另一条老路:工具白名单(仍然有效)
|
|
148
165
|
|
|
149
166
|
桥如果是以插件形式装进来、暴露工具给她的,仍然可以在 `config.tools.allow`(设置页「插件能力白名单」)里放行工具名。qq-bridge 默认要放行的是:
|
|
150
167
|
|
|
@@ -172,18 +189,32 @@ qq_send_message, qq_get_unread_messages, qq_reply
|
|
|
172
189
|
- [ ] 三个依赖字段还是 `{}`
|
|
173
190
|
- [ ] 没有新增人格设置入口
|
|
174
191
|
- [ ] 新输入口都过了 `guard.js`
|
|
175
|
-
- [ ] `node --test`
|
|
192
|
+
- [ ] `node --test` 1512 全绿
|
|
176
193
|
- [ ] 界面文案没有「他」「四道门」「三道门」
|
|
177
194
|
- [ ] 改了 `EVENT_SCHEMA_VERSION` 的话,`CHANGELOG.md` 里写了迁移说明
|
|
178
195
|
- [ ] 没动包名;真要动,先看下面「包名与装法」那节
|
|
196
|
+
- [ ] 动了升级 / 迁移脚本(`scripts/fix-legacy-link.mjs`、`lib/selfupdate.js`)的话,`README.md` 的升级表与 `RELEASE-NOTES-*.md` 同步改了
|
|
179
197
|
|
|
180
198
|
---
|
|
181
199
|
|
|
182
200
|
## 包名与装法(0.8.10 起,改名踩过一次,别再踩)
|
|
183
201
|
|
|
184
|
-
包名是 **`@ssamurias/dsh-fish`**(带 scope 的全名)。装法看 `README.md`
|
|
202
|
+
包名是 **`@ssamurias/dsh-fish`**(带 scope 的全名)。装法看 `README.md` 的「安装」,这里只列动代码时容易破坏的四条:
|
|
185
203
|
|
|
186
204
|
1. **`lib/client.js` 里 `__ModuleLoader__.load({ id })` 必须等于 `package.json` 的 `name`**,不是短名。client-modules 按 loader 条目名(完整包标识)解析浏览器包,名字对不上时客户端那半**静默不加载**——宿主照常跑、设置页空着,看起来像没装上。`test/client.test.mjs` 直接从 `package.json` 读 `name` 比对,别把那条断言改成写死字符串。
|
|
187
205
|
2. **`cordis.patch.yml` 的 `id` 保持短名 `dsh-fish`、`name` 写全名**(与 `@goodandready/dsh-voice` 一致)。`id` 变了会连带 client bundle 的 URL 对不上;`name` 写短名则 import 失败、直接崩在 web boot。
|
|
188
206
|
3. **别删升级脚本里的 normalize 那一步**(`lib/selfupdate.js` 的 `normalizeScript()`,安装脚本第 5/6 步)。0.8.9 之前装的 profile 里旧键还在,不清掉就会同时有两条鱼、两条 insert 撞一个 id,启动报 `duplicate factory registration`。
|
|
207
|
+
4. **`scripts/fix-legacy-link.mjs` 必须三种依赖写法都认**:源码目录 `link:`、`file:` 装的 tgz、`@ssamurias/dsh-fish@x.y.z` 版本号。版本号那条是 issue #43 的根因——照更新说明用 `dsh plugin add` 升上来的人,profile 里留下的就是版本号;脚本过去只认前两种,一句「没有老依赖,跳过」退出 0,旧键与旧 `bundles` 条目原封不动。改它之前先跑 `test/fix-legacy-link.test.mjs`(`migrateOne()` 是给它直接测的)。
|
|
208
|
+
|
|
209
|
+
---
|
|
210
|
+
|
|
211
|
+
## 发版
|
|
212
|
+
|
|
213
|
+
完整步骤(发布前自检、push、npm publish、tag、GitHub Release、发布后验证、回滚)写在 **`RELEASE.md`**,动手前通读一遍。三条不能省的顺序:
|
|
214
|
+
|
|
215
|
+
1. `node --test` 全绿、`npm pack --dry-run` 清单对(见 §6)。
|
|
216
|
+
2. 先推代码,再发 npm:**npm 成功之后才打 tag / 建 Release**——npm 失败时不留悬空 tag。
|
|
217
|
+
3. 发完核对 `npm view @ssamurias/dsh-fish version dist-tags`,再去回 issue。
|
|
218
|
+
|
|
219
|
+
本机约束另见 `D:\deepseekagent\避坑指南.md` 第 1 节(profile 的 `bundles` 与 `dependencies` 必须一一对应)与第 3 节(运行中替换已加载包会 `ERR_PNPM_EPERM`、`minimumReleaseAge` 会回滚刚发布的版本、`desktop` profile 被应用独占)。
|
|
189
220
|
|
package/CHANGELOG.md
CHANGED
|
@@ -1,4 +1,49 @@
|
|
|
1
|
-
## 0.9.
|
|
1
|
+
## 0.9.1 — 2026-11-06
|
|
2
|
+
|
|
3
|
+
社区报的 issue #32–#38、#41–#43(十条,@BBleae)加用户直接反馈的几条(换房问话每轮回弹、B 站评论看不到、艾特不出来、cookie 观感),另外合入四个社区 PR(#39、#40、#44、#45)。每一条都在本地复现过,都是我们这边的错,逐条修掉。
|
|
4
|
+
|
|
5
|
+
- **聊天行把时间线刷满,`idleWakes` 也被清零(#32)。** 她答你的话(`lib/turns.js` 打的 `uncommitted` 标)以前和「她自己认下的判断」混在一起数:一次醒来撞上你连说几十句,时间线 40 条全是「说了:…」,那一块也从「在发呆」变成「干了事」。现在单列成「聊天」:不进 `said`、不算这一轮做了什么、时间线上折成一行「聊了 N 句」,面板上的 `说 N` 也把聊天行减掉、另起一个 `聊天 N`。**事件流一个字节没动**,只是读法变了。
|
|
6
|
+
- **「她记下 N 条」和「记下的事」永远对不上(#32)。** 那个数字是 `countEvents().real`,本来就含你说话、她读到别人的说法、开局那份起点;文案只写「她记下」,看起来就像「她写了 N 条」。`real` 的定义**不改**(动了会牵一大串派生数字,测试也逐字段钉着返回形状),改成把话说清楚:两处文案各加一句口径说明(鼠标停上去能看见)。
|
|
7
|
+
- **当前会话被归档时,认回来的是最旧那条(#33)。** 宿主建会话是 `sessionIds: [新id, ...旧的]`,往**前**插;`pickLiveSession` 却取 `live[live.length - 1]`,注释还写着「末尾即最近那条」——方向反了。改成取 `live[0]`,注释一起改,测试里的假宿主也调成同样的方向。提醒一句:你在侧栏拖动过顺序时 `live[0]` 未必最新,那要问 `sessionController.list()`;这次只修明显反了的方向。
|
|
8
|
+
- **群聊那轮也写 `seen:`,冷落检测失灵(#34)。** `lib/capture.js` 里那条「他在场」的记账只看在不在叫醒窗口里,不看这句话是在哪儿说的:群里她调一次 `growth_commit` 就多一条 `seen:`,群友说一百句也算他来过,于是「你多久没说话」永远不触发。现在先查 `channels.json`,群房一律不记。窗口外那条兜底写法保留——删掉会让换房、定时任务那几次醒来失去在场证据(这也是 issue 作者试删后红了 4 条测试的原因)。
|
|
9
|
+
- **卡片里手打的答案认不出来,原文还落进日志(#35)。** 宿主回的是 `{id, selected: [], custom: '…'}`,`lib/ask.js` 以前只看 `selected[0]`,空数组于是取到 `undefined`,一律判成 `NO_MATCH`。改用 `askinchat.js` 早就写对的 `picked()`(非空 `selected` 优先,否则退回 `custom`)。顺带把日志里的回执原文换成形状摘要——只留 id、选了几个、自定义文本多长,你手打的私事不再落到日志文件里。
|
|
10
|
+
- **排队叫醒被上一轮的 `turn/end` 收走(#36)。** `watchTurnEnd` 认会话不认回合:手动叫她一次时她常常正开在别的一轮里,宿主的 `followup` 会把我们这条排进下一轮,当前那一轮先 `turn/end`,处境段当场被清,真醒的那一轮反而什么都没有。现在要求「监听挂上之后先见到本条会话的 `turn/start`,之后的 `turn/end` 才算」。拿不到事件总线(单测 / 老宿主)时退回老行为,靠 `waitIdle` 兜。
|
|
11
|
+
- **Windows 上 `withFileLock` 遇 EPERM 直接抛(#37)。** 文件正在被删除时 `open(..., 'wx')` 返回 EPERM 而不是 EEXIST,于是这一次换会话就失败。现在 EPERM 走独立重试分支、受 `timeout` 约束,且只认 win32——POSIX 上 EPERM 是真没权限,重试没意义。**不能并进 EEXIST 分支**:那里 stat 失败就判 stale 再 continue,不看超时,真的是权限错误时会死循环。
|
|
12
|
+
- **带 BOM 的 JSON 被当成坏文件改名(#38)。** `readJson` 直接 `JSON.parse`,BOM 抛 SyntaxError 就进坏文件分支,**内容完好的文件**被改名成 `.corrupt.<时间>`:`config.json` 退回全默认、`owner.json` 变 null、`home.json` 新建一条会话,用户以为自己的改动丢了。现在读之前先剥掉开头那一个 `\uFEFF`;`lib/upgrade.js` 里自己那份 `readJson` 同样的坑,一起修。
|
|
13
|
+
- **换房那句「想给下一个房间留一句话吗」每回一句都弹(用户报,2026-11-06)。** `rotateTalkSessionOnce` 先问话、再建会话:问话那条 `sc.prompt({mode:'queue'})` 投进的是她**此刻正在用的旧会话**,也就是你眼前的对话窗口。要是随后 `sc.create` 抛错或宿主没给 id,函数当场返回、`turns` 不归零,下一轮结账又到点、又进来、又问一遍——你那边看到的就是「每回一句都弹这句」。修法两处:① 问话挪到**新会话建好之后**,换不成就不问;② 换失败记一笔 `rotateFailAt`,5 分钟内不再重试(`ROTATE_RETRY_MS`),免得每轮都去撞一次宿主。顺带把 `PLUGIN_RPC` 补上 `carry` 前缀——原来漏了,`carry-ask-*` 掉进 `unknown` 分支,每换一次会话就多一行「这条消息认不出来源」的日志。**注意**:这条跟「轮数设 0 = 永不换」无关,`rotateAfter > 0` 的守卫一直都在,0 根本进不来;真因是「换失败后无限重试」。
|
|
14
|
+
- **设置里那句「上下文占满后会自动换」是空头承诺。** `lib/client.js` 的 hint 原来这么写,代码里没有任何「上下文满了就换」的实现——设 0 的人会一直聊到宿主上下文撑不住、那一轮直接失败。文案改成说实话:0 = 永不换,撑不住就那一轮失败,不会自动换。
|
|
15
|
+
- **「多久看一次有没有人回复」是个空旋钮,评论永远看不到(用户报,2026-11-06)。** 这个设置(`comment.scanEveryMin`)从写下来那天起就没人读:全仓只有设置页读写和 `/comment` 回显,调度器一次都没用过。真正的扫描只发生在「她被叫醒的那一轮里顺便拼一句」——不叫醒就永远不扫,用户开了开关、设了 5 分钟也一条都收不到。现在 `tick` 每分钟看一眼到点没有(`repliesScanDue`),到点就扫,**扫完无论有没有都记一笔 `lastScanAt`**(不记的话每 30 秒重问一次 B 站);扫到新回复就投一条笔记进她的会话(`queueNote` + `deliverQueued`,跟回复量那边同一套投递)。设 0 仍然表示不主动问。
|
|
16
|
+
- **她醒着的时候没有「看我自己的评论区」这个动作(同一条反馈)。** `bili_look` 只有首页 / 搜索 / 接着看 / 看某一条,翻不到自己的评论区;`bili_speak` 回楼中楼又要 `parentRpid`,她手上没有,于是「知道有人回了,回不了」。新增 `bili_look what=replies`:读她最近发过的评论下面的楼中楼,**不受「已读过」标记限制**(她主动看就该看全),每行末尾给出 `[reply=… parent=…]`,回楼中楼要用哪个编号一眼可见。`LOOK_BRIEF` 与工具说明同步。
|
|
17
|
+
- **@ 艾特:昵称被改成下划线,等于 @ 一个不存在的人(用户报,2026-11-06)。** `mentionPrefix` 原来把昵称里的空格换成 `_` 再拼 `@`。真机抓到的真实 @ 评论(`content.members` 非空)证明 B 站是按昵称**原文**匹配的,换了下划线就认不出来。改成原样拼;昵称为空时仍然不拼(空的 @ 只是一串没人认领的字符),设置页 hint 补一句「要跟 B 站上显示的一字不差」。
|
|
18
|
+
- **只粘 cookie 保存一次,就把「要 @ 的 uid / 昵称」清空了(同一条反馈)。** 保存按钮原来无条件提交 `{mid, nickname}`,空串也照发;而 `saveOwner` 对空串是「写入空值」。用户只粘 cookie、没填那两格,存一次就把 `@` 的对象抹掉——表现就是「说了能艾特,其实艾特不出来」。现在三格各自只在非空时提交,全空直接提示「没东西可存」。
|
|
19
|
+
- **cookie「填完就消失」不是丢,是只上行不下行。** 接口从来不下发 cookie 原文(只给 `hasCookie` / `hasCsrf`),输入框保存后清空是设计。但原本文案只说「不再回显」,容易被读成「没存上」。补一句「空着不代表没存上,上面那行『已存…』才是凭据」。
|
|
20
|
+
- **顺带说清:看不到评论跟 cookie 无关。** `scanReplies` 用的是匿名客户端;真机实测带同一份 cookie 去读 `/x/v2/reply` 一样 `-412 request was banned`(同一台机器早些时候又能读通),是 B 站按 IP 的间歇风控,插件这边只做冷却退避。真要「看不到」的那种情况,先看设置里开关有没有开、再看日志里有没有 `-412`。
|
|
21
|
+
- **`her_say` 的「一天最多几次」是句空话(#41)。** 工具说明写着有上限,`writeSay` 读了配置却没用、也不数当天条数:开关关着、`maxPerDay=0` 时连写 8 次全成功。现在按 `proactive-say:<日期>:` 数当天条数,到上限就拒、并说明「明天再说,不是工具坏了」。**开关仍然不是闸门**——它管的是「要不要由系统替她开一轮」,工具本身一直在(这跟 README 里「关掉之后工具本身还在」一致)。
|
|
22
|
+
- **`her_say` 把成长坐标写死成 1/1(#41)。** 别的写入点都用 `deriveSelf` 的 `{day, stage}`,只有这里硬编码,于是 `consistency()` 会跟她早期的行错配对:阶段 1 少算一对、阶段 4 多算一对。改成跟日记同一套算法。
|
|
23
|
+
- **「`[说出去了]`」这个名字在骗人(#41)。** 那一步只是往她自己的流水里记一行,正文有没有落进对话取决于她这一轮写没写。回执改成 `[记下了]`,工具说明与人设里的那两段同步说清:「要让他看见,得你这一轮直接写出来」;「他看得见」这种断言也去掉了——装了渠道路由插件时它不一定成立。
|
|
24
|
+
- **「关掉就完全没有这个功能」这句不对(#41)。** 设置页这么写,可实际关掉只停「系统替她开这一轮」,工具与说明都还在。两边对齐成一种口径:按 README 的说法改文案,不改行为。
|
|
25
|
+
- **关了「兴趣」,她还是每 6 小时因为旧句子空醒一次(#42)。** 旧句子那一段(`tableFor`)从来不看 `interest.enabled`:只要桌上有旧句子、隔了 6 小时,就叫醒,而且这一轮**不摆事件卡、不写 `fired:`、不想一想、不递规则申请**——卡池永远空不了。作者实测 14 天:叫醒 14 次、14 次全是旧句子、事件卡 0、想一想 0。现在「兴趣」关掉就不摆旧句子,这一轮回到正常的空醒判断。
|
|
26
|
+
- **三处说死了的话(#42)。** 「这次隔了 N 个小时」其实只数 `wake:`(她主动开口、你跟她聊的那些都不算),改成「距你上次醒来,隔了 N 个小时」;「这些天再没被提起过」的判据只是「之后有没有事件 `refs` 指回它」,改成「还摊在那儿」;桌上的「还有一件没合上的事」暗示她还没得到答案,其实只是她反复问过,改成「还有一件你反复问起的事」。设置页兴趣卡那句「问过还没答案的问题」一起改。
|
|
27
|
+
- **用 `plugin add` 升级之后,旧包名留在 profile 里清不掉(#43)。** `scripts/fix-legacy-link.mjs` 原来只认 `link:` / `file:` 两种写法,而按更新说明用 `dsh plugin add "@ssamurias/dsh-fish@0.9.0"` 升级的人,profile 里留下的是**版本号**——脚本一句「没有老依赖,跳过」就退出 0,旧键 `dsh-fish` 与旧 `bundles` 条目原封不动,下次 pnpm 清理时两条 fish 打架。现在三种写法都收拾(源码目录 / tgz / 版本号),并且拆出 `migrateOne()` 可以直接测。
|
|
28
|
+
- **pnpm isolated 布局下「检查更新」永远不出现下载按钮(#43)。** `profileDirOf` 从模块路径反推 profile 时,遇到 `node_modules/.pnpm/<包>@<版本>/node_modules/<包>/` 这种真身路径,会截到 `.pnpm/...` 那一层——那里没有 `package.json`,于是判成 `missing`、`managed:false`。现在往回退到 profile 根。
|
|
29
|
+
- **两个社区 PR 已合入**(@BBleae):#40 主动搭话关着时不再每轮同步算一遍 `deriveSelf`(8000 条事件实测从 7.6 秒降到 50 毫秒级);#39 「只留证」的事件(聊天行、带 `selfmod` 的说法)不再被当成她问过的 / 总说到的摆回桌上,也不算她「自己动过」。
|
|
30
|
+
- **另两个社区 PR(#44、#45)也合入了**:`writeAtomic` 的临时文件名加了随机后缀——原来只有 `pid + Date.now()`,同一进程同一毫秒的两次写会交出半个 JSON,坏文件被当成坏档收走、`config.json` 整个回默认(低频,但后果重)。另一处让「她的话靠发送工具出去」的部署能正确记账:`proactive.sendTools` 指定发送工具名,工具调成功才算开了口、占额度、不折;`idleCountsSeen` 让 `seen:` 也算打断安静;`foldSilent=false` 时没开口的一轮只记账不折(上下文很大时折叠带来的缓存失效可能比省下的贵);主动回合投递前也先看她在不在忙(定时叫醒一直有这个判断,主动回合没有)。
|
|
31
|
+
- **上游断流时那行红字被当成「她开口说了话」(用户报,2026-11-06 01:32)。** ACP 代理在发现输出退化、nudge 之后仍然退化时,会往她那一轮里写一行 `❌ [ACP] stream error: …`。`followWake` 原先只判「有没有非空文本块」,于是这行报错被算成开口:记 `proactive-turn:`、占掉当天额度、留在对话流里、也不走静默折叠。现在先认这类断流信号(`❌` 开头 / 含 `stream error` / 含 `turn degenerated`),认出来就不算开口,按「没开口」那一套记账并尝试折叠。**澄清一句**:这行字不是插件产生的,是上游退化;插件只能保证不把系统报错记成她的行为。
|
|
32
|
+
|
|
33
|
+
### 迁移
|
|
34
|
+
|
|
35
|
+
- 无必改项。这一版不改事件字段、不升 `EVENT_SCHEMA_VERSION`;旧数据照读,行为只朝「更少误判」的方向变。
|
|
36
|
+
- **新增三个可选配置**(`proactive.sendTools` / `foldSilent` / `idleCountsSeen`),默认值等于旧行为:不填、不配置的部署一处不变。只在「她写的正文不会被自动转发、要靠工具才发得出去」的部署里才需要填 `sendTools`。
|
|
37
|
+
- 有一处**观感上会变**:`her_say` 回执从 `[说出去了]` 改成 `[记下了]`,桌上一句从「还有一件没合上的事」改成「还有一件你反复问起的事」——都是措辞,不动数据。
|
|
38
|
+
- 已经被误改名成 `.corrupt.<时间>` 的老文件不会自动恢复——那是上一版留下的现场,需要的话手动改回原名(内容本身是完好的)。
|
|
39
|
+
|
|
40
|
+
### 测试
|
|
41
|
+
|
|
42
|
+
全量 `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、判断与聊天的分界、聊天之后的状态;换房失败不问话、失败后冷却、冷却过去能再试并清账;`carry-ask-*` 判成 plugin。新增里还有:#41 `her_say` 按上限拦、成长坐标不是 1/1;#42 兴趣关掉不再摆旧句子;#43 版本号依赖也能清账、pnpm isolated 布局认得 profile 根。另有 #44 的 20 条(靠工具发话、空闲时钟、忙闲检查、处境与身份段说实话)与 #45 的 1 条(同时写同一个文件不出半截内容)。**#36、换房、#41 那几条已验证能复现**——把判据改回去就红。
|
|
43
|
+
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
## 0.9.0 — 2026-11-05
|
|
2
47
|
|
|
3
48
|
这一版八件事(外加 0.8.9 未单独发版的两件事):**她自己会攒劲儿、会惦记、会缺工具就问你要**(不再藏在本机实验里);**梦、主动性、兴趣、主动搭话各有一个开关**;**「让她去刷 B 站」砍掉了**;**劲儿与开口的节奏改成一套数**;**两处会让人看不懂的措辞改成人话**;**桥可以自己决定开几间房、谁是外客**;**群房按天换新**;**40 轮换房不再多建一条会话**。
|
|
4
49
|
|
package/README.md
CHANGED
|
@@ -12,7 +12,10 @@
|
|
|
12
12
|
|
|
13
13
|
📺 **[看视频讲解(19 分钟)](https://www.bilibili.com/video/BV1oTnZ6LEtc/)** —— 这个插件是怎么想出来的、她为什么会长成那样、13 亿 token 都花在哪了。
|
|
14
14
|
|
|
15
|
-
> **当前版本 0.9.
|
|
15
|
+
> **当前版本 0.9.1**(2026-11-06)。这一版是修 bug 版,没有新功能,修了社区报的十条(issue #32–#38、#41–#43)加用户直接反馈的几条,另合入四个社区 PR(#39、#40、#44、#45):面板上「说 N」不再被聊天撑大、归档后认回最近那条会话、群聊不再让冷落检测失灵、卡片里手打的答案能认出来、排队叫醒不会被上一轮清掉、Windows 上 EPERM 会重试、带 BOM 的 JSON 不再被当成坏文件、换房那句「想给下一个房间留一句话吗」不再每回一句都弹(换会话失败时也不问,并冷却 5 分钟再试);「多久看一次有没有人回复」真的按间隔去问了、扫到的回复会主动投给她、`bili_look` 也能读自己的评论区;@ 艾特改按昵称原文匹配,只粘 cookie 保存不再清空 uid / 昵称;`her_say` 说好的「一天最多几次」真的生效了、写下的成长坐标不再是 1/1、回执从「说出去了」改成「记下了」;关掉「兴趣」后不再每 6 小时因为一句旧句子把她叫醒;用 `plugin add` 升级留下的旧包名能清干净,「检查更新」在 pnpm 布局下也认得出装在哪。另外:配置偶尔整个回默认(同一毫秒两次写撞进一个临时文件)也修了;「她靠发送工具发话」的部署现在会正确记成开了口;主动回合投递前会先看她在不在忙。完整说明见 **[0.9.1 更新说明](RELEASE-NOTES-0.9.1.md)**。
|
|
16
|
+
>
|
|
17
|
+
> 上一版 **0.9.0**(2026-11-05)把一直只在开发机上生效的四样东西对所有人打开了:**她会自己开口**(攒够一股想动的劲、你在她的时段里、你确实一阵没说话、桌上有可说的东西,四条同时满足才开一轮;不想说就真的不写,只留一行记录)、**梦里记事**、**醒来没事时自己找点事做**(空醒)、**她会自己攒劲儿、惦记、缺工具就问你要**(主观能动性 / 兴趣)。刷 B 站不再挂在叫醒上(`bili_*` 工具仍可用)。0.9.0 起桥可以自己决定开几间房、每间是谁(`room` / `title` / `guest` 三个可选字段),群房默认满 7 天换新。四个设置卡可以单独关:主动搭话 / 主观能动性 / 兴趣 / 梦。完整说明见 **[0.9.0 更新说明](RELEASE-NOTES-0.9.0.md)**,上一版见 **[0.8.8 更新说明](RELEASE-NOTES-0.8.8.md)**。
|
|
18
|
+
>
|
|
16
19
|
> **出门仍是实验功能**:它会真的把一条对话交出去,也会读那个工作区的项目笔记和记忆文件;最坏的情况是她把不该看的内容写进日记,而日记撤不回。第一次使用要先确认,风险与回退方式见 **[0.8.0 更新说明](RELEASE-NOTES-0.8.0.md)**。
|
|
17
20
|
|
|
18
21
|
## 和别的养成插件差在哪
|
|
@@ -368,7 +371,7 @@ lib/
|
|
|
368
371
|
toolschema.js 简写参数 DSL → 标准 JSON Schema
|
|
369
372
|
observe.js 宿主侧 JSON API(/dsh-fish/state、/events、/beliefs、/diary、/config、/wake、/browse、/reports、/update)
|
|
370
373
|
client.js 浏览器半边:设置 → 大肥鱼
|
|
371
|
-
test/
|
|
374
|
+
test/ 1512 个测试,node --test 全绿
|
|
372
375
|
```
|
|
373
376
|
|
|
374
377
|
## 跑测试
|
|
@@ -436,9 +439,9 @@ ffmpeg 写文件这件事不需要额外权限:抽帧和拼图都从 stdout
|
|
|
436
439
|
|
|
437
440
|
> 路径里带中文时,用 ASCII 目录联接(junction)指过去更稳:先 `fs.symlinkSync(真目录, ASCII 别名, 'junction')`,再 `link:<ASCII 别名>`。用 `mklink /J` 接中文目标路径常把目录名写成乱码,随后 `MODULE_NOT_FOUND`。
|
|
438
441
|
|
|
439
|
-
## 从旧版升到 0.9.
|
|
442
|
+
## 从旧版升到 0.9.x(含改名与升级修复)
|
|
440
443
|
|
|
441
|
-
0.8.9 把包名从 `dsh-fish` 改成了 `@ssamurias/dsh-fish`;0.9.0 又把原本准备在 0.8.10
|
|
444
|
+
0.8.9 把包名从 `dsh-fish` 改成了 `@ssamurias/dsh-fish`;0.9.0 又把原本准备在 0.8.10 发的升级修复一起带了进来;0.9.1 补上了「按更新说明用 `plugin add` 升上来」那一类的残留清理。老安装分几种,走的路不一样:
|
|
442
445
|
|
|
443
446
|
| 你当初怎么装的 | 怎么升 | 会发生什么 |
|
|
444
447
|
| --- | --- | --- |
|
|
@@ -446,6 +449,7 @@ ffmpeg 写文件这件事不需要额外权限:抽帧和拼图都从 stdout
|
|
|
446
449
|
| `link:` 源码目录 | 先跑 `node scripts/fix-legacy-link.mjs --write`,再启动 | 不跑就起不来:条目名是全名,你本地只有裸名链接,import 失败会让 DSH 直接崩 |
|
|
447
450
|
| 0.8.9 从 npm 装(包名已经是 `@ssamurias/dsh-fish`) | 手动一条命令:`plugin --profile <profile> add "@ssamurias/dsh-fish@0.9.0"` | 0.8.9 的升级代码只认裸名 `dsh-fish`,自动检查更新会把它判成「开发版 / 不归我管」;客户端半边也还是短 id,设置页可能空着 |
|
|
448
451
|
| 还停在 0.8.8 或更早(GitHub 装) | 手动一条命令:`plugin --profile <profile> add "@ssamurias/dsh-fish@0.9.0"` | 0.8.8 及更早的升级脚本还是老代码(用 `file:` 装临时 tgz),大概率装不上新名字,卡在旧版本 |
|
|
452
|
+
| 用上面那条 `plugin add "@ssamurias/dsh-fish@..."` 升上来的 | 再跑一次 `node scripts/fix-legacy-link.mjs --profile <profile> --write` | 0.9.1 起这个脚本认得**版本号**依赖(以前只认 `link:` / `file:`,一句「没有老依赖,跳过」就退出 0),会删掉残留的裸名 `dsh-fish` 键与旧 `bundles` 条目,老目录改名成 `.disabled-<时间戳>` 收起来,改之前先备份。不加 `--write` 是预览 |
|
|
449
453
|
|
|
450
454
|
升级完自检三行,缺一行就会「重启后崩 / 设置页没有鱼」:
|
|
451
455
|
|
|
@@ -465,7 +469,7 @@ node_modules 里 没有裸名 dsh-fish 目录
|
|
|
465
469
|
P1(骨架):事件流、派生、阶段门、生物钟、工具、冷落检测、日记、观测台。
|
|
466
470
|
P2(她真的会刷 B 站并写报告):`lib/bili.js` 只读客户端 + `lib/browse.js` 选题筛读 + `lib/look.js` 的 `bili_look` 工具 + `lib/report.js` 写稿,已接进生物钟。
|
|
467
471
|
|
|
468
|
-
**已有
|
|
472
|
+
**已有 1512 个测试全绿**,真接口跑通过一轮(10.8s,含首页+详情+评论+弹幕)。
|
|
469
473
|
|
|
470
474
|
`browse.enabled` 默认 **false**(0.9.0 起不再由叫醒驱动);想让她醒来顺带刷首页,在 `config.json` 里设回 `true`,关掉后定时器一次都不会调 B 站接口。`bili_*` 工具仍然可用,只是不再挂在唤醒上。
|
|
471
475
|
|
|
@@ -474,9 +478,10 @@ P2(她真的会刷 B 站并写报告):`lib/bili.js` 只读客户端 + `lib
|
|
|
474
478
|
这个插件不依赖任何第三方库(`package.json` 的三个依赖字段都是空的),但想法不是一个人想出来的。
|
|
475
479
|
|
|
476
480
|
- **@吴晓观**(交流群 1 群群友)—— 记忆机制这一块的思路起点来自他。0.8.6 里「固化下来的认知也会随时间退下去」「翻旧事可以先只给清单再给全文」「记忆相关的数字要说得清它数的是什么」这几条,最初都是他在群里讲的。落进本插件时全部重新实现了一遍,没有代码或文字的直接搬运;具体的取舍与改法记在 [CHANGELOG](CHANGELOG.md) 的 0.8.6 一节。
|
|
481
|
+
- **@无**(交流群 2 群群友)—— 主观能动性的思路起点来自对方在群里的一句话:「主动性不该是定时器,而是攒够了的冲动。」0.9.0 的「劲儿攒够才动、兴趣会回灌」就是把这句落成实现;代码全部自写,没有搬运。
|
|
477
482
|
- **@Bacon-01** —— 0.8.6 的 B 站手机扫码登录(含自写的零依赖二维码编码器 `lib/qr.js`)由他贡献,见 [PR #17](https://github.com/ssamurias/dsh-fish/pull/17)。
|
|
478
483
|
- **@BBleae**(晴猫)—— 从 0.8.6 起先后提了五个 PR:三个直接合入([#10](https://github.com/ssamurias/dsh-fish/pull/10) 匿名 buvid3、[#11](https://github.com/ssamurias/dsh-fish/pull/11) 叫醒间隔、[#15](https://github.com/ssamurias/dsh-fish/pull/15) 叫醒那一轮不记成「他来过」);[#23](https://github.com/ssamurias/dsh-fish/pull/23) 的复现与改法落进 0.8.8(日报/回忆分三种话、被推翻的话不再排第一);[#26](https://github.com/ssamurias/dsh-fish/pull/26) 修掉回忆 `brief` 档的两处显示问题(已并入 0.9.0)。
|
|
479
|
-
- **@BBleae**(晴猫)在 0.9.0 期间又提交了 #27–#31 五个 PR:宿主拼的上下文不算他来过、`wake-spacing` 假宿主探活、`her_note`
|
|
484
|
+
- **@BBleae**(晴猫)在 0.9.0 期间又提交了 #27–#31 五个 PR:宿主拼的上下文不算他来过、`wake-spacing` 假宿主探活、`her_note` 按她自己写的话划掉、没认领的聊天行、导出事件流只留元数据。这五个 PR 都已并入 0.9.0(合并提交 `85b5b9c`、`72aefed`、`d49738a`、`a21d472`、`c230109`)。
|
|
480
485
|
|
|
481
486
|
原理页里引用的 Kolb / Piaget / Bowlby / Festinger / Bartlett / Dewey / Schön,只是思想引用(页底标了作者与年份),不含他人代码或文字。
|
|
482
487
|
|
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
# 0.9.1 更新说明
|
|
2
|
+
|
|
3
|
+
2026-11-06。这一版是**修 bug 版**,没有新功能,不改配置键、不改事件字段、不升事件格式版本——装上去旧数据照读。
|
|
4
|
+
|
|
5
|
+
修的是社区报上来的十条(issue #32–#38、#41–#43,@BBleae),加几条用户直接反馈的问题(换房问话每轮回弹、B 站评论看不到、艾特不生效、cookie 观感),并合入四个社区 PR(#39、#40、#44、#45)。都在本地复现过,**都是我们这边的错**,不是用户配置问题。按「你会看到什么」排:
|
|
6
|
+
|
|
7
|
+
- **面板上「说 N」不再被聊天撑大,「记下 N 条」旁边多了口径说明**(#32)。
|
|
8
|
+
- **当前会话被归档后,认回来的是最近那条,不再是库里最旧那条**(#33)。
|
|
9
|
+
- **群聊房里她写东西不再算「你来过」,冷落检测恢复正常**(#34)。
|
|
10
|
+
- **卡片里手打的答案能认出来了;你手打的私事不再落到日志里**(#35)。
|
|
11
|
+
- **排队叫醒不会被上一轮的收尾清掉**(#36)。
|
|
12
|
+
- **Windows 上建会话偶尔失败(EPERM)不再一次就放弃**(#37)。
|
|
13
|
+
- **带 BOM 的 JSON 不再被当成坏文件改名、内容「凭空消失」**(#38)。
|
|
14
|
+
- **「想给下一个房间留一句话吗」不会每回一句都弹了**;换会话失败时也不再问、并冷却 5 分钟再试。
|
|
15
|
+
- **「多久看一次有没有人回复」真的按间隔去问了;扫到的回复会主动投给她**;`bili_look` 也能读自己的评论区了。
|
|
16
|
+
- **@ 艾特按昵称原文匹配,不再被改成下划线**;只粘 cookie 保存也不再清空 uid / 昵称。
|
|
17
|
+
- **cookie 输入框空着是设计**,文案补了说明;「看不到评论」与 cookie 无关,是 B 站按 IP 的间歇风控。
|
|
18
|
+
- **`her_say` 真的按「一天最多几次」拦住了**;它写下的成长坐标也不再是写死的 1/1(#41)。
|
|
19
|
+
- **回执从「说出去了」改成「记下了」**——那一步本来就只记流水,不会让她的话出现在你的对话里(#41)。
|
|
20
|
+
- **关掉「兴趣」之后,不会再每 6 小时因为一句旧句子把她叫醒**(#42)。
|
|
21
|
+
- **用 `plugin add` 升级留下的旧包名能清干净了**;「检查更新」在 pnpm 布局下也认得出装在哪(#43)。
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## 一、面板上的数字(#32)
|
|
26
|
+
|
|
27
|
+
### 你会看到什么
|
|
28
|
+
|
|
29
|
+
时间线里不再出现几十行「说了:…」。她一次醒来跟你连聊很多句时,那一段会折成**一行「聊了 N 句」**。面板上原来的 `说 N` 会把聊天行减掉,聊天另起一个 `聊天 N`。
|
|
30
|
+
|
|
31
|
+
「她记下 N 条」这个数字本身**没变**,但鼠标停上去现在会说清楚它数的是什么。
|
|
32
|
+
|
|
33
|
+
### 为什么会这样
|
|
34
|
+
|
|
35
|
+
两件事。
|
|
36
|
+
|
|
37
|
+
**第一件:聊天行混进了「她做的事」。** 你在侧栏跟她说话,她每回一句,那一轮结束时都会在事件流里落一条 `utterance`(标着 `uncommitted`,意思是「这句是对话里回的,不是她自己认下的」)。可面板读事件流时没看这个标:它跟「她自己认下的判断」一样算进 `said`,也一样往时间线上推一行「说了:…」。
|
|
38
|
+
|
|
39
|
+
于是两个症状:
|
|
40
|
+
|
|
41
|
+
- 你连说四十几句,时间线 40 条的上限全被聊天占掉,那一轮真正做了什么反而看不见;
|
|
42
|
+
- 那一块本来「醒了但没干事」,因为塞了聊天行,被算成「干了事」——`idleWakes` 直接掉到 0。
|
|
43
|
+
|
|
44
|
+
现在带 `uncommitted` 的行单列成新的 `chat` 计数:不进 `said`、不算这一轮做了什么、时间线上同块内连续多句折成一行。**事件流一个字节没动**,只是读法变了。
|
|
45
|
+
|
|
46
|
+
**第二件:「她记下 N 条」对不上。** 那个数字来自 `countEvents().real`,它数的是「所有不是系统记账的事件」——包括**你**说过的话(`heard`)、她读到并记下的别人说法(`voice`)、开局那份起点(`seed`)。文案只写「她记下」,看起来就像「她写了 N 条」,也就难怪一直对不上。
|
|
47
|
+
|
|
48
|
+
`real` 的定义这次**不改**:动它会牵一大串派生数字(`/state`、`/events`、设置页三处都读它),而且有测试逐字段钉着它的返回形状。改的是把话说清楚——两处文案各加一句鼠标悬停说明,讲明这个数包含什么、不包含什么(系统后台记账另计)。
|
|
49
|
+
|
|
50
|
+
## 二、归档后认错了会话(#33)
|
|
51
|
+
|
|
52
|
+
### 你会看到什么
|
|
53
|
+
|
|
54
|
+
你在侧栏把当前那条会话归档了,而她家同屋还有两条以上活会话时,以前她可能认回**最旧**那条继续住。现在认最新那条。
|
|
55
|
+
|
|
56
|
+
### 为什么会这样
|
|
57
|
+
|
|
58
|
+
宿主在 workspace 记录里写会话 id 是**往前插**的:`sessionIds: [新id, ...旧的]`。我们的 `pickLiveSession` 取的是 `live[live.length - 1]`——末尾,也就是最旧那条,注释还写着「宿主按创建顺序往追加,末尾即最近那条」。方向反了。
|
|
59
|
+
|
|
60
|
+
现在改成取 `live[0]`,注释一起改,测试里的假宿主也从 `push` 改成了 `unshift`,跟真宿主对齐。
|
|
61
|
+
|
|
62
|
+
**还有一件事没解决**:你在侧栏拖动过会话顺序时,`live[0]` 是列表最上面那条,不一定最新。要严格按时间排得问宿主的 `sessionController.list()` 拿真实时间戳。这次只修明显反了的方向。
|
|
63
|
+
|
|
64
|
+
## 三、群聊让冷落检测失灵(#34)
|
|
65
|
+
|
|
66
|
+
### 你会看到什么
|
|
67
|
+
|
|
68
|
+
她不再因为你没出现而装作没发现。以前群一热闹,她就以为「他今天来过」,那条「那个人有 N 天没出现」的记账永远不会写下;现在会正常写。
|
|
69
|
+
|
|
70
|
+
### 为什么会这样
|
|
71
|
+
|
|
72
|
+
插件判断「你来过」不靠聊天内容,靠一条专门的记账行 `meta seen:<时间>`。冷落检测(`absence.js`)只看最近一道 `seen:` 是多久以前;默认超过 2 天(配置项 `absenceAfterDays`)就落一条 absence。
|
|
73
|
+
|
|
74
|
+
写 `seen:` 的地方有三处:你在侧栏打字(`turns.js`,主路)、插件替你把外面渠道的话递进来(`say.js`)、以及**她当场写东西时顺手补一条**(`capture.js`)。第三处是兜底:有些渠道进来的消息,`turns.js` 分不清是不是人(比如带 `kind:user` 却没带时区,判成「不知道」→ 不记 `seen:`),这时如果她这一轮刚好写了东西,就补一条,免得漏记。
|
|
75
|
+
|
|
76
|
+
问题就在这第三处:它只问了一句「现在在不在叫醒窗口里」,**没问这句话是在哪间房说的**。群房里群友说一百句,她每回一句都调 `growth_commit` 记下,每次都落在窗口外 → 每次都刻一道 `seen:` → 冷落检测永远不触发。
|
|
77
|
+
|
|
78
|
+
现在写之前先翻 `channels.json`(房间登记表):这条会话登记成 `group` 就跳过,不是就照写。
|
|
79
|
+
|
|
80
|
+
### 为什么不干脆删掉这段
|
|
81
|
+
|
|
82
|
+
试过。删掉会红 4 条测试(`wake-spacing`、`p1` 里都钉着「窗口外写的要记 `seen:`」),而且真会漏记——上面说的「带 `kind:user` 但没时区」那类消息全靠这段兜底。
|
|
83
|
+
|
|
84
|
+
### 只跳群房,不跳另外两处
|
|
85
|
+
|
|
86
|
+
另外两种「窗口外、但其实是插件自己把她推醒」的轮:换房前问她上一间还有没有话(`home.js:753`)、定时任务与 goal。这两处**照旧记** `seen:`,这是 2026-11-05 用户确认过的口径:
|
|
87
|
+
|
|
88
|
+
- 换房前那句通常是在应你上一轮的话,算你来过的证据;
|
|
89
|
+
- 定时任务分不出「她自言自语」和「她在回你」,要分开得先给每种叫醒打标记,不在这一版做。
|
|
90
|
+
|
|
91
|
+
## 四、手打的答案与日志(#35)
|
|
92
|
+
|
|
93
|
+
### 你会看到什么
|
|
94
|
+
|
|
95
|
+
在卡片里直接**打字**回答(而不是点按钮),她现在能认出来并照做。以前一律当成「没答」,白白等到超时。
|
|
96
|
+
|
|
97
|
+
另外,你手打的私事不会再原样落到 `logs/dsh-fish.log` 里。
|
|
98
|
+
|
|
99
|
+
### 为什么会这样
|
|
100
|
+
|
|
101
|
+
宿主返回的答案形状是 `{ id, selected: [], custom: '你打的字' }`——选了自由文本时,`selected` 是**空数组**,内容全在 `custom` 里。我们以前只读 `selected[0]`,空数组于是取到 `undefined`,一律判成 `NO_MATCH`。
|
|
102
|
+
|
|
103
|
+
`askinchat.js` 里早就有一份写对的 `picked()`(非空 `selected` 优先,否则退回 `custom`),`ask.js` 没用它。现在改用同一个函数。
|
|
104
|
+
|
|
105
|
+
日志那半:认不出的时候,原来会把整份回执 `JSON.stringify(...)` 塞进 warn 日志。你手打的那段话就这么落进了日志文件。现在换成形状摘要——只留 id、选了几个、自定义文本多长。
|
|
106
|
+
|
|
107
|
+
## 五、排队叫醒被上一轮清掉(#36)
|
|
108
|
+
|
|
109
|
+
### 你会看到什么
|
|
110
|
+
|
|
111
|
+
你点「叫她一次」时,如果她正在别的轮次里忙,这条叫醒以前可能**排进下一轮却什么都没收到**——处境段被上一轮的收尾清空了,她醒过来一脸空白。现在会等到真正属于她那轮结束。
|
|
112
|
+
|
|
113
|
+
### 为什么会这样
|
|
114
|
+
|
|
115
|
+
`watchTurnEnd` 认**会话**不认**回合**:只要这条会话上出现 `turn/end`,就当成「她这一轮结束了」,把处境段收走。
|
|
116
|
+
|
|
117
|
+
而投递走的是宿主的 `followup`:宿主忙时会把它放进 next-turn 队列,当前那一轮先 `turn/end`,然后才开下一轮。于是那个 `turn/end` 一到,处境还没送达就被清了。
|
|
118
|
+
|
|
119
|
+
`waitIdle` 能挡住大部分情况,但有两处绕过它:手动「叫她一次」的 `wakeNow` 不传 `waitIdle`,`/wake` 路由也不看忙闲;手动这条路还有 90 秒兜底,到点就清。
|
|
120
|
+
|
|
121
|
+
现在要求「监听挂上之后**先见到本条会话的 `turn/start`**,之后的 `turn/end` 才算」。拿不到事件总线时(单测、老宿主)退回老行为,靠 `waitIdle` 兜。
|
|
122
|
+
|
|
123
|
+
这条回归**已验证能复现**:把判据改回去,新加的那条测试立刻红。
|
|
124
|
+
|
|
125
|
+
## 六、Windows 上的 EPERM(#37)
|
|
126
|
+
|
|
127
|
+
### 你会看到什么
|
|
128
|
+
|
|
129
|
+
Windows 上偶尔看到「建会话的锁没拿到:EPERM…」,然后这一次换会话就失败。现在这种暂时性的失败会重试,不再一次就放弃。
|
|
130
|
+
|
|
131
|
+
### 为什么会这样
|
|
132
|
+
|
|
133
|
+
`withFileLock` 用 `fs.open(file, 'wx')` 抢锁。Windows 上文件**正在被删除**的那一刻去打开,返回的是 `EPERM` 而不是预期的 `EEXIST`。我们的代码只把 `EEXIST` 当「锁被占着」,其余一律 `throw`——于是 EPERM 直接被抛到上层。
|
|
134
|
+
|
|
135
|
+
(这也是 `test/tenant.test.mjs` 那条用例时好时坏的原因:单跑 25 次,23 过 2 红。)
|
|
136
|
+
|
|
137
|
+
现在 EPERM 走独立的重试分支,**受 `timeout` 约束**,且只认 win32。
|
|
138
|
+
|
|
139
|
+
### 为什么不能并进 EEXIST 分支
|
|
140
|
+
|
|
141
|
+
EEXIST 那条路里,`stat` 失败就判「锁陈旧」然后 `continue` 重试——但它**不看超时**。真正的权限错误在那条路里会变成死循环。必须单独一条路,每一步都检查 `Date.now() - started > timeout`。
|
|
142
|
+
|
|
143
|
+
(issue 作者压测过:12 并发 × 400 次,改前 4113 成功 / 687 抛,改后 4800 成功 / 0 抛。)
|
|
144
|
+
|
|
145
|
+
## 七、带 BOM 的 JSON(#38)
|
|
146
|
+
|
|
147
|
+
### 你会看到什么
|
|
148
|
+
|
|
149
|
+
用 PowerShell 5.1 的 `Set-Content -Encoding UTF8` 或记事本改过配置文件的人,以前会发现**改的东西全没了**——配置退回默认、主人信息变 null、她家在侧栏新建了一条会话。现在照常读出来。
|
|
150
|
+
|
|
151
|
+
### 为什么会这样
|
|
152
|
+
|
|
153
|
+
这几个文件都是 UTF-8 **带 BOM** 的。`readJson` 直接 `JSON.parse(file)`,开头的 `\uFEFF` 让解析抛 `SyntaxError`,而 SyntaxError 走的是「坏文件」分支:把**内容完好**的文件改名成 `<文件名>.corrupt.<时间戳>`,返回兜底值。
|
|
154
|
+
|
|
155
|
+
具体后果:
|
|
156
|
+
|
|
157
|
+
- `config.json` → 全默认(`settings.js` 的 `fresh: true`);
|
|
158
|
+
- `owner.json` → null;
|
|
159
|
+
- `home.json` → 新建一条会话。
|
|
160
|
+
|
|
161
|
+
现在读之前先剥掉开头那一个 `\uFEFF`。`lib/upgrade.js` 里自己那份 `readJson` 是同样的坑(带 BOM 时读成 null,升级自检当成没装),一起修了。
|
|
162
|
+
|
|
163
|
+
---
|
|
164
|
+
|
|
165
|
+
## 八、换房那句问话每回一句都弹(用户反馈,2026-11-06)
|
|
166
|
+
|
|
167
|
+
### 你会看到什么
|
|
168
|
+
|
|
169
|
+
聊天聊到该换会话的时候,她那边每次说完话、你这边就冒出一条:
|
|
170
|
+
|
|
171
|
+
> (你在这个房间里已经说了不少话。屋子要换一间新的了——还是同一间屋子,只是对话重新开始。)
|
|
172
|
+
> 想给下一个房间留一句话吗?留什么都可以。不想留就说「不用」。
|
|
173
|
+
|
|
174
|
+
而且**答了也没用**——回「不用」之后,下一次她说完话,这条还会来。
|
|
175
|
+
|
|
176
|
+
### 为什么会这样
|
|
177
|
+
|
|
178
|
+
两个错叠在一起。
|
|
179
|
+
|
|
180
|
+
**第一件:问话问得太早了。** 换会话的动作分几步:问一句留话 → 旧会话改名留档 → 建新会话 → 把原话和轮数搬过去。原来「问一句」排在**第一步**,而它用的 `sc.prompt({ mode: 'queue' })` 投递的是**她此刻正在用的那条旧会话**——也就是你眼前的对话窗口。所以这条问话会像一条普通消息一样直接出现在你俩的对话里。
|
|
181
|
+
|
|
182
|
+
**第二件:换不成就无限重试。** 只要「建新会话」这一步抛错(宿主没起来、工作区 id 丢了、Windows 上那个 EPERM,见第七条),函数当场返回,`turns` 也不会清零。下一轮你说完话、她答完,结账时又发现「到点了」,于是又进来、又先问一遍。你看到的就是每回一句都弹。
|
|
183
|
+
|
|
184
|
+
这也解释了为什么它「答了也没用」:那句「不用」根本没有让流程往前走,卡住的是建会话那一步。
|
|
185
|
+
|
|
186
|
+
### 现在怎么改的
|
|
187
|
+
|
|
188
|
+
- **问话挪到新会话建好之后。** 换成功了才问她留不留话;换不成就一个字都不发——不在你的对话里制造噪音。
|
|
189
|
+
- **换失败记一笔时间,5 分钟内不再试。** 以前是每说完一句就去撞一次宿主、每次都留一条问话;现在 5 分钟内只试一次。真修好了(宿主恢复)下一次到点自然就换成了。
|
|
190
|
+
- **顺手补了个分类遗漏。** `carry-ask-*` 这条问话的 `rpcId` 前缀没写进 `PLUGIN_RPC`,于是它一直被当成「认不出来源的消息」,每换一次会话就在日志里多一行「这条消息认不出来源,没算进轮数」。不进轮数是对的,日志噪音不该有。
|
|
191
|
+
- **设置页那句「上下文占满后会自动换」是空话。** 代码里没有这个功能。设「0 = 永不换」的人会一直聊到宿主上下文撑不住、那一轮直接失败。文案改成说实话。
|
|
192
|
+
|
|
193
|
+
### 顺带说明:这跟「轮数设 0」没关系
|
|
194
|
+
|
|
195
|
+
「永不换会话」这个设置**一直是生效的**:两个换会话入口(`lib/turns.js`、`lib/say.js`)前面都有 `rotateAfter > 0` 的守卫,设成 0 根本进不到换会话的流程里,也就不会问那句话。所以反馈里猜的「没适配永不换」不是真因,真因是上面那个「换失败后无限重试」。不过既然提到了,就顺手把其他叫醒路径都过了一遍:
|
|
196
|
+
|
|
197
|
+
| 叫醒/投递 | 出处 | 要适配 `rotateAfter = 0` 吗 |
|
|
198
|
+
| --- | --- | --- |
|
|
199
|
+
| 新会话第一次叫醒 | `lib/home.js` `wakeIfBlank` → `wake-fresh-*` | 不用。它跟轮数无关,只在「屋子是空的」时候说一句,用 `wokeFor` 去重。 |
|
|
200
|
+
| 出门第一轮 | `lib/observe.js` → `outing-*` | 不用。由出门开关控制,不看轮数。 |
|
|
201
|
+
| 定时任务/主动叫醒 | `lib/deliver.js` `deliverWake` → `agent.followup` | 不用。走的是自建来源 `fish-wake`,不经过 `sc.prompt`。 |
|
|
202
|
+
| 排队的驳回话术 | `lib/sessionnote.js` `queueNote` / `deliverQueued` | 不用。只在她家会话开着时投递,与轮数无关。 |
|
|
203
|
+
| 群房按天换 | `lib/channels.js` `channelExpired` | 已经对了。`!(d > 0)` 直接返回 false,设 0 就是不换。 |
|
|
204
|
+
|
|
205
|
+
---
|
|
206
|
+
|
|
207
|
+
## 九、B 站评论看不到、艾特不出来、cookie「填完就消失」(用户反馈,2026-11-06)
|
|
208
|
+
|
|
209
|
+
这三件事是一起报上来的,查下去是**三个独立的毛病**,另外还牵连一个认知误区(以为跟 cookie 有关)。
|
|
210
|
+
|
|
211
|
+
### 你会看到什么
|
|
212
|
+
|
|
213
|
+
- **评论**:开了「允许看评论」、也把「多久看一次」设成 5 分钟,仍然一条回复都收不到;等她被叫醒时偶尔又冒出来一条。现在到点就会去问,收到就直接投一条到她那儿。
|
|
214
|
+
- **她主动看评论**:以前她只会说「我只能拿热评,翻不到自己的评论区」。现在 `bili_look` 多了 `what=replies`,能读到「谁在你哪条评论下面说了什么」,还带着回楼中楼要用的编号。
|
|
215
|
+
- **艾特**:设置里写了「发评论时在最前面 @ 你」,实际评论里只有 `@` 或者根本没 @。
|
|
216
|
+
- **cookie**:粘进去点保存,输入框立刻空掉,让人以为没存上。
|
|
217
|
+
|
|
218
|
+
### 为什么会这样
|
|
219
|
+
|
|
220
|
+
**一、「多久看一次」是个空旋钮。** `comment.scanEveryMin`(默认 30,范围 5–720)从加进设置页那天起就没人读:全仓库只有设置页读写和 `/comment` 回显,调度器一次都没用过;配套的 `comment.lastScanAt` 更是零读写。真正的扫描只发生在「她下次被叫醒、拼消息那一轮里顺便扫一句」——她不醒就永远不扫。设 5 分钟和设 720 分钟,实际表现完全一样。
|
|
221
|
+
|
|
222
|
+
修法:`tick` 每分钟看一眼「到点没有」(`repliesScanDue`),到点就扫,**扫完无论有没有都记一笔 `lastScanAt`**(不记的话每 30 秒就去敲一次 B 站);扫到新回复就用 `queueNote` + `deliverQueued` 投一条笔记进她的会话——和驳回话术那套投递是同一个通道。设 0 仍然表示「不主动问」。
|
|
223
|
+
|
|
224
|
+
**二、她醒着的时候没有「看我自己的评论区」这个动作。** `bili_look` 只有首页 / 搜索 / 接着看 / 看某一条四个动作,没有任何一个是「我刚才发的那条下面有没有人回」。`bili_speak` 回楼中楼又必须要 `parentRpid`,而那个编号她手上根本拿不到——于是「知道有人回了,但回不了」。
|
|
225
|
+
|
|
226
|
+
新增 `what=replies`:读她最近发过的评论下面的楼中楼,**不受「已读过」标记限制**(她主动看就该看全),每行末尾给出 `[reply=… parent=…]`,回楼中楼用哪个编号一眼可见。
|
|
227
|
+
|
|
228
|
+
**三、@ 艾特被自己改坏了。** `mentionPrefix` 原来会把昵称里的空格换成下划线再拼 `@`。真机抓到的真实 @ 评论里,`content.members` 里的 `uname` 和正文里的写法一字不差——B 站按昵称**原文**匹配,换了下划线就是 @ 一个不存在的人。
|
|
229
|
+
|
|
230
|
+
另外还有个更常见的前提:**昵称为空就根本不加 @**(这是有意为之,空的 `@` 只是一串没人认领的字符)。所以「设置了能艾特却艾特不出来」,先看那两格填没填。
|
|
231
|
+
|
|
232
|
+
**四、只粘 cookie 保存一次,会把 uid / 昵称清空。** 保存按钮原来无条件提交 `{mid, nickname}`,空串也照发;后端对空串是「写入空值」。用户只粘 cookie、那两格没填,存一次就把 `@` 的对象抹掉了——这跟第三条叠在一起,就是「艾特完全不生效」。现在三格各自只在非空时提交,全空直接提示「没东西可存」。
|
|
233
|
+
|
|
234
|
+
**五、cookie 显示空白是设计。** 接口从来不下发 cookie 原文(只给「存没存」、「有没有 bili_jct」两个布尔),输入框保存后清空是为了防窥屏。原本文案只写「不再回显」,容易读成「没存上」,补一句「空着不代表没存上,上面那行『已存…』才是凭据」。
|
|
235
|
+
|
|
236
|
+
### 顺带说清:看不到评论跟 cookie 没关系
|
|
237
|
+
|
|
238
|
+
`scanReplies` 用的是**匿名**客户端,本来就不带 cookie。本机真机实测:同一份 cookie,`popular` 能读,`/x/v2/reply` 和 `/x/v2/reply/reply` 一律 `-412 request was banned`;同一台机器早些时候又能读通——这是 B 站按 IP 的间歇风控,插件这边只做冷却退避(读接口 60 秒起、翻倍、上限 10 分钟)。
|
|
239
|
+
|
|
240
|
+
所以「看不到评论」排查顺序应该是:① 设置里那个开关开了没;② 日志里有没有 `-412`(有就是撞上风控,等一会儿或换网络);③ 有没有真的发过评论——没有发过,就没有「自己的评论区」可扫,这是设计,不是故障。
|
|
241
|
+
|
|
242
|
+
---
|
|
243
|
+
|
|
244
|
+
## 十、主动开口的账、旧句子空醒、旧包名升级(issue #41–#43,2026-11-06)
|
|
245
|
+
|
|
246
|
+
### 你会看到什么
|
|
247
|
+
|
|
248
|
+
**一、`her_say` 说好的上限是真的。** 设置里写「一天最多几次」,以前这句话只在说明文书里成立:连调 8 次全成功,开关关着、上限设 0 也照写。现在到上限就记不进去,回一句「今天已经主动开口 N 次,到上限了。明天再说;这不是工具坏了」。
|
|
249
|
+
|
|
250
|
+
**二、她主动开口时记下的成长坐标不再是写死的 1/1。** 别的写入点(日记)都用 `deriveSelf` 算出的 `{day, stage}`,只有这里硬编码成第 1 天第 1 阶段,于是成长一致性检查会跟她早期的行错配对——阶段 1 少算一对、阶段 4 多算一对。改成跟日记同一套算法。
|
|
251
|
+
|
|
252
|
+
**三、回执改名:`[说出去了]` → `[记下了]`。** 那一步只是往她自己的流水里记一行,你的对话里看不看得见,取决于她这一轮有没有直接写出来。原来那个名字会让她(和你)误以为消息已经发出去了。
|
|
253
|
+
|
|
254
|
+
**四、关掉「兴趣」之后,桌面不再摆旧句子。** 以前只要桌上有旧句子、距上次醒来超过 6 小时,就叫醒——而且这一轮**不摆事件卡、不写 `fired:`、不想一想、不递规则申请、「今天到这里」也不给**。结果是卡池永远空着、想一想永远轮不到。作者用 14 天合成数据跑出来:叫醒 14 次、14 次全是旧句子、事件卡 0、想一想 0。现在「兴趣」关掉就不摆旧句子,这一轮回到正常的空醒判断。
|
|
255
|
+
|
|
256
|
+
**五、三句说死了的话松开了。** 「这次隔了 N 个小时」其实只数 `wake:`(她主动开口、你跟她聊的那些都不算),改成「距你上次醒来,隔了 N 个小时」;「这些天再没被提起过」的判据只是「之后有没有事件 `refs` 指回它」,改成「还摊在那儿」;桌上的「还有一件没合上的事」暗示她还没得到答案,其实只是她反复问过,改成「还有一件你反复问起的事」。设置页兴趣卡里那句「问过还没答案的问题」一起改。
|
|
257
|
+
|
|
258
|
+
**六、用 `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-<时间戳>`。
|
|
259
|
+
|
|
260
|
+
**七、「检查更新」在 pnpm 的 isolated 布局下也能显示下载按钮了。** 从模块路径反推 profile 根时,遇到 `node_modules/.pnpm/<包>@<版本>/node_modules/<包>/` 这种真身路径,以前会截到 `.pnpm/...` 那一层——那里没有 `package.json`,于是判成「没装」,`managed:false`,按钮永远不出现。现在往回退到 profile 根。
|
|
261
|
+
|
|
262
|
+
**八、两个社区 PR 已合入**(@BBleae):#40 主动搭话关着时不再每轮同步算一遍 `deriveSelf`(8000 条事件实测从 7.6 秒降到 50 毫秒级);#39 「只留证」的事件(聊天行、带 `selfmod` 的说法)不再被当成她问过的 / 总说到的摆回桌上,也不算她「自己动过」。
|
|
263
|
+
|
|
264
|
+
**九、又是一对社区 PR(#44、#45)已合入**(@BBleae):
|
|
265
|
+
|
|
266
|
+
- **配置偶尔整个回默认,是它自己撞坏的。** `writeAtomic` 写文件时先写临时文件再改名,而临时文件名只有「进程号 + 毫秒」。同一毫秒里两次写(设置页自动保存、调度器记账撞在一起)会共用一个临时文件,内容交错,改名后就是半个 JSON。坏文件进 `readJson` 的坏档分支被收走并返回 `null`,`updateConfig` 按默认值处理再写回去——`window`、`wakesPerDay`、`proactive.enabled` 这些就都回到默认了。现在临时文件名带 4 字节随机后缀。
|
|
267
|
+
- **「她靠工具发话」的部署现在记对了账。** 有的部署里她的正文不会自动转发,要靠一件发送工具才发得出去。这种部署上主动回合只认正文,于是她明明发出去了却被记成「没开口」:不占额度、不起冷却,够长的还被折成一行墓碑,模型侧之后看不到自己发过什么。新增 `proactive.sendTools`(填发送工具名,工具调成功才算开口)、`proactive.idleCountsSeen`(`seen:` 也算打断安静,不然「你刚聊完」它以为「好久没人理你」)、`proactive.foldSilent`(上下文很大、靠缓存省钱时关掉折叠)。处境段与身份段也会照实写「用这个工具发、正文发不到聊天里」。
|
|
268
|
+
- **主动回合投递前也看她在不在忙了。** 定时叫醒一直有这个判断,主动回合没有——她正开在别的一轮里时投递,处境段挂在旧轮上、旧轮一结束就被清掉,真醒的那一轮只收到一个空格。现在两边都看本进程的忙碌表,也认别的插件发来的 `dsh-fish/busy` 事件(谁在忙谁自己说,不用互相知道包名)。
|
|
269
|
+
|
|
270
|
+
### 需要你动手的一步(只影响从 0.9.0 以前升级的人)
|
|
271
|
+
|
|
272
|
+
如果你当初是用 `plugin add` 装的,profile 里可能还留着旧的 `dsh-fish` 键。手动跑一次:
|
|
273
|
+
|
|
274
|
+
```bash
|
|
275
|
+
node scripts/fix-legacy-link.mjs --profile desktop --write
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
不加 `--write` 是预览,只告诉你它会改什么。它会把旧键、旧 `bundles` 条目清掉,老链接改名成 `.disabled-<时间戳>` 而不是删除。
|
|
279
|
+
|
|
280
|
+
---
|
|
281
|
+
|
|
282
|
+
## 迁移
|
|
283
|
+
|
|
284
|
+
**无必改项。** 这一版不改事件字段、不升 `EVENT_SCHEMA_VERSION`;旧数据照读,行为只朝「更少误判」的方向变。
|
|
285
|
+
|
|
286
|
+
新增三个**可选**配置(`proactive.sendTools` / `foldSilent` / `idleCountsSeen`),默认值等于旧行为——不填、不配置的部署一处不变。只有「她写的正文不会被自动转发、要靠工具才发得出去」的部署才需要填 `sendTools`。
|
|
287
|
+
|
|
288
|
+
一处**观感上会变**:`her_say` 的回执从 `[说出去了]` 改成 `[记下了]`,桌上一句从「还有一件没合上的事」改成「还有一件你反复问起的事」。都是措辞,不动数据。
|
|
289
|
+
|
|
290
|
+
一件要说明的:**已经被误改名成 `.corrupt.<时间>` 的老文件不会自动恢复**。内容本身是完好的,只是名字被改了;需要的话手动改回原名即可(这次决定不自动扫、不自动改回)。
|
|
291
|
+
|
|
292
|
+
## 测试
|
|
293
|
+
|
|
294
|
+
全量 `node --test` = **1512 pass / 0 fail**(0.9.0 是 1441,本版新增 70 条,其中四个社区 PR 带来 29 条)。
|
|
295
|
+
|
|
296
|
+
新增的:#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 那几条已验证能复现**——把判据改回去就红。
|