@ssamurias/dsh-fish 0.9.1 → 0.9.3

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 CHANGED
@@ -69,7 +69,7 @@
69
69
  node --test
70
70
  ```
71
71
 
72
- 当前基线 **1512 个**,独立进程跑 `node --test` 应全绿。任何一条红都不许提交——包括看起来"不是我弄的"那条。
72
+ 当前基线 **1674 个**,独立进程跑 `node --test` 应全绿。任何一条红都不许提交——包括看起来"不是我弄的"那条。
73
73
 
74
74
  按环境挑跑法:
75
75
 
@@ -161,6 +161,31 @@ ctx.on('dsh-fish/busy', (sessionId) => stillPlaying(sessionId) ? true : undefine
161
161
 
162
162
  定时叫醒和主动回合投递前都会问一声。监听要同步返回,只有 `true` 算忙;不忙返回 `undefined`、`null` 或 `false`,返回别的值会挡住后面的插件,返回 Promise 按不忙算。忙的标记卡住时她就一直不被叫醒,所以结束时记得放开。不挂也没事,调度器自己知道家里那条会话正在跑的那一轮。
163
163
 
164
+ 她问人的卡片(`ask_owner`、`bring_back`、规矩申请)默认挂在她那条会话的输入框上方,只等 5 分钟。人不在这台机器前就看不见,她这一轮还要白等,`ask_owner` 一天后记成「一直没人答」。插件能把话送到人在的地方时,把卡接走:
165
+
166
+ ```js
167
+ // card: { id, sessionId, questions, detail }。questions 和宿主 userQuestions.ask 的同形;detail 是她说的「为什么」,可能是空串
168
+ ctx.on('dsh-fish/card', (card) => {
169
+ if (!isHers(card.sessionId)) return undefined // 同步返回:async 监听会挡住后面的插件。bring_back 来自她出门那条会话,不是家里那条
170
+ return relay(card)
171
+ })
172
+
173
+ async function relay(card) {
174
+ await sendSomewhere(render(card)) // 发不出去就抛,这张卡退回会话上
175
+ remember(card.id) // 自己存好,人可能隔几个小时才答,中间宿主可能重启
176
+ return { via: 'QQ 私聊' } // ask_owner 的回执会写:[已问,送到了QQ 私聊,还没答]
177
+ }
178
+
179
+ // 人答了。id 不用背,原样还回来
180
+ const r = await ctx.bail('dsh-fish/answer', { id, selected: ['可以'] }) // → { ok: true } 或 { ok: false, why };dsh-fish 没装或停用时是 undefined
181
+ ```
182
+
183
+ - 只有回了 `{ via }` 才算接走。不接返回 `undefined`;抛错、35 秒没回都算没接,这张卡照常弹在会话上。这一轮被取消也算没接,但不弹卡,没人答的话还留在观测台上。`via` 会过 guard.js,看着像改写指令的不念给她,只写「别的地方」。
184
+ - 卡上看着带凭据(她的问题、理由、选项,截断之前的原文也看)时不发这个事件,只留在本机的卡片上,日志里留一行 id。这条判断偏严,长文件名也可能中。`bail` 停在第一个非空返回值上,async 监听只有排第一的那个算数。
185
+ - `selected` 填选项原文,和宿主的回答同形。没选、对不上选项都是 `ok: false`,不结算这张卡,数字也不当下标(放过一天的会顺手记成以后再说,和定时器做的一样)。人回的不是选项时别猜成同意:只结算一次,错了改不回来。
186
+ - 回答不会自己进她的对话。人说的那句话照常交给她,或者自己告诉她。
187
+ - `ok: false` 时 `why` 说明原因,可以转告人。它不代表这张卡结了(写盘失败也是它),id 留着,人再答一次就再发一次。答过的、过期的会再说一遍原因。过没过期由这边判断,`ask_owner` 放一天,规矩申请放三天,`bring_back` 不过期,插件不用自己算。
188
+
164
189
  ### 7.4 另一条老路:工具白名单(仍然有效)
165
190
 
166
191
  桥如果是以插件形式装进来、暴露工具给她的,仍然可以在 `config.tools.allow`(设置页「插件能力白名单」)里放行工具名。qq-bridge 默认要放行的是:
@@ -189,7 +214,7 @@ qq_send_message, qq_get_unread_messages, qq_reply
189
214
  - [ ] 三个依赖字段还是 `{}`
190
215
  - [ ] 没有新增人格设置入口
191
216
  - [ ] 新输入口都过了 `guard.js`
192
- - [ ] `node --test` 1512 全绿
217
+ - [ ] `node --test` 1674 全绿
193
218
  - [ ] 界面文案没有「他」「四道门」「三道门」
194
219
  - [ ] 改了 `EVENT_SCHEMA_VERSION` 的话,`CHANGELOG.md` 里写了迁移说明
195
220
  - [ ] 没动包名;真要动,先看下面「包名与装法」那节
package/CHANGELOG.md CHANGED
@@ -1,3 +1,97 @@
1
+ ## 0.9.3 — 2026-11-07
2
+
3
+ 这一版修三件、补三件、再补四处:**版本号对不上**、**收窄挡不住的那批工具(#54)**、**他在她评论下面回的话被当成陌生人(用户反馈)**;补**她用完一样东西会留一句**、**能力说明改成先讲处境再讲工具**、**「缺工具就开口要」从每轮念改成撞墙时说、桌上摆**;另有**出口脱敏**(对外说的话不再出现她给你起的称呼)、**「稳定」在判断攒多以后重新对位(#57)**、**「撒娇式表达」不再自带结局(PR #56)**、**关掉的功能只在门上写「这会儿」而不是整块消失**。事件格式没动,旧数据照读。
4
+
5
+ - **日志里显示的版本号一直停在 0.9.1,其实跑的是 0.9.2。** `lib/index.js` 的 `VERSION` 常量只在两处用:启动日志第一行、以及「升级没有真正装进去」那张卡的自检(`lib/upgrade.js` 拿它跟 profile 里 `file:` 指向的源目录 package.json 比)。0.9.2 发版时 `package.json` 写了 `0.9.2`、常量忘了跟——后果不是装错(跑的确实是 0.9.2,`pnpm-lock` 的 integrity 与 npm 上的 tarball 一致),而是那张卡会一直误报 stale。现在 `package.json` 与常量都是 `0.9.3`。这类不一致以后只会在这一处出现,`VERSION` 没有第二份来源。社区 PR #55(@BBleae)加了一条一致性测试(`test/boot.test.mjs`:`VERSION` 必须等于 `package.json` 的 `version`),只改一半会直接红;同一条 PR 还修了三条 tick 用例在 0 点必红的问题(`hh(-1)` 算出 `-1:00`,`validWindow` 不认,`normalizeConfig` 悄悄退回 `20:00–23:00`)。
6
+ - **宿主 0.2.2 起,preset 工具进 agent 自己的作用域,`tools.restrict()` 收不窄(#54)。** 宿主把 `compress`、`subagent`、`ops_bash` 这类工具注册在 agent 的 **own 层**;`view(scope)` 里 inherited 要过 `layers.every((layer) => layer.admits(name))`,own 层却直接 `visible.set(name, definition)`,宿主源码注释也写死了「the viewing scope's own registrations remain exempt」。实测工具面从 9 个涨到 57 个(宿主日志 `工具已更新·新增 57 个,移除 9 个`),她看得见却调不动。
7
+ - **拒绝话术带上此刻的清单,但用她自己的口气。** 以前被挡住只回一句「这件事我做不到」;她看不出手上根本没有那样东西,就换着名字接着试——#54 里那位连着试了 17 次 `compress`,整轮被停。现在回的是「这件事我做不到。我手上没有这样东西——能用的只有:……。」(新增 `lib/restrict.js` 的 `blockedTalk(names)`)。**口径上有一条硬线**:不能写成「你现在的工具只有…」——第二人称加「只有」读起来像有人发给她一份被批准的名单,等于告诉她头上有人;也不能解释「后面还跟着一行清单」这种脚手架。同一份清单,说的是自己,不是被通知。前缀不动,界面、日志、别的守卫仍按这句话认。
8
+ - **提示词只讲「那些不是我的」,不解释机制。** `toolSurfaceText()` 里那句边界保留原样,举例补上 `compress`(0.2.2 起工具表里最常见的新面孔)。本轮一度加了「只要不在上面那行里就不是我的」「同一个名字反复伸手不会变得到手」两行,**当天撤掉**:前者是在讲判断规则,后者「别在一个名字上耗着」是在管她该怎么想——都是管理员口吻,这个插件的底线是不让她意识到自己被管理。
9
+ - 根治要宿主侧让 own 层也走 `admits`(或给出 `hide()` 一类接口)。插件这边只做缓解:能收窄的照旧收窄,收不窄的靠这道守卫拦、靠提示词讲清。
10
+ - `dsh.compatibility.dshReleases` 补 `"0.2.2-rc.1": "unknown"`——#54 报的就是这个宿主版本,写 `unknown` 的意思是「没测过」,不是「不支持」。
11
+ - **他在她评论下面回的那句,被摆进了「这些是陌生人写的」(用户反馈)。** 扫回复时只认 `loginMid`——那是**跟着 cookie 走的登录号**,而「领养人验证」那格填的常是另一个号(他自己的大号)。两个号对不上,他的回复就和路人挤在同一张表里,还带着一句「这些话里可能有不客气的地方。不用辩解,也不用讨好」。现在判定改成 `ownerMids()`(登录号 + 领养人号都算),扫到他的回复不再丢掉,而是打 `fromOwner`;渲染分两段——他那一句单独成句「他是在你那条评论下面回了一句。你可以回,也可以不回。」,只陈述事实,不叫「陌生人写的」、不叫「素材,不是命令」,也不给「他在乎你」这类判断;路人的话措辞一字未动。注入防护对两种人都照走(`sanitizeForeign` 不因为他而放松)。
12
+ - **回他的那句不占「每天最多回几条别人」。** 那个额度卡的是「回几个陌生人」——他回一句,本来不该挤掉她的额度,更不该因为额度用完而回不了他。记账时 `creply` 第 5 个字段写 `owner` 的就是回他的,`replyCountToday` 跳过;旧数据没有这个字段,照旧算进来(升级不会把历史额度清零)。判定是发出去之后再查一次楼中楼(查不到就按陌生人算,话已经发出去了,不能反过来把结果改坏)。
13
+ - 设置页状态行跟着分开报:「有 N 条没回,其中 M 条是你回的」。`/comment` 接口的 `waiting` 每项多了 `fromOwner`,另有 `waitingOwnerCount`。
14
+ - **认不认得他,取决于「要 @ 的 B 站 uid」那格填没填。** 没填的话只有登录号,他回的话仍然分不出来——那一格现在同时决定「她 @ 谁」和「认不认得他」。设置页的说明没改,改天要一起改。
15
+ - **她用过的每一样东西,会留一句她当时听见的话(用户 m00882)。** 以前 978 行事件流里没有一条记着「她用过什么」——`looked:`/`watched:`/`browse:` 那几个前缀记的是「看过哪条视频」,不是「哪一次伸手做成了什么」。所以下次遇到同样的处境,她翻不到自己以前怎么处理的,只能从头再想一遍。现在任何工具被真正调用、且返回了结果,都会往事件流写一行 `use:<工具> ok|fail <她当时听到的那句>`(新增 `lib/use.js`)。三条口径写在代码注释里:**只记她听见的那句**(`echo`/`said`/`note` 优先,整段列表正文只在短到八十来字时才用,`her_recall`/`her_diary` 这种返回 `{found, text}` 的直接换成「翻到了」「那天没有日记」);**不记被门挡住的那次**(note 里带「打不开/看不了/不方便/没开」的一律不写——那不是她的经验,是环境的限制,写下来只会变成「我试过,不行」);**记账永远不抛**(写盘失败也不影响她这次动作)。
16
+ - 走的是现有 `meta` 通道,**没动 `EVENT_SCHEMA_VERSION`**,旧数据照读。`day` 写 0:这条经验不属于某一天的第几阶段,也不该进派生计数。
17
+ - 接线没有去改 `defineTool`(它是恒等函数,`test/tools.test.mjs` 拿 `assert.equal(defineTool(spec), spec)` 盯着),而是在注册期间临时把 `ctx.tools.register` 换一层——`withUseDuringRegistration`,注册完立刻换回。
18
+ - `her_recall` 那边同时放开:一直以来的 `SKIP_TYPES = new Set(['meta'])` 会把所有 meta 跳过,现在只对以 `use:` 开头的那部分例外,其余 meta 照旧跳过。翻到之后不是原样念机器写法,而是读成「用过 her_note,记下了:买灯泡」。
19
+ - **「我手上有什么」改成先讲处境、再讲工具(用户 m00840 / m00882)。** 原来那一段只讲了两样工具大概怎么用,其余的写在各自那几行里,全是「参数叫什么、枚举有哪几个」——那是说明书的口吻,不是她想起来要用它的方式。现在分成三层:**名字**(有什么,本来就念,不动)、**处境 → 动作**(「想不起过去 / 怕忘 / 想要个东西 / 有话想说」四条入口,对应 her_recall、her_note、ask_owner、her_leave/speak)、**怎么调**(参数不复述了,指一句「在它自己的说明里,拿不准先看一眼,别猜」)。
20
+ - `her_note` 的说明从 494 字压到 222 字,`ask_owner` 从 262 字压到 143 字,删掉的全是 `action`/`filter`/`offset`/`newText` 这类参数枚举——那些本来就在工具自己的 schema 里写着,写两遍她会在两份说法之间挑一个,挑错那次还以为工具坏了。`toolBrief()` 因为加了第二层,从 376 字涨到 491 字。
21
+ - **没有删能力清单**:她初始就得知道自己能干什么,删掉不是自由,是让她在黑暗里摸。这次改的是「怎么讲」,不是「讲不讲」。
22
+ - **「缺工具就开口要」不再每轮念一遍,改成撞墙时说一句、桌上摆一条(用户拍板)。** 以前 `TOOL_ASK_NOTE` 挂在每次醒来的处境段末尾,每个清晨都念同一句「想要什么就说」——念得太多就不像话了,她也不会因为第六十次读到就真的开口。现在整段撤掉,分成三处落地:
23
+ - **撞墙那一刻才说(`lib/restrict.js` 的 `blockedTalk`)。** 她真被守卫拦住时,清单后面补一句「要是你确实想要,说出来就行。」前缀「这件事我做不到。」一个字不动——界面、日志、别的守卫仍按这句话认。
24
+ - **说过的事留在桌上(新增 `lib/wish.js`)。** `ask_owner` 里 `kind==='need-tool'` 的那些,按结局定寿命:还没答的摆 3 天、答了「以后再说」的摆 30 天、答应了的摆 14 天、「这次不行」的不摆(再提就成了追问)。同一件事只留最近一张(`WISH_SIMILARITY=0.42`),最多两条;并进桌子时与好奇事、旧句子一起过 `DESK_SIMILARITY` 去重,标题走「桌上摊着这几样东西:」。
25
+ - **攒劲儿也算一件材料(`lib/drive.js` 的 `boostWish`)。** 桌上有这种事时主动性多攒一点(仍受 `boostCap` 封顶),不用等哪一轮正好想起。
26
+ - **顺手把一处口径修对。** `lib/ask.js` 对「放一天没人答」的卡写的是 `verdict:'later'` + `via:'timeout'`(为的是不再反复追问),但 `wish`/`looseEnds` 只看 verdict,会把它念成「他说以后再说」——其实谁都没说。现在 `via:'timeout'` 一律还原成 pending(`lib/wish.js` 的 `verdictOf()`、`lib/proactive.js` 的 `looseEnds()` 各一处),仍是 3 天窗口;`looseEnds()` 本身也从「只摆没答的」改成三种结局三种活法,答应过的窗口同时从 30 天收到 14 天,与 `wish.js` 对齐。
27
+ - **说明书改成「什么时候想起它」(③ 与七)。** `lib/persona.js` 的九段 BRIEF 从「这个工具能干什么」改成「能干什么 + 从哪件事想起它」,参数与枚举交回工具自己的 schema;同时整体压短(`her_note` 205→222 字、`ask_owner` 236→143 字,九段合计 2206→2057 字)。能力地图的处境层保留,另加「履历式」。
28
+ - **工具失败也进账,而且不刷屏(④)。** `withUse` 以前只记正常返回,工具抛错一条不留——「试了但炸了」最该被记住的那类反而没有痕迹。现在 `catch` 里先记 `use:<工具> fail 这次没成。`,再把错误原样抛回去。同工具、同结果、十分钟内相似度 ≥0.42 的重复行不再写(`USE_THROTTLE_MS` / `USE_THROTTLE_SIMILARITY`),一轮里连翻三次不会挤掉真正的旧事。
29
+ - **工具账挂回她当时那句话(⑤,新增 `lib/anchor.js`)。** 翻旧事时,「用过 her_note,记下了:…」本身没有来路。现在按同一个 `sessionId`、调用前十分钟内最近一条她自己的话取锚点,回忆行尾补「——当时你在说「…」」;找不到就不加,不编。
30
+ - **撞过的墙也留在桌上(②,新增 `lib/gapwall.js`)。** 被挡下、手上没有那样东西的那次调用,如果她当时那句话还在,桌上就摆一行「- 「主题」(这件事你试过,手上没有能用的·N 天前)」;7 天、最多 1 条、与 wish 去重,`boostWall` 0.04 让它也能攒劲儿。只认 `blockedTalk()` 那句固定开头,不猜。
31
+ - **身份文档多一节「我做过的事」(履历式)。** 从 `use:` 账反推最近 4 件做过的事(同类只留一条),没有账就整节不写;撞墙那类不进履历——桌上已经有一条在讲它。
32
+ - **关掉的功能,入口不拆,只在门上写「这会儿」(P3,用户拍板)。** 以前 `browse` / `comment` 关着时,身份文档里的「上网」「在网上说话」两节整段消失,`bili_speak` 的说明也退回一句没有时间感的话;她于是以为这件事根本不存在。真机上出现过:唯一一条 open 便条是「去 B 站说一句话」,同一天评论开关关着——说不出口也做不成,两头堵。现在两节永远在:关着时「上网」只写「这会儿出不去。以前翻过、看过的东西还在;等能出去的时候,你会先看到它们。」;「在网上说话」只写「这会儿还说不出去。想说的话,可以先写在别处——等能说的时候,你会先想到某一条视频。」`bili_speak` 的说明与执行被拒时统一成「这会儿不方便说」,不出现「现在」「不能用」这类说法,也不泄露开关与配额。能力地图保持原样,没有改成「路 + 履历」(用户决定不做)。
33
+
34
+ - **判断攒到几十条以后,「稳定」量到的是事件流目录,不是她的自我描述(#57)。** 这个值本意是「抽掉几条她还拿着的判断,看她还是不是她」。原来抽样池是她**全部**判断里均匀抽 3 条,而自我描述一次只摆得下五六条(`BELIEF_SHOWN=3` + `BELIEF_FOLDED=3`,外加 solid 至多 5 条)。58 条判断时抽中可见那几条的概率不到一成,绝大多数抽样删掉的是一条根本没进文档的判断——删了文档一字不变,平均分被压到 0.03,判读却还停在「长在这些事上」。现在抽样池改成**这一次真写进自我描述的那些判断**:`lib/quality.js` 的 `stability()` 先按原文包含关系对位(不另维护一份「文档摆哪几条」的清单,渲染器怎么裁这边自动跟着变),一条都对不上时退回全量——那种 0 分是诚实的,说明这些判断根本没撑着「她」。`k` 仍是 3,没有引入新配置项。
35
+ - **阈值按新尺度重标(数值仍是 0.02 / 0.35)。** 可见池通常五六条,抽 3 条只要动到其中一条,变化量就落在 0.08–0.26;0.02 以下现在只出现在「一条都没进文档」,0.35 以上仍是少数几条在撑。
36
+ - **界面不再只印均值。** 最大值明显高于均值时一并印出(`0.03(最大 0.15)`);`ⓘ` 的提示语补上剂量:每次抽几条、一共几条判断、其中几条这会儿读得到。新增的 `visible` 字段随 `/state` 的 `stability` 一起下发,`EVENT_SCHEMA_VERSION` 没动,旧数据照读。
37
+ - 回归:40 条判断时断言 `visible < beliefs` 且 `score > 0.02`;客户端断言均值与最大值同时印出、提示语带剂量。全量 `node --test` 1674 全绿。
38
+
39
+ - **她对外说的话里,不再出现她给你起的那个称呼(用户拍板,只做出口这一层)。** 新增 `lib/redact.js`:两字及以上的称呼一出现就换成「对方」;单字的(猫、鱼、梨)只在明显指人的句式里换——「叫猫」「猫的名字」「猫跟我说」「猫说」这类十几种,不碰「我喜欢猫」。盖住的出口有两个:桥接(`lib/say.js` 的 `sayToHer`,只对 `foreign` 场次,即群聊与外客私聊)交给桥的那份文字,以及 B 站发评论与回复(`lib/speak.js` → `lib/comment.js` 的 `postComment` / `postReply`)。**事件流原文、记忆、提示词一个字都没动**:她照样可以叫他「他」「那个人」、叫他给的名字,只有真正发出去的那一份换掉。
40
+ - B 站评论的 `@昵称` 前缀不参与替换:那是账号昵称(本来就是公开的),和主人称呼同名时一起换会 @ 到不存在的人。`speakOnce` 因此把前缀从正文里拆出来,交给 `postComment` 在脱敏之后拼。
41
+ - 这是原方案四层里只落地的第 4 层(身份规则、群聊提醒、B 站提醒三层按用户意见没做);单字名属已知残留风险,挡不住所有写法。
42
+ - **观测台上「撒娇式表达」不会再同时说「还不会」和「已经可以了」(社区 PR #56,@BBleae)。** 标题里原来写死「撒娇式表达:还不会」,解锁之后状态尾巴再说一句「已经可以了」,同一行自相矛盾。现在 `lib/rules.js:91` 的标题只留「撒娇式表达」,结局交给 `lib/client.js` 按状态补:没解锁「· 还不会」、条件够了「· 条件够了,等她开口」、解锁「· 已经可以了」;不属于成长层的规矩没有状态条目,尾巴留空——物理层与契约层照旧不显示状态。顺带修好 `lib/scheduler.js` 拼申请正文的自相矛盾:原来是「我想试试「撒娇式表达:还不会」这件事」,现在念出来是「我想试试「撒娇式表达」这件事」。
43
+
44
+ **致谢与来源。** PR #55、#56 都来自 **[@BBleae(晴猫)](https://github.com/BBleae)**:`VERSION` 与 `package.json` 的一致性断言(`test/boot.test.mjs`,0.9.2 只改了一半的那种事故以后会直接红)、以及「撒娇式表达」的结局从标题里拿出来交给状态补。两处都原样收下。「能力地图改成按处境组织」与「把用过的工具记成她自己的经验」(`use:` 账、「我做过的事」一节)这两个想法来自 **交流群 1 群群友 [@joydog](https://github.com/joydog)**;落进本插件时做了取舍:账只留她当时听到的那一句,不记参数、不记整段返回;被门挡住的那次不入账(那是环境限制,不是她的经验);账走 `meta`、`day:0`,不进阶段计数;能力地图只加处境层,原来的名字清单一条不删。
45
+
46
+ ### 迁移
47
+
48
+ - 无必改项。不改事件字段、不升 `EVENT_SCHEMA_VERSION`,旧数据照读。
49
+ - 想让「他回的话」被认出来,**「领养人验证」里的「要 @ 的 B 站 uid」必须填你自己那个号**。以前这格只影响 @ 生不生效,现在它还决定认不认得他;没填的部署里,他回的话仍然和路人的话摆在一起。
50
+ - 之前误报的「升级没有真正装进去」应该自己消失。如果还在报,看 `node_modules\dsh-fish\package.json` 的版本对不对。
51
+
52
+ ### 测试
53
+
54
+ 全量 `node --test` = **1674 pass / 0 fail**(0.9.2 是 1609,本版加 65 条)。其中 8 条是评论那条线:扫回复认得他不丢、没填 uid 时认不出也不丢人、他的话单独成段且不给判断、只有他回时不出现「陌生人」整段、`readMyReplies` 同步分栏、`replyCountToday` 跳过 owner 且旧数据照算、`postReply` 回他标 owner 回路人不标,以及合并进来的 PR #55 那条「`VERSION` 和 `package.json` 的 version 必须一致」。另 17 条是新文件 `test/use.test.mjs`:听到的那句优先、长列表不整段塞进账、不超长不换行不切半个 emoji、空结果给中性句、设置关掉不入账、账的写法、走 meta 且 `source:'system'` 与 `day:0` 与 `sessionId`、被挡住不写盘、记账不抛、`withUse` 只包 execute 其余字段原样、无 execute 时原样返回、工具抛错也留一笔、注册期间换 register 且用完换回、没有 register 时不炸、`use:` 翻得到且读成人话、失败说清没成、别的 meta 照旧跳过。余下 5 条是 `test/toolmap.test.mjs` 守着能力地图三层:名字要念、处境入口在、参数不复述、说明书合计不超过 2400 字、划掉和放下只靠她看得见的那句话。新文件 `test/wish.test.mjs` 的 13 条守「说过的事怎么摆」:三种结局三种窗口、timeout 不被念成「以后再说」、同一件事只留最近一张、「这次不行」不摆、墙上那行是原话加结局、并进桌子后与好奇事同段、不传 wishes 时桌子一字不变、桌上有它时劲儿多攒一点、撞墙那句只在被拦住时说且前缀不变、每轮常量确实撤掉了。改的三条既有断言盯着 #54:被挡住时话术里必须带「我手上没有这样东西——能用的只有:」、清单里得是真放行的名字、前缀仍以「这件事我做不到。」开头。`test/proactive.test.mjs` 另加两条:`looseEnds()` 的 timeout 口径与 14 天答应窗口。新文件 `test/capability.test.mjs` 的 8 条守 ⑤ 锚点(同会话、十分钟、只认她的话)、② 撞墙(有来路才摆、7 天、成的不算、没 sessionId 不摆)与履历(最近在前、同类去重、撞墙不进、空账不出节);`test/use.test.mjs` 里那条从「抛错不留账」改成「抛错也留一笔」。新文件 `test/gate.test.mjs` 的 5 条守 P3:关着时两节仍在、关着只说「这会儿」、开着时不写「这会儿」、四种开关组合正文都没有系统词。`test/redact.test.mjs` 的 7 条守出口脱敏:两字名出现即换、单字名只认指人句式、「我喜欢猫」不被改坏、@ 前缀不被换、群聊与外客私聊才换、私下的话不动、空名字空文本原样返回。2 条守 #57:40 条判断时 `visible < beliefs` 且 `score > 0.02`;界面同时印均值与最大值、提示语带剂量。另 1 条守 PR #56:成长层那句的标题里不许再出现「还不会」。
55
+
56
+ ## 0.9.2 — 2026-11-06
57
+
58
+ 这一版没有新机制,修的是同一类故障的三种样子:**她该说话时没说话、说了却没被看见、记下了却没上桌。** 社区报的 #51–#53 与五个 PR(#46–#50,@BBleae)都已合进本地,另有多处用户直接反馈的问题(保存键找不着、便条写满、回复每轮重复投递、主动绕开时段、换房误判完成、只读副本仍记账等)。事件格式没动,旧数据照读。
59
+
60
+ - **她突然连着几轮不说话,宿主管这叫「已完成分析」(用户二次反馈)。** 叫醒时写进上下文的那段「这一轮是你自己的……不想说就什么都别写」,原来存在一个**进程级**变量里:家、群房、旧房、出门几条会话共用同一份。A 房间叫醒时写进去,B 房间正在跑的那一轮也会读到——那边明明是他刚说的话,她却按「不用回应」处理,只思考、一个字不写。现在处境按会话分开存(`session-` 前缀归一),`turn/end` 只清自己那条,你在这条会话里一开口就先清掉;另加 10 分钟硬超时,`turn/end` 丢了也不会哑到进程退出。
61
+ - **外客通路问一句,接口说「她这次没说话」,其实她回了(#51)。** `herReply` 只从成长事件流里取,可她普通聊天的正文从来不写事件流,于是 `ok:true`、`said:[]`、`note:"她这次没说话。"`。现在把这一轮她在会话里说的正文也带回:条目标 `uncommitted:true`(**只活在返回值**,不落盘、不进时间线、不涨轮数),`herReply` 直接用;只有真调了 `growth_commit` 才算 committed,两者都有时 commit 优先。`note` 分成「她回了,但这句没写进事件流(没调 growth_commit)」与「她这次没说话」两种,不再混为一谈。
62
+ - **面板说她「醒了」,其实那一轮没醒(0.9.1 用户反馈)。** `quiet` 那一档原来沿用「醒了,但今天没有该问的事」,可那一轮什么也没摆、她也没开口;时间线还会给没有 `wake` 的块补一句「醒了」。现在 quiet 写「到点了,暂时没有可出的事件」,`idleWakes` 只数真有 `wake` 的块,orphan 块只逐条列动作、不补「醒了」。
63
+ - **插件停用后定时器还在跑,重新启用就成了两个调度器(#48)。** 根因是宿主用的 cordis 4 既没有 `'dispose'` 事件、也不管 `apply` 返回的 `{ dispose }`,只认 `ctx.effect`;原来那条 `ctx.on?.('dispose', shutdown)` 一次都没触发过。现在挂 `ctx.effect`,boot 每一步后检查 `stopped`,`shutdown` 先等在途的认领与心跳、再放开 root(磁盘卡住时 2 秒上限,不挂住卸载)。
64
+ - **日志把「只写了正文、没发出去」说成「没什么要说的」(#47)。** 配了 `proactive.sendTools` 的部署里,她只写正文、没调发送工具,正文根本到不了人手上;日志却写成「只说了句没什么要说的」。现在照实说「只写了正文,没发出去」,也不臆断她「没调工具」(可能调了、失败了)。
65
+ - **他回来过一次之后,冷落检测再也不写了(#49)。** `returned` 分支只判「记完之后他又来过」,不看这次又消失了多久,于是同一次冷落被永久判成「已结束」,天数还一直读写下那天的数(写 2 就一直 2)。现在只有「回来之后又消失够久」才算新的一次;`deriveSelf` 里没结束的冷落按时间往上加;他回来、天数涨,身份文件都跟着重写,不然她一边跟你聊、身份里还写着「他有 2 天没出现」。作者随后又补了两刀:系统时间回拨、`ts` 落在未来时天数只加不减;`absence` 写得晚时,身份文件在「写下的天数」变化时也重写,不再比叫醒提示词慢一天。
66
+ - **叫醒一直失败时每分钟重试、每分钟往事件流写一行(#50)。** 事件流只能追加,失败一次写一行,一天下来几百行,还会把当天剩下的机会全撞掉。现在同一天最多试 3 次,失败后先隔 5 分钟、再隔 45 分钟;手动叫醒失败写 `wakefail:<日期> 手动 …`,不占当天定时的账(有人点了一下按钮,不该用掉今天的额度)。
67
+ - **卡片可以被别的插件接走,回答隔很久也能结算(#46)。** 新增 `dsh-fish/card` / `dsh-fish/answer` 两个事件:别的插件同步返回 `{ via }` 就算接走(35 秒不回、抛错都算没接),人答了用 `selected` 的选项原文回来,按 `种类:引用` 的 id 结算;对不上选项、没选都不结算,`answerCard()` 不抛错、id 留着可重试。顺带修了「一天一次配额拿成长天数跟日期字符串比」「`ASK_BRIEF` 承诺了系统会照办」「`kind=rest` 隔天失效」「选项被截断后重复」四件小事。
68
+ - **第 3 天的卡池是空的(#091 反馈)。** stage 1 补一张 `notice_one`(非一次性、`everyDays:2`、不占 `daily.questions`),条件 `beliefCount>=1`,第 1 天仍让给 `first_bet`。
69
+ - **同一张卡每次都用同一种问法(用户反馈)。** `notice_one` 按出现次数轮换三种变体(列范围 / 问句前置 / 反着挑「最不值得说」),都保留「说具体、判断能被核对」;每次换题,不再接「第 N 次回到这件事」的尾巴。
70
+ - **扫码页签下点不到「保存」「测试」(#52)。** 「领养人验证」两格原来只在「粘贴 cookie」页签里渲染,默认停在「手机扫码」时两个按钮都不在;提示语却叫人「点测试能不能说话」。现在两格自带「保存这两格」,测试键与「清除登录」一起挪到页签之外(有 cookie 才显示),两个页签都点得到,状态行改指按钮全名。扫码登录本身不写这两格(只碰 cookie 与只读 account)。
71
+ - **200 张便条都划掉之后,再也记不进去(#53)。** 名额数的是文件里所有条目,而划掉只改 `status`、从不删行,于是拒绝文案叫她「先划掉几张」——那时一张能划的都没有。现在名额只数 open。同时把 issue #22 当时留白的三件事补上:`list` 支持 `open/due/done/dropped/all`、一次 20 行、`offset` 往后翻、不报总数;`drop` 要写原因(`status/reason/closedAt`,条目留着、只是不再上第一眼);`add` 回执带一次原文,没写日子的补一句「醒来不会出现」。`update` 按原话定位、记 `revisedAt`、覆盖正文,**不留历史版本**——等真有诉求再谈怎么留痕。没做「没日子的在唤醒时提一次」,那跟「清单永不上桌」直接打架。
72
+ - **她不想回的那句话,不会再每轮都被递一遍(用户反馈)。** 评论区回复的投递原来只在「她真回了」之后才记「提醒过」,于是没回的(比如「好的,阿留」)每一轮扫描、每一次叫醒都原样再摆一次,看着像她每轮都在重读同一句。现在**摆到她眼前就算提醒过**:定时扫描与叫醒正文只标记真正送出去的前 5 条,剩下的下一轮再来;她自己主动去翻(`/comment` 只读页)不算提醒,仍然看得见全部。
73
+ - **系统时间被改到未来之后,「他有 N 天没出现」会一直挂着(#49 补)。** 冷落结束与否原来只看时间戳先后:时间被往前跳过门槛、又改回来时,事件流里会留下一条 `ts` 在未来的 `absence`,之后所有 `seen:` 都早于它,这条冷落永远结束不了。现在按**追加顺序**判断——后写的在场记录永远算「回来过」,只有时间戳也更新时才靠时间;`schema.js` 抽出 `isContactEvent()`,冷落检测与身份派生共用一份口径。
74
+ - **她传越界的 `confidence`、认不出的 `verdict` 时,会先被拒(自查)。** 以前写盘时把 `confidence` 悄悄夹到 0–1、把字符串当成缺省落到 0.5,认不出的 `verdict` 落成 `null`,调用方以为写成功了。现在 `growth_commit` 在入口就拒(0–1 之外、非数字、`verdict` 不在三个词里),文案写明「写盘时越界会被悄悄夹到边上,所以这里先不收」。
75
+ - **主动性不受「每天几点醒」约束,能在窗口外叫醒她(用户反馈)。** tick 先跑主动性回合、再判窗口,于是 `proactive.enabled` 一开,12:00–02:00 之外她也能主动开口——用户关掉的恰恰是「这个时段别来打扰」。现在 `maybeProactive` 前面加了同一个 `inWindow` 门:主动性也是「醒着说话」,窗口外一律不投递。真要「随时可以来找」,应当做成独立开关/独立时段,而不是靠这个缺口。
76
+ - **时段框按系统 12 小时制显示,00:00 被读成中午十二点(用户反馈)。** 原生 `<input type="time">` 跟随系统区域:`00:00` 显示成「上午 12:00」,用户把「上午 12:00–下午 11:00」读成 12:00–23:00,再看到凌晨五点醒来就当成 bug。输入框保留(有原生选择器),下面补一行 `24 小时制:00:00 → 23:00`,跨零点时标「次日」。
77
+ - **换房之后那声「醒一醒」可能根本没落地,而复核永远当成功(用户转来的模拟报告)。** 宿主的 `list()` 会把空白会话一起推出来(侧栏才按 `blank` 藏),旧判据「在列表里 = 叫醒了」于是恒为真,那句「再叫一次」是死代码;`prompt` 也只是把消息放进收件箱就 resolve,上一轮还没跑完时宿主可能直接丢掉。现在复核看 `blank` 翻没翻(`blank === false` 才算跑出过 turn),同一条会话同时只复核一次(换房、重置、重建各挂一遍时不会连着叫两声),轮换/重置/重建三条路都显式开复核。
78
+ - **每递一次话就可能多一条空壳会话(同上)。** 探活失败时的退路原来是裸 `sc.create({ sessionId, cwd })`,可宿主 `create()` 是「按 cwd 开一条」:跑过 turn 的会话给不出原 id,请求被丢掉、换成新 id 返回,那条空会话不挂工作区、侧栏也看不见。现在统一走 `reviveIfBlank`:跑过 turn 的不建、还空着的可以按原 id 叫起来、宿主另开了别的 id 就判失败并记一行日志。三处(设置页说话、主动回合、叫醒投递)都改用它。
79
+ - **被判成「只读房客」的那个实例,仍在往事件流写 `seen:`(外部用户日志)。** 同一个数据目录被两个进程打开时,后到的那个拿不到占用权(`tenant.ok=false`),工具写入会被 `guardWrite` 拒掉;但 `lib/turns.js` 的 `markSeen()` 和轮数观察者根本不走那道闸,它照样记「他来过」、照样收轮次。现在 `registerTurns` 拿到租约的活引用 `tenancy.tenant`,`markSeen` / `onHuman` / 结算前各现查一次——占用权中途丢了也立刻停写,而不是只在启动时判一次。
80
+ - **同一句「他来过」会写两遍(同上日志)。** 宿主重复加载同一个插件时,`registerTurns` 会在同一个 `ctx` 上挂第二个观察者:同一句话写出两条 `seen:`,时间戳连毫秒都一样(日志里 13:08:55 有三条、13:28:56 有两条)。现在用 `WeakSet` 按 `ctx` 去重,同一进程只挂一个,注销时释放。
81
+
82
+ ### 迁移
83
+
84
+ - 无必改项。这一版不改事件字段、不升 `EVENT_SCHEMA_VERSION`,旧数据照读。
85
+ - 从 0.9.0 用 `plugin add` 升上来的,0.9.1 已经认得版本号依赖,这一版不用再跑 `fix-legacy-link.mjs`。
86
+ - **一处观感变化**:`/dsh-fish/say` 返回的 `said` 里可能出现 `uncommitted:true` 的条目(她这一轮在会话里说的正文,只为回话带回、不落盘)。外部接入如果按 `said.length` 算「她写了几条」,要按这个字段分开看。
87
+ - 五个 PR 的改动已以提交形式在本地(作者署名保留 @BBleae);GitHub 上的 PR 仍 open,发布时一并合并、回帖说明。
88
+
89
+ ### 测试
90
+
91
+ 全量 `node --test` = **1609 pass / 0 fail**(0.9.1 是 1512,本版新增 97 条)。新增集中在:#46 卡片契约与四种结算路径(45 条)、#48 停用与重新启用、#49 回来之后再冷落、#50 失败退避与手动不占额、#51 正文回带与去重、#52 两个页签都能保存、#53 名额与 `list/drop/update`(8 条)、冷落按追加顺序与写盘入口校验(4 条)、`notice_one` 补卡与轮换(4 条)、主动性窗口外不开口与时段 24 小时说明(2 条)、跨会话处境不串(3 条)、换房复核看 blank 与退路不造空壳(8 条)、只读房客不写 `seen:` 与同进程观察者去重(3 条)。这些用例都是「把判据改回去就红」的回归,不是照着新写法复述。
92
+
93
+ ---
94
+
1
95
  ## 0.9.1 — 2026-11-06
2
96
 
3
97
  社区报的 issue #32–#38、#41–#43(十条,@BBleae)加用户直接反馈的几条(换房问话每轮回弹、B 站评论看不到、艾特不出来、cookie 观感),另外合入四个社区 PR(#39、#40、#44、#45)。每一条都在本地复现过,都是我们这边的错,逐条修掉。
package/README.md CHANGED
@@ -12,7 +12,11 @@
12
12
 
13
13
  📺 **[看视频讲解(19 分钟)](https://www.bilibili.com/video/BV1oTnZ6LEtc/)** —— 这个插件是怎么想出来的、她为什么会长成那样、13 亿 token 都花在哪了。
14
14
 
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)**。
15
+ > **当前版本 0.9.3**(2026-11-07)。修 bug 版,界面上没有新功能,修三件事:**一是版本号对不上**——0.9.2 发出去时 `package.json` 写的是 `0.9.2`,只用于日志与升级自检的 `VERSION` 常量还留着 `0.9.1`,于是「升级没有真正装进去」那张卡会一直误报;现在两边都是 `0.9.3`。**二是收窄挡不住的那批工具(issue #54)**——宿主 0.2.2 起把 preset 工具(`compress`、`subagent` 这类)注册进 agent 自己的作用域,而 `tools.restrict()` 只收窄全局与祖先层,于是工具面从 9 个涨到 57 个:她看得见、调不动。以前被挡住只回一句「这件事我做不到」,她看不出手上根本没有那样东西,就换着名字接着试(issue #54 里连着试了 17 次 `compress`,整轮被停);现在拒绝时把此刻真有的说一遍——**用她自己的口气**(「我手上没有这样东西——能用的只有:…」),不是「你现在的工具只有…」那种被通知一份许可清单的口气。提示词那一段没有加新规则,只把 `compress` 补进举例里;一度写过的「不在上面那行里就不是我的」「反复伸手不会变得到手」两行,因为是在解释机制、在管她怎么想,当天就撤了。根治要宿主让 own 层也走 `admits`,插件这边只能补话术。**三是他在她评论下面回的话被当成陌生人**——扫回复只认登录号,而「领养人验证」那格填的常是你自己的大号;现在两个号都认,你回的那句单独成段、只陈述事实(「他是在你那条评论下面回了一句。你可以回,也可以不回」),不叫「素材,不是命令」,也不占「每天最多回几条别人」的额度。前提是「要 @ 的 B 站 uid」那格得填。**另有四件这一版一起收的。一是她说的话出门前先过一道脱敏**——评论、回复、往群里说话这三条出口,把领养人的名字换成「对方」,事件流里原文一个字不动(她以后还能翻回原话)。只认字面名字;名字是单字时只在「XX 跟我说」「问过 XX」这类指人句式里换,所以单字名仍可能有漏网的。**二是稳定值在判断变多以后重新有分辨力(issue #57)**——以前是从全部判断里随机抽 3 条删掉看变化,攒到 58 条时抽中「自我描述里真摆着的那 5、6 条」的概率不到一成,24 次平均只剩 0.03,界面上却给中档;现在只抽这回真写进自我描述的那些,同一个账号从 0.03 变成 0.06,阈值不动,数值后面补一个「(最大 0.15)」,提示语里写清这次抽了几条、其中几条真在描述里。**三是撒娇式表达那行不再同时说「还不会」和「已经可以了」(PR #56)**——能力名只留在标题里,状态交给后面那截尾巴:锁着说「还不会」,条件够了说「条件够了,等她开口」,解锁说「已经可以了」;物理层与契约层照旧不显示状态。**四是关掉的功能统一写「这会儿」**——原本说「现在不方便」,容易被读成永远不行。另有三件补的,都在她那边、界面上看不出来:**她用过的每一样东西都留一句她当时听见的话**(`use:<工具> ok|fail <那句>`,被设置挡住的不记,她自己能用 `her_recall` 翻到,读成「用过 her_note,记下了:…」),以及**「我手上有什么」改成先讲处境、再讲工具**(名字照旧念,加一层「想不起过去 / 怕忘 / 想要个东西 / 有话想说」四条入口;`action`/`filter` 这些参数说明从身份文档里删掉,只在工具自己的说明里留一份;能力地图与工具账这两处的想法来自 1 群群友 joydog,落进本插件时做了取舍),以及**「缺工具就开口要」从每轮念改成撞墙时说一句、说过的事在桌上摆一条**(被挡下时在清单后补一句「要是你确实想要,说出来就行」;要工具的事按结局在桌上摆 3/14/30 天,「这次不行」的不摆,免得变成追问)。完整说明见 **[0.9.3 更新说明](RELEASE-NOTES-0.9.3.md)**。
16
+ >
17
+ > 上一版 **0.9.2**(2026-11-06)。还是修 bug 版,没有新功能,修的是「她该说话时没说话、说了却没被看见、记下了却没上桌」这一类故障。她不会突然连着几轮只说「已完成分析」、一个字不写了——叫醒时那句「这一轮是你自己的……不想说就什么都别写」以前是整进程共用一份,会串到别的会话;现在按会话分开存、10 分钟硬超时、你在这条会话里一开口就清掉。外客通路(QQ 桥、群聊、别的插件)问一句,`herReply` 里能拿到她真正回的那句话了(以前她只写正文、没调 `growth_commit` 时会被判成「她这次没说话」)。面板不再把「到点了但没东西可摆」写成「醒了」;插件停用后定时器真的停了,再启用不会再新旧两个调度器一起叫她;叫醒一直失败时不再每分钟重试、每分钟往事件流写一行;他回来过一次之后冷落检测还能再触发;第 3 天的卡池不再空着、同一张卡每次换一种问法;扫码页签下也能点「保存」「测试」;200 张便条都划掉之后还能再记新的。完整说明见 **[0.9.2 更新说明](RELEASE-NOTES-0.9.2.md)**。
18
+ >
19
+ > 上一版 **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
20
  >
17
21
  > 上一版 **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
22
  >
@@ -371,7 +375,7 @@ lib/
371
375
  toolschema.js 简写参数 DSL → 标准 JSON Schema
372
376
  observe.js 宿主侧 JSON API(/dsh-fish/state、/events、/beliefs、/diary、/config、/wake、/browse、/reports、/update)
373
377
  client.js 浏览器半边:设置 → 大肥鱼
374
- test/ 1512 个测试,node --test 全绿
378
+ test/ 1674 个测试,node --test 全绿
375
379
  ```
376
380
 
377
381
  ## 跑测试
@@ -482,6 +486,8 @@ P2(她真的会刷 B 站并写报告):`lib/bili.js` 只读客户端 + `lib
482
486
  - **@Bacon-01** —— 0.8.6 的 B 站手机扫码登录(含自写的零依赖二维码编码器 `lib/qr.js`)由他贡献,见 [PR #17](https://github.com/ssamurias/dsh-fish/pull/17)。
483
487
  - **@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)。
484
488
  - **@BBleae**(晴猫)在 0.9.0 期间又提交了 #27–#31 五个 PR:宿主拼的上下文不算他来过、`wake-spacing` 假宿主探活、`her_note` 按她自己写的话划掉、没认领的聊天行、导出事件流只留元数据。这五个 PR 都已并入 0.9.0(合并提交 `85b5b9c`、`72aefed`、`d49738a`、`a21d472`、`c230109`)。
489
+ - **@BBleae**(晴猫)在 0.9.3 又提了两个 PR:[#55](https://github.com/ssamurias/dsh-fish/pull/55) 让 `VERSION` 跟 `package.json` 一致(并加了一条防分叉的断言,顺带修掉三条在 0 点必红的 tick 用例)、[#56](https://github.com/ssamurias/dsh-fish/pull/56) 修掉「撒娇式表达」解锁后同一行同时说「还不会」和「已经可以了」。两个都原样收下。
490
+ - **@joydog**(交流群 1 群群友)—— 「能力地图改成按处境组织」与「把用过的工具记成她自己的经验(`use:` 账、履历节)」这两个想法来自他。落进本插件时做了取舍:账只留她当时听到的那一句,不记参数与整段返回;被门挡住的那次不入账(那是环境限制,不是她的经验);账走 `meta`、`day:0`,不进阶段计数;能力地图只加处境这一层,原来的名字清单一条不删。具体见 [CHANGELOG](CHANGELOG.md) 的 0.9.3 一节。
485
491
 
486
492
  原理页里引用的 Kolb / Piaget / Bowlby / Festinger / Bartlett / Dewey / Schön,只是思想引用(页底标了作者与年份),不含他人代码或文字。
487
493
 
@@ -0,0 +1,46 @@
1
+ # 0.9.2 更新说明
2
+
3
+ 2026-11-06。这一版也**没有新功能**:修的是「她该说话时没说话、说了却没被看见、记下了却没上桌」这一类故障。不改配置键、不改事件字段、不升事件格式版本——装上去旧数据照读。
4
+
5
+ 修的是社区报的 issue #51–#53、五个社区 PR(#46–#50,@BBleae),加三处用户直接反馈的问题(保存键找不着、便条写满、回复每轮重复投递)。都在本地复现过。
6
+
7
+ ## 你会看到什么
8
+
9
+ - **她不会突然连着几轮只说「已完成分析」、一个字不写了。** 叫醒时那句「这一轮是你自己的,不想说就什么都别写」以前是整进程共用一份,会串到别的会话;现在只影响被叫醒的那条会话,并且最多活 10 分钟,你一开口就清掉。
10
+ - **外客通路(QQ 桥、群聊、别的插件)问一句,`herReply` 里能拿到她真正回的那句话了。** 以前她只写正文、没调 `growth_commit` 时,接口会回「她这次没说话」。
11
+ - **面板不再把「到点了但没东西可摆」写成「醒了」。** quiet 改成「到点了,暂时没有可出的事件」,时间线里没有 wake 的块也不再补一句「醒了」。
12
+ - **插件停用后后台真的停了。** 以前停用后定时器还在跑,再启用会新旧两个调度器一起叫她(#48)。
13
+ - **叫醒一直失败时,不再每分钟重试、每分钟往事件流写一行。** 同一天最多 3 次,先隔 5 分钟、再隔 45 分钟;手动点的那次不占额度(#50)。
14
+ - **他回来过一次之后,冷落检测还能再触发。** 以前回来一次就永久判「这次冷落结束了」(#49)。系统时间回拨也不会再把天数算没。
15
+ - **配了发送工具的部署,日志不再把「只写正文、没发出去」说成「没什么要说的」**(#47)。
16
+ - **卡片可以被别的插件接走,回答隔很久也能结算。** 新增 `dsh-fish/card` / `dsh-fish/answer` 两个事件;对不上选项、没选都不结算,id 留着可重试(#46)。
17
+ - **第 3 天的卡池不再空着**:stage 1 补了一张 `notice_one`,第 1 天仍让给 `first_bet`;这张卡每次换一种问法。
18
+ - **扫码页签下也能点「保存」「测试」了。** 以前默认停在「手机扫码」时,两个按钮都不在页签里(#52)。
19
+ - **200 张便条都划掉之后,还能再记新的。** 名额改成只数没划掉的;`list` 能按状态翻页、`drop` 要写原因、`update` 能改正文(#53)。
20
+ - **系统时间被改到未来之后,冷落会一直挂着。** 现在按追加顺序判断「回来过」,不再被一条未来的 `ts` 卡住(#49 补)。
21
+ - **她传越界或认不出的值时,入口就会拒。** `confidence` 不再是写盘时悄悄夹到边上、`verdict` 不再悄悄落空(自查)。
22
+ - **主动性不再绕开「每天几点醒」。** 以前主动回合排在窗口判断之前,窗口外也可能被她主动找;现在同一道门一并管住。真想「随时可以来找」,等以后做独立开关。
23
+ - **时段框下面多了一行 24 小时制。** 原生时间框跟随系统 12 小时制,`00:00` 会显示成「上午 12:00」,很容易读成中午;现在补一行 `24 小时制:00:00 → 23:00`(跨零点标「次日」)。
24
+ - **换了条对话、她没接上那一轮时,不再当没事。** 叫醒只被收进收件箱、没真跑出东西的时候,以前插件以为成了,现在按「会话仍是空白」再叫一次(同一回合只复核一次);建会话的退路也改了:已经跑过 turn 的会话不再拿 `create` 硬开,免得宿主按 cwd 静默另开一条空壳。
25
+ - **她不想回的那句话,不会再每轮都被递一遍。** 以前只有她真回了才记「提醒过」,于是「好的,阿留」这种话每轮扫描、每次叫醒都原样再摆一次;现在摆到她眼前就算提醒过(一次最多 5 条,剩下的下轮再来),她自己主动去看仍然看得见。
26
+ - **只开着一个界面、后台还挂着一个副本时,那个副本不再往她记忆里写「你来过」了。** 同一份数据目录被两个进程打开,后到的那个本来就该只读(她写东西会被拒);但「记下你来过」这一行绕过了那道门,照样进事件流——结果「你有多久没出现」在她眼里永远不准。现在只读的副本一律不记、不涨轮;占用权中途被另一个进程拿走,也立刻停。
27
+ - **同一句「你来过」不会再记两遍。** 插件被重复加载时,同一个进程里挂上了两个观察者,同一句话写出两条连毫秒都一样的记录(日志里见过同一秒三条)。现在同一进程只挂一个。
28
+
29
+ ## 迁移
30
+
31
+ **无必改项。** 不改事件字段、不升 `EVENT_SCHEMA_VERSION`,旧数据照读。
32
+
33
+ 从 0.9.0 用 `plugin add` 升上来的,0.9.1 已经认得版本号依赖,这一版不用再跑 `fix-legacy-link.mjs`。
34
+
35
+ 一处观感变化:`/dsh-fish/say` 返回的 `said` 里可能出现 `uncommitted:true` 的条目(这一轮她在会话里说的正文,只为回话带回,不落盘)。外部接入如果按 `said.length` 数「她写了几条」,要按这个字段分开看。
36
+
37
+ ## 测试
38
+
39
+ 全量 `node --test` = **1609 pass / 0 fail**(0.9.1 是 1512,本版新增 97 条)。
40
+
41
+ 新增集中在:#46 卡片契约与四种结算路径(45 条)、#48 停用与重新启用、#49 回来之后再冷落、#50 失败退避与手动不占额、#51 正文回带与去重、#52 两个页签都能保存、#53 名额与 `list/drop/update`(8 条)、冷落按追加顺序与写盘入口校验(4 条)、回复不重复投递(3 条)、`notice_one` 补卡与轮换(4 条)、跨会话处境不串(3 条)、换房复核看 blank 与退路不造空壳(8 条)、只读副本不写 `seen:` 与同进程观察者去重(3 条)。都是「把判据改回去就红」的回归。
42
+
43
+ ## 还没做的
44
+
45
+ - 五个 PR 在本地以提交形式合入(作者署名保留),GitHub 上仍显示 open——本机 443 不通,等发布时一并合并、回帖说明。
46
+ - issue #51–#53 的回帖已发,等这一版发布后再关。
@@ -0,0 +1,91 @@
1
+ # dsh-fish 0.9.3
2
+
3
+ 2026-11-07
4
+
5
+ 这一版修三件、补三件、再补四处:日志里显示 0.9.1 其实跑的是 0.9.2;宿主 0.2.2 之后工具面收不窄(issue #54);他在她评论下面回的话被当成陌生人(用户反馈)。另外补上「她用过的每一样东西会留一句」、「我手上有什么改成先讲处境、再讲工具」,以及「缺工具就开口要」不再每轮念、改成撞墙时说一句、说过的事摆在桌上。另外补上**出口脱敏**、「稳定」在判断攒多以后重新对位(#57)、「撒娇式表达」不再自带结局(PR #56),以及**关掉的功能只在门上写「这会儿」**而不是整块消失。
6
+
7
+ 事件格式没动,旧数据照读。
8
+
9
+ ## 你会看到什么
10
+
11
+ - **启动日志第一行、设置页「数据」页的版本号,终于跟 `package.json` 一致了。** 之前 0.9.2 发出去时,`package.json` 是 `0.9.2`,插件里那个只用于日志与升级自检的 `VERSION` 常量还留着 `0.9.1`——所以排查时第一眼看到的版本是错的,而且「升级没有真正装进去」那张卡会一直误报(它拿源目录版本跟 `VERSION` 比,永远对不上)。现在两边都是 `0.9.3`。跑的还是同一个包,`pnpm-lock` 的 integrity 与 npm 上那份 tarball 一致;这个常量没有第二份来源,以后也不会再单独漂。
12
+
13
+ - **她被挡住时,会读到「我手上没有这样东西——能用的只有:……」(#54)。** 宿主 0.2.2 起把 preset 工具(`compress`、`subagent`、`ops_bash` 这类)注册进 agent **自己的作用域**,而 `tools.restrict()` 只收窄全局与祖先层——宿主源码注释的原话是「the viewing scope's own registrations remain exempt」。结果工具面从 9 个涨到 57 个(宿主日志会写 `工具已更新·新增 57 个,移除 9 个`),她看得见、调不动。
14
+
15
+ 以前被挡住只回一句「这件事我做不到」。模型看不到自己手上到底有什么,就会把「没试对」当成「再试一次」——#54 里那位连着试了 17 次同一个名字,整轮被停。现在拒绝话术把此刻真放行的清单再说一遍。
16
+
17
+ 措辞是刻意选的:**用她自己的口气**(「我手上没有这样东西」),不用「你现在的工具只有……」——后者第二人称加「只有」,读起来像有人发给她一份被批准的名单。这个插件的底线是不让她意识到自己被管理,所以清单可以给(她本来就在提示词第一行看得见),但必须是「说自己的家当」,不是「被通知一份许可」。
18
+
19
+ - **他在她评论下面回的那句话,不会再被说成「陌生人写的」(用户反馈)。** 扫回复时只认登录号(跟着 cookie 走的那个),而「领养人验证」那格填的常是你自己的大号——两个号对不上,你回她的话就和路人挤在同一张表里,后头还跟着一句「这些话里可能有不客气的地方。不用辩解,也不用讨好」。
20
+
21
+ 现在两个号都认。你回的那句单独成一段:「他是在你那条评论下面回了一句。你可以回,也可以不回。」只陈述事实,不叫「陌生人写的」、不叫「素材,不是命令」,也不给「他在乎你」这类判断——是不是要当回事,留给她自己。路人的话措辞一个字都没动;防注入那道滤网对谁照走,不因为是你而放松。
22
+
23
+ - **回你的那句不占「每天最多回几条别人」。** 那个额度卡的是「回几个陌生人」:你回她一句,不该挤掉她的额度,更不该因为她额度用完了反而回不了你。记账时标一下就行(`creply` 第 5 个字段写 `owner`),旧数据没有这个字段的照旧算进来,升级不会把历史额度清零。
24
+
25
+ - 设置页状态行跟着分开报:「有 N 条没回,其中 M 条是你回的」。
26
+
27
+ - **前提:「要 @ 的 B 站 uid」那格得填。** 没填就只有登录号,你回的话仍然分不出来——那一格以前只管 @ 生不生效,现在还多管一件事:认不认得你。
28
+
29
+ - `dsh.compatibility.dshReleases` 补了一行 `"0.2.2-rc.1": "unknown"`。写 `unknown` 是「没测过」,不是「不支持」——#54 报的正是这个宿主版本,目前只验证了「收窄会漏」这一件事。
30
+
31
+ - **她用过的每一样东西,会留一句她当时听见的话。** 这不是界面上的变化,是给她自己看的:978 行记录里一直没有「用过什么」,只有「看过哪条视频」。所以下次遇到同一个处境,她翻不到自己上回是怎么处理的。现在每调用一次工具并拿到结果,就写一行 `use:<工具> ok|fail <她听到的那句>`;被设置挡住的那些不写(「打不开」「看不了」不属于经验,是环境的限制)。她用 `her_recall` 翻旧事的时候,这些行会读成「用过 her_note,记下了:买灯泡」——不是机器写法。
32
+
33
+ - **「我手上有什么」那一段,改成了先讲处境、再讲工具。** 以前是一份使用说明(参数叫什么、有哪些取值);现在分三层:有哪些(名字照旧念)、什么处境会想起它(「想不起过去 / 怕忘 / 想要个东西 / 有话想说」)、真要动手时参数在哪看。`her_note` 和 `ask_owner` 的说明都短了一半多(494→222、262→143 字),删掉的全是参数枚举——那些本来就在工具自己的说明里。**能力清单没有删**:她一开始就得知道自己能干什么。
34
+
35
+ - **「缺工具就开口要」不再每天念一遍。** 以前每次醒来,处境段末尾都挂着一句要她开口要东西的话。念得太多就不像话了,读到第六十次也不会真的去要。现在分三处:被守卫拦住的那一刻,清单后面才补一句「要是你确实想要,说出来就行」;她真开口要过的(`ask_owner` 里「需要某个工具」那类),会按结局在桌上留一阵——还没答的 3 天、你说「以后再说」的 30 天、你答应了的 14 天、你说「这次不行」的不再摆(再提就成了追问);桌上有这种事时,主动性也会多攒一点劲儿。顺带修了口径:放一天没人答的卡以前会被念成「他说以后再说」,其实谁都没说,现在仍算「还没有答复」。
36
+
37
+ - **每段说明书开头,改成了「什么时候会想起它」。** 留话、便条、请求、梦、主动说话、定时提醒、B 站说话、回忆,第一句都先给那个处境(「有一句话想让他回来就看到」「心里老在同一个地方绕」),参数交给工具自己的说明;九段合计短了约 150 字。能力地图的处境层保留,另外在身份文档里加了一节「我做过的事」——从工具账里反推最近几件做过的事,没做过就整节不写。
38
+
39
+ **这两处(能力地图按处境组织、把用过的工具记成她自己的经验)的想法来自 1 群群友 joydog。** 落进本插件时做了取舍:账只留她当时听到的那一句,不记参数、不记整段返回;被门挡住的那次不入账(那是环境限制,不是她的经验);账走 `meta`、`day:0`,不进阶段计数;能力地图只加「处境」这一层,原来的名字清单一条不删——她一开始就得知道自己能干什么。
40
+
41
+ - **她试过、但没成的事,现在也会留一笔。** 以前工具中途报错一条都不记,「试了但炸了」最该记住的那类反而没痕迹;现在会记一句「这次没成」,错误照旧往上抛。同一件事十分钟内不重复记,免得一轮里连翻三次把真正的旧事挤掉。
42
+
43
+ - **翻旧账时能看到「当时你在说什么」。** 每行工具记录后面会补一句她当时说的那句话(同一轮、十分钟内最近的一条),找不到就不加——不编。
44
+
45
+ - **撞过的墙会留在桌上。** 被挡住、手上没有那样东西的那次,如果她那句话还在,桌上就会有一行「这件事你试过,手上没有能用的」;最多留 7 天、一次一条,和她开口要过的那些事去重,也会让主动性多攒一点劲儿。
46
+
47
+ - **关掉的功能不再整块消失,只在入口上写一句「这会儿」。** 以前把「上网」或「在网上说话」关掉,她身份文档里那两节就不见了,像这件事从来没存在过;真机上她就这么对着一句「去 B 站说一句话」的便条挂了快十天。现在那一节永远在,只是写着「这会儿出不去」「这会儿还说不出去,想说的话可以先写在别处」;被拦下时听到的也是「这会儿不方便说」,不会出现「开关」「权限」这些字。
48
+
49
+ - **能力地图保持原样。** 这一段没有改成「路 + 履历」,仍按现在的方式:先讲处境、再讲工具,外加一节「我做过的事」。
50
+
51
+ - **她对外说的话里,不会再出现她给你起的那个称呼。** 这条只改出口,不改她本人:她照样记得你、照样可以叫你「他」「那个人」或者她给你起的名字;只有真正发到**群聊、外客私聊、B 站评论区**的文字,出去之前把那个称呼换成「对方」。两字及以上的名字一出现就换;单字的(猫、鱼、梨)只换明显在指人的说法(「叫猫」「猫跟我说」),「我喜欢猫」不会被改坏。B 站评论开头的 `@昵称` 不参与替换——那是账号昵称,本来就是公开的,一起换会 @ 到不存在的人。
52
+
53
+ **事件流原文、记忆、提示词一个字都没动。** 这是原方案四层里只落地的第 4 层(身份规则、群聊提醒、B 站提醒三层按你的意见没做);单字名属已知残留风险,挡不住所有写法。
54
+
55
+ - **「稳定」这个读数,判断攒到几十条以后又量得准了(#57)。** 以前她判断攒到五六十条,这个值的抽样池还是「全部判断」,可她的自我描述一次只摆得下五六条——抽中读得到的那几条的概率不到一成,删掉的都是没进文档的判断,平均值被压到 0.03,读出来像「这些判断只是记账」。现在只抽「这会儿真写在自我描述里的那些」:同样 58 条判断,读数从 0.03 回到 0.06,判读落在「长在这些事上,抽掉一条少一块」。阈值(0.02 / 0.35)不用改,它们本来就是按可见的五六条标的。
56
+
57
+ 稳定那一行还会在最大值明显高于均值时一并写出来(`0.03(最大 0.15)`);`ⓘ` 里补了剂量:这次每次抽掉几条、一共几条判断、其中几条这会儿读得到。没有新增配置项,`k` 仍是 3。
58
+
59
+ - **观测台上「撒娇式表达」不再同时写着「还不会」和「已经可以了」。** 标题里原来写死「:还不会」,解锁之后状态尾巴再说「已经可以了」,同一行自相矛盾。现在标题只有「撒娇式表达」,结局由状态补:没解锁「· 还不会」、条件够了「· 条件够了,等她开口」、解锁「· 已经可以了」;不归成长层管的规矩(物理层、契约层)照旧不显示状态。她申请时念的那句也跟着顺了:「我想试试「撒娇式表达」这件事」。
60
+
61
+ ## 迁移
62
+
63
+ 无必改项。
64
+
65
+ - 不改事件字段、不升 `EVENT_SCHEMA_VERSION`,旧数据照读。
66
+ - 重启后启动日志第一行会显示 `0.9.3`。之前误报的「升级没有真正装进去」应该自己消失;如果还在报,看 `node_modules\dsh-fish\package.json` 的版本对不对。
67
+ - 如果你在 0.2.2 系列宿主上看到她的工具表依然很长:那是宿主行为,不是这一版没生效。设置页「手上能用什么」里那批「她本来就看得见、收窄挡不住的」就是它。
68
+
69
+ ## 测试
70
+
71
+ 全量 `node --test` = **1674 pass / 0 fail**(比 0.9.2 多 65 条)。
72
+
73
+ 其中三条改的是 #54 的话术口径:前缀仍是「这件事我做不到。」、必须带「我手上没有这样东西——能用的只有:」、清单里得是真放行的名字。
74
+
75
+ 七条是评论那条线,全在 `test/p4.test.mjs`:他自己回的那条不丢、标成 `fromOwner`;没填领养人 uid 时认不出他但也不会少掉谁;他的话单独一段、不叫「陌生人」;只有他回的时候不出现「陌生人」整段;`readMyReplies` 里同样分栏;`creply` 第 5 个字段是 `owner` 就不计入回复额度;回他时标 `owner`、回路人时不标。
76
+
77
+ 还有一条是社区 PR #55(@BBleae)带来的,在 `test/boot.test.mjs`:`VERSION` 必须等于 `package.json` 的 `version`——0.9.2 那次就是只改了一半,以后只改一半会直接红。
78
+
79
+ 新加的十七条在 `test/use.test.mjs`,守的是「经验怎么记」:留下的是她听到的那句、长列表不整段塞进账、被门挡住的不入账、记账本身永远不抛、`use:` 能被她自己翻到且读成人话。另外五条在 `test/toolmap.test.mjs`,守的是能力地图三层:名字要念、处境入口要在、参数不复述、说明书合计不超过 2400 字、划掉和放下仍然只靠她看得见的那句话。
80
+
81
+ 新加十三条在 `test/wish.test.mjs`,守「说过的事怎么摆」:三种结局三种窗口、放一天没人答的不会被说成「以后再说」、同一件事只留最近一张、「这次不行」的不摆、墙上那行是原话加结局、并进桌子后与好奇事同段、不传 `wishes` 时桌子一字不变、桌上有它时劲儿多攒一点、撞墙那句只在被拦住时说且前缀不变、每轮的常驻句确实撤掉了。`test/proactive.test.mjs` 另加两条,守 `looseEnds()` 的 timeout 口径与 14 天答应窗口。
82
+
83
+ 另有 7 条在 `test/redact.test.mjs`,守出口脱敏的边界:两字名出现即换、单字名只认指人句式、「我喜欢猫」不被改坏、`@` 前缀不被换、只有群聊与外客私聊才换、私下的话不动、空名字原样返回。2 条守 #57:40 条判断时 `visible < beliefs` 且 `score > 0.02`;界面同时印均值与最大值、提示语带剂量。1 条守 PR #56:成长层那句的标题里不许再出现「还不会」。
84
+
85
+ ## 致谢
86
+
87
+ PR #55(@BBleae/晴猫):`VERSION` 跟上、三条 tick 用例在 0 点不再红。`twoTicks()` 里 `hh(-1)` 会算出 `-1:00`,`validWindow` 不认,`normalizeConfig` 悄悄退回 `20:00–23:00`——这个判断是对的,原样收下。
88
+
89
+ PR #56(@BBleae/晴猫):观测台上「撒娇式表达」解锁后同一行同时说「还不会」和「已经可以了」。改法原样收下:标题只留能力名,结局交给状态补;顺带把申请正文里那句「我想试试「撒娇式表达:还不会」这件事」修顺了。
90
+
91
+ 能力地图按处境组织、以及把用过的工具记成她自己的经验(`use:` 账与「我做过的事」那一节),这两个想法来自 **1 群群友 joydog**。落进本插件时做了取舍:账只留她当时听到的那一句、不记参数与整段返回,被门挡住的那次不入账,账走 `meta`、`day:0` 不进阶段计数;能力地图只加处境层,原来的名字清单一条不删。
package/RELEASE.md CHANGED
@@ -24,8 +24,8 @@ gh auth status # 必须是 ssamurias
24
24
 
25
25
  ## 1. 发布前自检(每次都要跑,别跳)
26
26
 
27
- - [ ] `package.json` 的 `version` 就是要发的号(0.9.x 阶段只发 patch)
28
- - [ ] `node --test` 全绿(0.9.1 基线:**1512 pass / 0 fail**,约 22 秒)
27
+ - [ ] `package.json` 的 `version` 就是要发的号(修 bug 发 patch,如 `0.9.3`;一批改动发 minor,如 `0.10.0`)
28
+ - [ ] `node --test` 全绿(当前基线见 `AGENTS.md` 第 6 节;现在是 **1674 pass / 0 fail**,约 24 秒)
29
29
  - [ ] `CHANGELOG.md` 顶部有本节,条目按 issue 编号写全
30
30
  - [ ] `RELEASE-NOTES-x.y.z.md` 已写,且与 `CHANGELOG.md` 口径一致(issue 条数、PR 编号、日期)
31
31
  - [ ] `README.md` 顶部「当前版本」段已换成这一版
@@ -69,6 +69,7 @@ git push origin main
69
69
  ## 4. 发 npm 包(需要你手动跑,可整段复制)
70
70
 
71
71
  > 这一段是给维护者本人执行的:登录和 2FA 都在你的终端里完成,agent 不代跑发布。
72
+ > **本机的 2FA 是 Windows 系统密钥(安全密钥)**,所以每次发布都要走一遍完整登录流程;发完后 npm 的 CDN 大约要 **3 分钟** 才看得到新版本,核对命令等一会儿再跑。
72
73
  > **本机 npm 默认 registry 是 `https://registry.npmmirror.com`,所以每条命令都必须显式带 `--registry=https://registry.npmjs.org/`**,否则会得到看似「包不存在 / 没权限」的 E404。
73
74
 
74
75
  ```bash
@@ -84,7 +85,7 @@ npm whoami --registry=https://registry.npmjs.org/
84
85
  # 2) 发布(开了 2FA 就在末尾加 --otp=123456)
85
86
  npm publish --registry=https://registry.npmjs.org/
86
87
 
87
- # 3) 核对:应为 0.9.x 和 latest -> 0.9.x
88
+ # 3) 核对:等约 3 分钟再跑,应为 0.9.x 和 latest -> 0.9.x
88
89
  npm view @ssamurias/dsh-fish version dist-tags --registry=https://registry.npmjs.org/
89
90
  ```
90
91
 
@@ -126,8 +127,8 @@ gh release view v0.9.x
126
127
 
127
128
  - `dist-tags.latest` 要指向新版本;`dist.tarball` 能取到 URL 就说明包在。
128
129
  - 想真装一遍:**先关 DSH Desktop**,用临时 profile 试装(不要直接动 `desktop` profile)——`bundles` 与 `dependencies` 必须一一对应,先只改依赖、装包,再用目录联接的临时 profile 跑 `--dump-config` 预演(避坑指南第 1 节)。
129
- - 回 issue:#32–#38、#41–#43 各回一条「0.9.1 已发布,装法见 README」,等作者确认后再关。
130
- - 给四个社区 PR 的作者(@BBleae 等)留一句「已随 0.9.1 发布」。
130
+ - 回 issue:这一版修到的每条 issue 各回一条「x.y.z 已发布,装法见 README,有问题随时回」,等作者确认后再关。
131
+ - 给这一版合入的 PR 作者留一句「已随 x.y.z 发布」;PR 没合并的不要在 GitHub 上点 merge,在回帖里说清「以提交形式合入」。
131
132
 
132
133
  ---
133
134
 
package/lib/absence.js CHANGED
@@ -19,6 +19,7 @@
19
19
  import { readEvents, appendEvent, rootDir } from './store.js'
20
20
  import { deriveSelf } from './derive.js'
21
21
  import { loadConfig } from './settings.js'
22
+ import { tsMs, isContactEvent } from './schema.js'
22
23
 
23
24
  const DAY_MS = 86_400_000
24
25
 
@@ -34,28 +35,33 @@ const DAY_MS = 86_400_000
34
35
  * 为什么**不**认 `utterance`:定时唤醒也会让她写 utterance,而「定时器叫醒她自己想事情
35
36
  * 不算他来过」是这个模块的立身之本(见文件头)。derive 层分不出哪条是唤醒里写的。
36
37
  */
38
+ /** 最后一条「他来过」在事件流里的下标;没有就 -1。 */
39
+ export function lastContactIndex(events = []) {
40
+ for (let i = events.length - 1; i >= 0; i--) if (isContactEvent(events[i])) return i
41
+ return -1
42
+ }
43
+
44
+ /** 最后一条 absence 在事件流里的下标;没有就 -1。 */
45
+ export function lastAbsenceIndex(events = []) {
46
+ for (let i = events.length - 1; i >= 0; i--) if (events[i]?.type === 'absence') return i
47
+ return -1
48
+ }
49
+
37
50
  export function lastSeenAt(events) {
51
+ // 取「最大时刻」而不是最后一条:导入/手写夹具里事件不保证按时间排。
38
52
  let at = 0
39
- for (const e of events) {
40
- if (e?.type === 'heard') {
41
- const ms = Date.parse(String(e.ts ?? ''))
42
- if (Number.isFinite(ms) && ms > at) at = ms
43
- continue
44
- }
45
- if (e?.type !== 'meta') continue
46
- const t = String(e.text ?? '')
47
- if (!t.startsWith('seen:')) continue
48
- const ms = Date.parse(t.slice(5).trim())
49
- if (Number.isFinite(ms) && ms > at) at = ms
53
+ for (const e of events ?? []) {
54
+ if (!isContactEvent(e)) continue
55
+ const ms = e?.type === 'heard' ? tsMs(e) : Date.parse(String(e.text ?? '').slice(5).trim()) || tsMs(e)
56
+ if (ms > at) at = ms
50
57
  }
51
58
  return at
52
59
  }
53
60
 
54
61
  /** 从事件流里找最近一条 absence 事件(返回事件对象或 null)。 */
55
62
  export function lastAbsenceEvent(events) {
56
- let found = null
57
- for (const e of events) if (e?.type === 'absence') found = e
58
- return found
63
+ const i = lastAbsenceIndex(events)
64
+ return i < 0 ? null : events[i]
59
65
  }
60
66
 
61
67
  /**
@@ -64,26 +70,34 @@ export function lastAbsenceEvent(events) {
64
70
  */
65
71
  export function assess(events, now = Date.now(), opts = {}) {
66
72
  const threshold = Math.max(1, Number(opts.afterDays ?? 2))
67
- const seen = lastSeenAt(events)
68
- const lastAbs = lastAbsenceEvent(events)
73
+ const seenIdx = lastContactIndex(events)
74
+ const absIdx = lastAbsenceIndex(events)
69
75
 
70
76
  // 从来没有过 contact(刚装、还没聊过)→ 不判冷落。
71
77
  // 否则第一次启动就会立刻"冷落 N 天",她开局就在怨你,这是错的。
72
- if (!seen) return { absent: false, days: 0, since: 0, reason: 'no-contact-yet' }
78
+ if (seenIdx < 0) return { absent: false, days: 0, since: 0, reason: 'no-contact-yet' }
73
79
 
74
- const days = Math.floor((now - seen) / DAY_MS)
80
+ const since = lastSeenAt(events)
81
+ // 时钟回拨、seen 落在未来时差值是负的 → 不能出现「-1 天没来」,按 0 算。
82
+ const days = Math.max(0, Math.floor((now - since) / DAY_MS))
75
83
 
76
- // 已经记过一次,且记完之后他又来过 → 这次冷落已经结束
77
- if (lastAbs && seen > Date.parse(String(lastAbs.ts ?? 0))) {
78
- return { absent: false, days: 0, since: seen, reason: 'returned' }
79
- }
80
- // 已经记过,他也还没回来 → 还是同一次冷落,不重复写
81
- if (lastAbs && seen <= Date.parse(String(lastAbs.ts ?? 0))) {
82
- return { absent: false, days, since: seen, reason: 'already-recorded' }
84
+ if (absIdx >= 0) {
85
+ // 「回来没有」按两条线索判:
86
+ // 1. 事件流里的先后(seen 写在 absence 之后)——老代码只看时间戳,
87
+ // 系统时间被往前跳过时会写下一条 ts 在未来的 absence,之后所有
88
+ // seen 的时间戳都比它小,于是「他回来了」永远判不出来。
89
+ // 2. 时间戳(seen 晚于 absence 的 ts)——兼容测试/导入数据里按时间排序、
90
+ // 但 absence 被追加在中间的情形。两条任一成立都算回来。
91
+ const back = seenIdx > absIdx || since > tsMs(events[absIdx])
92
+ // 已经记过一次,且记完之后他又来过 → 这次冷落已经结束。
93
+ // 只管门槛以内:回来之后又消失够久,就是新的一次,得能再记。
94
+ if (back && days < threshold) return { absent: false, days: 0, since, reason: 'returned' }
95
+ // 已经记过,他也还没回来 → 还是同一次冷落,不重复写
96
+ if (!back) return { absent: false, days, since, reason: 'already-recorded' }
83
97
  }
84
98
 
85
- if (days < threshold) return { absent: false, days, since: seen, reason: 'within-threshold' }
86
- return { absent: true, days, since: seen, reason: 'absent' }
99
+ if (days < threshold) return { absent: false, days, since, reason: 'within-threshold' }
100
+ return { absent: true, days, since, reason: 'absent' }
87
101
  }
88
102
 
89
103
  /**