@ssamurias/dsh-fish 0.8.9 → 0.9.0

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.
Files changed (49) hide show
  1. package/AGENTS.md +32 -9
  2. package/CHANGELOG.md +84 -2
  3. package/LICENSE +7 -0
  4. package/README.md +64 -17
  5. package/RELEASE-NOTES-0.8.0.md +87 -0
  6. package/RELEASE-NOTES-0.8.1.md +93 -0
  7. package/RELEASE-NOTES-0.8.2.md +57 -0
  8. package/RELEASE-NOTES-0.8.3.md +71 -0
  9. package/RELEASE-NOTES-0.8.4.md +47 -0
  10. package/RELEASE-NOTES-0.8.5.md +244 -0
  11. package/RELEASE-NOTES-0.8.6.md +224 -0
  12. package/RELEASE-NOTES-0.8.7.md +202 -0
  13. package/RELEASE-NOTES-0.8.8.md +224 -0
  14. package/RELEASE-NOTES-0.9.0.md +267 -0
  15. package/THIRD-PARTY-NOTICES.md +3 -0
  16. package/lib/away.js +7 -2
  17. package/lib/channels.js +53 -13
  18. package/lib/client.js +99 -49
  19. package/lib/compact.js +26 -1
  20. package/lib/derive.js +10 -6
  21. package/lib/desk.js +1 -1
  22. package/lib/dream.js +3 -0
  23. package/lib/dreamtable.js +15 -0
  24. package/lib/drive.js +44 -7
  25. package/lib/freewake.js +17 -1
  26. package/lib/home.js +54 -2
  27. package/lib/inbox.js +7 -2
  28. package/lib/index.js +8 -12
  29. package/lib/interest.js +23 -3
  30. package/lib/log.js +22 -1
  31. package/lib/look.js +2 -2
  32. package/lib/note.js +35 -13
  33. package/lib/notify.js +2 -0
  34. package/lib/observe.js +13 -7
  35. package/lib/outing.js +2 -0
  36. package/lib/persona.js +2 -1
  37. package/lib/proactive.js +12 -7
  38. package/lib/say.js +104 -58
  39. package/lib/scheduler.js +67 -37
  40. package/lib/selfcheck.js +2 -3
  41. package/lib/selfdoc.js +3 -6
  42. package/lib/selfupdate.js +176 -41
  43. package/lib/settings.js +28 -2
  44. package/lib/turns.js +41 -3
  45. package/lib/update.js +14 -0
  46. package/lib/upgrade.js +19 -3
  47. package/lib/voices.js +2 -0
  48. package/package.json +4 -2
  49. package/scripts/fix-legacy-link.mjs +147 -0
@@ -0,0 +1,71 @@
1
+ # 大肥鱼 0.8.3 更新说明
2
+
3
+ > 补丁版,**没有新功能,五处修复**。最要紧的一条是设置页被一个早就没有的工具名挡住了整页——改哪个开关都存不进去;其余是记录不对、按钮空转、拒绝理由说不清,让你照着提示去做却还是不行。
4
+ > **没有需要你改配置的地方,装上就行。**
5
+
6
+ ## 一、设置页因为一个不存在的工具名,整页都存不进去
7
+
8
+ **你会看到:** 改任何一个开关都提示「这一项没存上」,技术信息里写「这些工具名不存在:delete」。把白名单里那个名字删掉,其它设置立刻又能存了。
9
+
10
+ 原因是 `POST /config` 的校验:只要「插件能力白名单」里有一个宿主上不存在的名字,整张配置表就返回 400。而设置页的自动保存是**整表提交**,所以不只是白名单那一项存不进去——你改别的开关,也跟着一起失败。原来那句提示还说「改别的照样会存」,正好说反了。
11
+
12
+ 现在把两条路分开:
13
+
14
+ - **你在输入框里敲完、点「保存白名单」** → 名字打错了就当场拦下来,告诉你是哪个;
15
+ - **页面自动保存(把盘上的旧名单整表回传)** → 丢掉坏名字、其余照常存,服务端把丢掉的名字回给界面。
16
+
17
+ 设置页顶部会出现一张卡片,列出被跳过的名字,并给一个「从名单里清掉这些名字」按钮——只清草稿里还留着的字,不动别的设置。失败文案也改成「不是你填错了,已经保存的内容没丢」。
18
+
19
+ > 这一条来自 issue #14(@ming-fish)。他那边残留的 `delete` 是自己旧版本加进去的,宿主上一直没有这个工具;旧版本还允许整张表带着它存进去,于是升级后就卡住了。
20
+
21
+ ## 二、旁听一条都没记下:不是没生效,是永远轮不到那几轮
22
+
23
+ **你会看到:** 开了旁听,`events.jsonl` 里却一条 `heard` 都没有,像是功能根本没跑。
24
+
25
+ 查下来不是校验拒收、不是配置、也不是写盘失败——判断条件写的是「总轮数能被 3 整除」,而总轮数是**从装插件那天起算的**。旁听是 0.8.0 才装上的,装上时计数已经是 4,下一次能整除要等到第 6 轮;如果此后没有一次对话,就永远等不到。看上去就像没生效。
26
+
27
+ 改成按「离上次记过去了几轮」判断:上次记的轮次落在 `home.json` 里,超过 3 轮就记一句,记完更新。语义仍然是「每 3 轮一句」,但新装或刚升级时最多再等 3 轮就会记第一句。
28
+
29
+ ## 三、她该不该写,三处默认值被扳过来了
30
+
31
+ 三处提示词原来都偏向「不写也行」,现在改成「有想留下的就留下;确实没有,不写也行」——退路都留着,只是默认不再是「不必写」:
32
+
33
+ - 写判断的那个工具说明末尾;
34
+ - 回话提示的收尾(先回你,回完了再决定要不要顺手记一句);
35
+ - 身份文档里「我手上有什么」那一段(这句每轮都在,出门时还被身份快照原样带走,只改工具说明不改它,两处口气会互相抵消)。
36
+
37
+ 「不说话本来就是允许的」这半句是特意留下的——那是权利,不是默认值。
38
+
39
+ ## 四、手动点「唤醒」却回一句「今天不打扰」
40
+
41
+ **你会看到:** 设置页那个「现在叫醒大肥鱼」按钮有时点了没反应,只弹「没有满足条件的事件模板(今天不打扰)」。
42
+
43
+ 原因不是故障:手动唤醒和定时唤醒走的是同一套挑卡逻辑,手动只是把当天次数清零,模板的**冷却**和**前提条件**照样要过。早期阶段只有五张卡,且大多是「一生只用一次」「冷却两天」「要求连着两天没来」;这几条同时不成立时(比如刚聊完、天天都在),候选池就是空的——点多少次都只会被挡回来。
44
+
45
+ 现在把两种拒绝拆开:
46
+
47
+ - **卡池空** → 手动照样叫醒,但不派活,只给一句「现在没有待办,这一轮是你自己的——想做什么就做什么,也可以什么都不做」。它不占任何一张真卡的冷却。
48
+ - **当天次数用满** → 继续拒绝。那是「今天该不该自己醒」的配额,手动不该突破。
49
+
50
+ ## 五、被挡在「这个你还没看过」上的人,得知道该往哪使劲
51
+
52
+ 说话工具拒绝时原来只说「想看那一条,就先看一眼再说」。照着做却还是被拒——因为她翻的是别的视频、搜的是别的关键词,而闸门只认**这一条**。现在这句话点名两个能走通的动作(看指定那一条,或取那一支的画面),并提前说清「翻了别的、搜了别的,都不算这一条」。仍然不透露配额与开关。
53
+
54
+ ## 测试
55
+
56
+ `node --test`:**1090 个全绿**(0.8.2 是 1001 个)。新增的用例都挂在上面五条修复上:整表提交时坏名字丢掉且其余照存、旁听的新计数口径、卡池空时手动唤醒不空转、拒绝语里点名正确动作、旧文案不再出现。
57
+
58
+ ## 怎么升级
59
+
60
+ 1. 在 DSH 里打开「设置 → 插件」,检查更新,装 v0.8.3;或者
61
+ 2. 到 [GitHub 的 Releases 页面](https://github.com/ssamurias/dsh-fish/releases) 下载这次的源码包,解压后用「设置 → 插件 → 从本地安装」选那个文件夹。
62
+
63
+ > 这个插件**不走 npm**(`dsh-fish` 这个名字在 npm 上属于另一个项目),别用 `npm i dsh-fish` 装。
64
+
65
+ 装完**重启 DSH**(插件只在启动时加载)。
66
+
67
+ ## 出问题了怎么办
68
+
69
+ - 插件压根没出现:先跑 `node tools/check-install.mjs`,它会告诉你是不是副本有问题;
70
+ - 已经进来了但某个功能不对:看日志 `~\.dsh-fish\logs\dsh-fish.log`,第一行写这次启动的版本号;
71
+ - 反馈时带三样:版本号、截图、以及 `check-install.mjs` 的输出。
@@ -0,0 +1,47 @@
1
+ # 大肥鱼 0.8.4 更新说明
2
+
3
+ > 补丁版,**没有新功能,四处修复**。最要紧的一条是换会话之后面板永远停在 0 / N——你会以为她不记轮数了,其实从换会话那一刻起,你说的每句都被静默丢掉了。
4
+ > **没有需要你改配置的地方,装上就行。**
5
+
6
+ ## 一、换会话之后,面板永远停在 0 / N
7
+
8
+ **你会看到:** 日志里有一行「聊满 40 轮,换了个新会话」,之后再也没有任何记录。新会话里聊了几十轮,打开 `home.json` 还是 `"turns": 0`,面板一直显示 0 / 40。而且日志里**一个字都不会解释为什么**。
9
+
10
+ 三处问题叠在一起,缺一条照样坏:
11
+
12
+ **1. 新会话是空白会话,侧栏不显示它。** 宿主的可见性规则是「空白且不是当前会话 → 不显示」,而插件用 API 建的会话永远不会被设成 current。于是换完会话,你**根本看不见**新的那一条,自然还在旧的里继续说话。`/reset` 和「重新搭一间」早就把新会话叫醒过,唯独轮换这条路漏了——所以它俩一直好着,只有轮换坏着。现在换完会话会叫醒新会话(要等她这一轮动起来,侧栏才会显示)。
13
+
14
+ **2. 她的房间只认一条会话,其余静默丢弃。** 你留在旧会话里说话,每句都在「这条不是她家」那一步被丢掉了——不涨轮、不记原话、也不再换会话,而且不留任何日志。现在收窄条件认回来:如果 `home.json` 记着「刚被换下来的正是这条」,就把家搬回来,轮数接着涨。**不认任意会话**——认宽了等于谁在别处跟她说一句都能把她的家搬走。
15
+
16
+ **3. 换会话时整份覆盖 `home.json`,把别的字段抹了。** 原来只写七个字段,`preset`(她挑的那套权限组合)、`title`、`hearAt`(旁听落在第几轮)换一次会话全丢。表现是换完会话旁听节奏归零、下次重建会话时以为她从没被限过权。现在改成「先继承旧档,再盖掉该换的几项」。
17
+
18
+ **顺带修的一处:写盘偶发失败且不留痕迹。** 排查上面第 1 条时抓到的——Windows 上原子写把临时文件改名覆盖目标时,如果目标正被另一个句柄读(宿主读 `home.json`、杀软扫目录),会报 `EPERM`/`EACCES`/`EBUSY`,这不是权限问题,隔几十毫秒重试就好。以前这里不重试,失败被上层吞掉。实测新写的字段十次里有两三次没落盘,只在机器忙时复现。现在对这几个码做 5 次退避重试,穷尽后清理临时文件并抛出,不再静默。
19
+
20
+ > 这一条也来自 issue #14(@ming-fish)。他报的时候是 0.8.1,我一开始以为是没升级——查下来是这版代码里就有的真问题,跟版本无关。
21
+
22
+ ## 二、换会话提示里的会话名字写的是旧的
23
+
24
+ `/say` 换完会话会给一句提示「上一段还在侧栏里,叫『跟她说话·<日期> 之前』」。但那条会话从 0.7.x 起就叫「跟大肥鱼说话」了——照着这句去侧栏里找,找不到。现在两处一致。
25
+
26
+ ## 测试
27
+
28
+ `node --test`:**1094 个全绿**(0.8.3 是 1090 个)。新增 4 条都挂在上面的修复上:换会话之后要叫醒新会话且把「叫过了」记账落盘、人还在旧会话里说话能被认回来且轮数接着涨、认回只认刚换下来那条、轮换后 `preset` / `hearAt` / `title` 不能丢。
29
+
30
+ 修复前同一个测试文件连跑 10 次有 3 次失败(就是那个偶发丢写),修复后连跑 15 次全绿。
31
+
32
+ ## 怎么升级
33
+
34
+ 1. 在 DSH 里打开「设置 → 插件」,检查更新,装 v0.8.4;或者
35
+ 2. 到 [GitHub 的 Releases 页面](https://github.com/ssamurias/dsh-fish/releases) 下载这次的源码包,解压后用「设置 → 插件 → 从本地安装」选那个文件夹。
36
+
37
+ > 这个插件**不走 npm**(`dsh-fish` 这个名字在 npm 上属于另一个项目),别用 `npm i dsh-fish` 装。
38
+
39
+ 装完**重启 DSH**(插件只在启动时加载)。
40
+
41
+ **已经在 0.8.3 上踩过这个坑的话:** 升级后不用重置,`home.json` 里 `turns` 还是 0,下一轮正常涨起来;如果当时换下来的那条旧会话里还有你没说完的话,在那条里说一句,家会自己搬回去。
42
+
43
+ ## 出问题了怎么办
44
+
45
+ - 插件压根没出现:先跑 `node tools/check-install.mjs`,它会告诉你是不是副本有问题;
46
+ - 已经进来了但某个功能不对:看日志 `~\.dsh-fish\logs\dsh-fish.log`,第一行写这次启动的版本号;
47
+ - 反馈时带三样:版本号、截图、以及 `check-install.mjs` 的输出。
@@ -0,0 +1,244 @@
1
+ # 0.8.5 更新说明
2
+
3
+ 2026-11-01。四件事:**她读到的第一句话不再说「你是个 coding agent」**、**「她手边有什么」不再把她自己说死**、**「刚说过话,还不到时候」不再被系统记账骗**,以及**「(现在没有人跟你说话。)」不再出现在对话框里**(它还会连带把那一轮的回复弄丢)。
4
+
5
+ ---
6
+
7
+ ## 一、她那条会话的最前面,一直写着「你是个 coding agent」
8
+
9
+ ### 你会看到什么
10
+
11
+ 她那轮的系统提示词,开头是这样的:
12
+
13
+ ```
14
+ You are an AI agent powered by DeepSeek Harness.
15
+
16
+ You are a coding agent powered by the deepseek-flash model.
17
+ ```
18
+
19
+ 然后是一万多字的工具交代(怎么用 bash、怎么给子代理派活、怎么做 GitHub PR 自检……),**一直读到第 7554 个字符**,才第一次出现「## 你是谁」。
20
+
21
+ 也就是说:先告诉她「你是个 coding agent」,再讲七千字的工具用法,最后才告诉她「你是大肥鱼,你刚开始存在,没有人告诉过你为什么在这里」。
22
+
23
+ 顺序颠倒的不是文字,是她的自我认知。她只能先认下前面那句。
24
+
25
+ ### 为什么会这样
26
+
27
+ 宿主的系统提示词是**一叠纸按编号平铺拼起来**的,编号小的在前。**没有任何「后来者压过前者」的机制**——不是权重,就是排列顺序。
28
+
29
+ | 编号 | 那一段 | 谁写的 |
30
+ | --- | --- | --- |
31
+ | -1000 | You are an AI agent powered by DeepSeek Harness. | 宿主 |
32
+ | 0 | You are a coding agent powered by the model. | 宿主(部署配置) |
33
+ | 1000–3100 | 各工具说明 | 宿主 |
34
+ | **8500** | **## 你是谁(她的身份)** | **我们** |
35
+ | 10200 | persona 结尾 | 宿主 |
36
+
37
+ 那句 coding agent 来自宿主的部署配置(`dsh-web-app` 的 `cordis.patch.yml` 与 `presets/standard.patch.yml`,两处同文)。改它会影响这台机器上**所有** agent 的会话——不该动。
38
+
39
+ ### 为什么不用「给她单独一个 preset」
40
+
41
+ 这条路我们走过三次,三次都撞在同一堵墙上:宿主的 `presets.select()` 有一条规则——**会话只要跑过一轮,preset 就锁死了**(抛 `agent-preset/locked: This session has already started`)。会话是在插件里建的,建完立刻就开跑,所以永远赶不上。
42
+
43
+ ### 这次怎么解决的
44
+
45
+ 宿主在组装系统提示词的最后一步,留了一条**可以改写结果**的链(`system-prompt/assemble`)。宿主自己的三个插件也在用它(一个改模型选择、一个加文件引用、一个注入工作目录)。
46
+
47
+ 我们挂在**她那条会话自己**的链上,等宿主拼完了,把结果改两处再交回去:
48
+
49
+ 1. **摘掉** `deployment:persona-prefix`(就是那句 coding agent);
50
+ 2. 把**她的身份段提到紧跟第一行**的位置。
51
+
52
+ 改完她的提示词开头变成:
53
+
54
+ ```
55
+ You are an AI agent powered by DeepSeek Harness.
56
+
57
+ ## 你是谁
58
+ ...
59
+ ```
60
+
61
+ 只影响她那条会话。其他 agent、别的插件、你自己跟 AI 对话都不受影响。她身份的编号常量没动(还是 8500),挪的只是这一次拼出来的顺序。
62
+
63
+ **失败了会怎样:** 钩子上任何一步出问题,都会把宿主原来那份原样交回去——最坏情况是回到今天这个样子,不会让她一整轮读不到系统提示词。
64
+
65
+ ### 删掉「coding agent」她就不会编程了吗
66
+
67
+ 不会。**她在家本来就没有编程工具。**
68
+
69
+ 她手上只有十个:记录想法、翻自己的旧事、翻自己的日记、留一句话、刷 B 站、看视频画面、在评论区说话、有事问你同不同意、醒来记一句梦,加上宿主给的 `read_image`。
70
+
71
+ **没有 bash、没有读写文件、没有 grep 搜索。** 那句「你是个 coding agent」是她兑现不了的空话。删掉只减少矛盾,不减少真实能力。
72
+
73
+ ### 以后真想让她编程
74
+
75
+ 三条路,按推荐顺序:
76
+
77
+ 1. **让她出门。** 开到别的工作区那条会话时,工具面不收窄,那个工作区的全套工具她都能用——这是唯一已经在真机上验证过的路径。
78
+ 2. **在设置页白名单里填工具名。** 填 `ops_bash`、`read_file` 之类的宿主工具名,它们会跟着进她的 `fish:tools` 段。代价:她屋里的工作目录是 `~/.dsh-fish/home`,只能改自己家里的文件。
79
+ 3. **给她做一个专用工具。** 照 `bili_watch` 的样子包一个形状固定的工具(参数是视频号而不是任意路径),符合「不开通用口」的原则。
80
+
81
+ ---
82
+
83
+ ## 二、「手边有什么」不再把她自己说死
84
+
85
+ ### 原来是这样写的
86
+
87
+ ```
88
+ 我手边能用的东西只有这几样:growth_commit、her_recall、…
89
+ 工具表里如果还列着别的名字(比如带 ops_、team_、subagent 这类),那不是我的:别去调,也别把它们说成我会的事。
90
+ 真伸手去用,只会得到一句「这件事我做不到」,还白搭一轮。
91
+ 有人问我手上有什么,我按这几样说;没有的就直说没有,不要许诺。
92
+ ```
93
+
94
+ ### 问题在哪
95
+
96
+ 问题不在「能不能调」——边界本来就是真的,工具表里那些名字她确实够不着。
97
+
98
+ 问题在于:**这段话读起来像天花板。** 它把「此刻手上有什么」讲成了「我是什么样的人、能干什么」。
99
+
100
+ 后果是:你哪天在白名单里加了新工具,工具定义确实进了她的上下文,可她读到的那段还在说「你不是干这个的」,于是她会自己把新工具劝回去——**不是不能,是不肯**。
101
+
102
+ ### 现在怎么写的
103
+
104
+ 把两件事分开说:
105
+
106
+ - **名字照旧从当下的白名单现算**(本来就随白名单重算,不是写死的清单);
107
+ - **明说这是此刻、不是上限**:「这是我此刻的家当,不是我的上限——他随时能再添几样进来,添了我就照着用,不必先把自己劝回去。」
108
+ - **边界留着,但不叫「别去调」**:工具表里那些不是她的名字,「认得出,但不冒充」;真伸手会得到「这件事我做不到」——不是不肯,是够不着。
109
+ - 最后一句也改了:「有人问我手上有什么,我按**当时**有的说;以后变多了,就按变多了的说。」
110
+
111
+ 身份文档(`home/她的身份.md`)里的同一段跟着改,末尾加一句:
112
+
113
+ > 新东西进来不用重新认识自己:手上多了一样,就多一样能做的。
114
+
115
+ 设置页那句「大肥鱼自己可以调用的工具**只有八个**」也改成了「从她的白名单现算……**你在白名单里加了什么,下一轮就跟着进她手边,不用重装**」——界面不替她的成长画天花板。
116
+
117
+ ---
118
+
119
+ ## 三、她明明一整天没说话,主动开口却说「刚说过话」
120
+
121
+ ### 你会看到什么
122
+
123
+ 她那一整天一次都没自己开口。日志里反复出现同一句理由:
124
+
125
+ ```
126
+ proactive skip 刚说过话,还不到时候
127
+ ```
128
+
129
+ 可你回头看,她最后一次跟你说话是十几个小时以前,中间只是写了几次日记、刷了几条视频、系统记了几次「今天没有该问的事」。
130
+
131
+ ### 为什么会这样
132
+
133
+ 判断「安静了多久」的那个函数,取的是**整条事件流里最新的一条记录**,不分类型。而事件流里大部分记录不是话——本机 441 条事件里,`quiet:`(今天没找到模板)占 272 条,**61.7%**;真正她说的 `utterance` 只有 28 条,**6.3%**。
134
+
135
+ 于是「今天没找到模板」「她看了条视频」「到点写日记」这些系统记账,每写一次就把「安静了多久」清零一次。
136
+
137
+ ### 真机抓到的一次
138
+
139
+ 2026-10-30 22:03:07,日志写「刚说过话,还不到时候」:
140
+
141
+ - 插件算出来的 idle:**0.01 小时**(一分钟前);
142
+ - 那条记录是什么:`meta:watched`——她看了一条 B 站视频;
143
+ - 她真正没说话已经多久:**17.55 小时**。
144
+
145
+ 看视频不是说话,但旧代码读不出来。
146
+
147
+ ### 差多少
148
+
149
+ 拿本机事件流跑,醒着的时间窗(12:00–02:00,每 30 分钟采一次样):
150
+
151
+ | 配置里的 `minIdleHours` | 改之前每天能掷几次 | 改之后 |
152
+ | --- | --- | --- |
153
+ | 1(本机现在用的) | 13.3 | 17.3 |
154
+ | 6(默认值) | 9.3 | 15.3 |
155
+
156
+ 阈值调得越大,被吃掉的可掷点越多——因为记账行每天写、间隔又短,它几乎永远把时钟压在阈值以下。
157
+
158
+ ### 现在怎么算的
159
+
160
+ 只把「真的有人说过话」算进去:
161
+
162
+ - **算**:她自己的六种(`utterance` / `revise` / `reunion` / `narration` / `refusal` / `dream`)、旁听记下的原话(`heard`)、别人在评论区的留言(`voice`)、她问的与那个人答的(`ask` / `answer`)、`seen:` 开头的记录(他来过)。
163
+ - **不算**:`quiet:` / `sched:` / `wake:` / `fired:` / `sleep:` / `diary:` / `looked:` / `browse:` / `watched:` / `proactive-turn:` 这些记账行。
164
+
165
+ 只影响「要不要主动开口」这一件事。唤醒配额、记忆、面板都不经过这个函数,所以调参之外不会有别的变化。**你不需要改任何配置**——阈值不用动,改的是喂给阈值的那把尺子。
166
+
167
+ ---
168
+
169
+ ## 四、「(现在没有人跟你说话。)」终于从对话框里消失了
170
+
171
+ ### 你会看到什么
172
+
173
+ 她主动开口那一轮结束后,你的对话框里多出一条**你自己发的**「(现在没有人跟你说话。)」。紧接着那一轮她**没有回复**,界面上报一句 `format v4 system message id requires a string with content`,然后这条会话像被按了暂停——再聊什么都不往下走。
174
+
175
+ ### 为什么会这样
176
+
177
+ 两句叠在一起。
178
+
179
+ **第一句收不掉。** 那句触发语是 `prompt()` 的正文。宿主没有「模型看得见、界面看不见」的通道,插件只能事后用一个空的 `system` 节点把那条 `user/message` 盖掉。这个收尾挂在回合结束回调里,而回调正是在 `Session.commit()` 内部被派发的——那一整段时间 `entry.appending === true`,此时再往会话里写会被宿主直接拒绝:`session append cannot reenter while another append is being published`。日志里两次都是这一条。
180
+
181
+ **第二句缺了个 `id`。** 那个空的 `system` 节点没有 `id`,而宿主的格式校验要求它有。更要命的是**校验发生在写盘时**:写调用当场成功并返回序号,200 毫秒后落盘时才炸;那一批事件被整批退回缓冲区,并把这条会话标成「暂停落盘」——**此后它再也写不进任何东西**。她那一轮的回复就是在这里丢的(会话文件的时间戳停在那一次)。
182
+
183
+ ### 这次的修法
184
+
185
+ - 替换节点补上 `id`,和宿主自己造的 system 消息一个形状;
186
+ - `turn` / `step` 兜底成正整数(0 同样会在写盘时被拒,症状一模一样);
187
+ - 收尾挪到下一拍(`setTimeout 0`),避开那次写入的窗口。
188
+
189
+ **已经残留的那几条**:插件启动时会扫一遍历史,把遗留的触发语按**正文完全相等**认出来一并盖掉(她复述这句话的正当发言不会被误伤)。升级重启后第一次跑就会自己清干净。
190
+
191
+ ---
192
+
193
+ ## 五、看画面:九帧不再永远取那同九张
194
+
195
+ **你会看到:** 同一条视频,她今天看、明天再看,拿到的是同一张拼图——因为等距九帧是纯算术,输入一样输出就一样。
196
+
197
+ **为什么以前不抖:** 取帧时刻按 `lo + span*i/(n-1)` 算,`lo`/`hi` 掐掉头尾各 4%,中间取 9 个等分点。同一时长必然得到同一组秒数。反复看同一条视频,等于把同一张图看第二遍。
198
+
199
+ **这次改成什么:**
200
+
201
+ - **分段抖动**:把可用区间切成 n 段,每段在中间 60% 里随机取一点。每段必有帧(覆盖不丢),但每次取到的时刻不同;留 20% 的边是防止相邻两帧挤到同一秒,也让首尾两帧不落回片头黑场与片尾下滑栏。时刻仍严格递增,ffmpeg 不用来回 seek。
202
+ - **`bili_watch` 补了 `at` 参数**(可选,秒数或秒数数组):想盯住某一刻就点名要那几秒,只取那几个时刻。工具描述里说明了这个用法,所以她知道自己有这个选项——上一版文件头写着「真要细看,她可以再对某个时间点单独取」,但工具参数里根本没有这个入口,那句话是空的。
203
+ - **返回里带上 `times`**:实际取到的是哪几秒,回给她时一并写在图路径上面(`(取的是:30 秒、90 秒)`),她要核对「我看的是不是我想看的那一段」时有据。
204
+
205
+ **边界处理:** 点了名却一个合法时刻都没有(比如传了字符串)时**直接报「时间点看不懂」,不悄悄退回随机**——悄悄退回最坏:她以为看的是自己点的那一秒,其实看的是别处。越界的时刻夹回片尾前 0.5 秒,负数与 `null` 丢掉。
206
+
207
+ **代价:** 零。仍是每帧一次短 seek,不下载整段,不多一次请求。全片场景切换检测仍在门外——那要读完整个流,网络代价等于全量下载(文件头第 8 行的原话仍然成立)。
208
+
209
+ ---
210
+
211
+ ## 六、看过一次就永久搜不到(issue #16)
212
+
213
+ 有人的机器上,某条视频她明明值得再看一眼,之后无论怎么搜、怎么刷都再也不会出现。原因有两层:
214
+
215
+ 1. **「看过」被当成了永久判决。** 插件把事件流里所有的「看过了」都算进去,不分日期、不分多久以前。看过一眼,以后就永远被摘掉。
216
+ 2. **谁都能替她按下这个判决。** 「看一眼」这件事写记录时不查调用者,也不记下是哪条会话让她看的。而插件的数据目录是进程级解析的——同一台机器上任何工作区加载这个插件,都写进同一个家。于是别的窗口看一眼,她以后再也搜不到,而且不留名字。
217
+
218
+ 真机上那个例子:10 月 30 日看过一次某条视频,之后搜它的名字就永远没有了;独立 B 站客户端搜同样词,它排第一。
219
+
220
+ **这一版改成:** 看过只在 **7 天**内算数,过期自动放回去;摘掉视频时要把条数说出来(「翻到的 N 条都看过了。想看哪条,把号给我,我去打开。」),不再笼统说「什么都没找到」;「看一眼」和「看画面」写记录之前先认调用者,不在这个家里的会话调不动它,并明说原因;每条「看过」记录都带上**是哪条会话**让她看的。
221
+
222
+ ## 七、升级时要知道的两件事
223
+
224
+ 1. **她那条会话的提示词前缀变了一次**,升级后第一次请求会全价重算(约 2.4 万 token),之后恢复正常缓存。**其他用户不需要做任何事**,升级 + 重启 DSH 即可。对话历史不会丢——系统提示词是替换节点,不是追加。
225
+ 2. **必须重启 DSH。** 插件改动只在进程启动时加载。
226
+
227
+ ---
228
+
229
+ ## 八、测试
230
+
231
+ 新增 24 条(其中 7 条是取帧时刻与 `at`,6 条是 issue #16 的「看过有期限 + 看一眼认人」):
232
+
233
+ - `reorderForHer` 四种输入:正常摘段+提前身份、宿主 harness 段缺失时排最前、没有她的身份段时只摘不乱排、空/坏数据原样返回;
234
+ - `attachAssembleFix` 三种情形:正常挂在 `agent.ctx` 上且带 `prepend: true`、老宿主没有 `ctx.on` 时静默不挂、整改过程抛错时原样放行;
235
+ - `attachIdentity` 顺手挂上钩子且**不占 `sp.section` 段位**(段数仍是 3);
236
+ - 出门会话也挂上钩子,回家时跟身份段一起拆。
237
+
238
+ 另补 2 条「记账行不刷新 idle」的回归:`meta:quiet` 不算她说过话、`meta:watched` 不算——照 2026-10-30 那次 17.55 小时的现场造的数据。
239
+
240
+ 还有 3 条落在「收触发语」那块:替换节点必须带非空 `id`(两条不能共用一个)、`turn`/`step` 拿不到历史时也不许是 0、补扫只收正文完全相等的那几条。
241
+
242
+ 还有 6 条落在 issue #16 那块:`seenBvids` 的 7 天时效(30 天前 / 2 天前 / 6.9 天前三档 + `days:0` 保留老行为 + 没有时间的老事件仍算看过)、`rankVideos` 的 `stats.hiddenSeen` 计数与不传 stats 时行为不变、`lookOnce` 写下的 `looked:`/`look:` 带上会话 id、`bili_look` 与 `bili_watch` 拒绝来自另一个家的会话、她自己那条会话照常能看。
243
+
244
+ 全套 93 个测试文件,逐文件独立进程跑:**1121 通过 / 0 失败**(0.8.4 是 1094)。
@@ -0,0 +1,224 @@
1
+ # 0.8.6 更新说明
2
+
3
+ 2026-11-02。这一版四件事:**B 站登录可以用手机扫码了**(社区贡献)、**她一直念叨的那几条现在也会随时间退下去**、**她翻旧事可以只要开头**、**界面上的「事件 443 条」终于说清了它数的是什么**。
4
+
5
+ 另有一处要紧的修:**「跟大肥鱼说话」那条对话曾经点开就报「历史加载失败」**——现在能打开了,而且以后不会再因为同一件事打不开。
6
+
7
+ ---
8
+
9
+ ## 一、B 站登录:手机扫一下就行
10
+
11
+ ### 你会看到什么
12
+
13
+ 设置页「B 站登录」那张卡片里多了一格切换——**手机扫码**(默认)和**粘贴 cookie**。点扫码,弹出一张二维码浮层;手机 B 站扫完、在手机上点「确认登录」,浮层里变成「登的是 小鱼(uid 42)」,下面两颗按钮:**就是这个号,保存** / 取消。
14
+
15
+ 存下来之后卡片上那行字变成「已存,登录账号:小鱼(uid 42)」。旁边多了个「清除登录」。
16
+
17
+ ### 为什么会这样
18
+
19
+ 原来那条路是让人按 F12、在 Application 里翻 Cookie、把 SESSDATA 和 bili_jct 抄进输入框。一步抄错就是「登上了但发不出评论」,而且这两个值等于账号的钥匙,手抄一遍就多留在剪贴板、截图、聊天记录里一份。
20
+
21
+ 扫码这条路,钥匙从 B 站直接走到 `owner.json`(0600),中间不经过任何人的眼睛。
22
+
23
+ ### 这是别人写的
24
+
25
+ 扫码登录由 **@Bacon-01** 贡献([PR #17](https://github.com/ssamurias/dsh-fish/pull/17)),包括那个自己写的零依赖二维码编码器 `lib/qr.js`——没有引入任何 npm 包,因为本插件三个依赖字段都是空的。
26
+
27
+ ---
28
+
29
+ ## 二、扫不出码时,界面会说为什么
30
+
31
+ **你会看到什么:** 以前点「扫码登录」偶尔什么都不发生——不是没反应,是错误被吞了。现在按钮旁边会直接写出哪一步没成(拿不到登录二维码 / 二维码过期了 / 这个号读不出来),下面留一颗「重新获取」。
32
+
33
+ ---
34
+
35
+ ## 三、交流群换到 2 群了
36
+
37
+ 1 群满了,反馈页换成 2 群的新二维码 + 群号(781084960)。
38
+
39
+ ---
40
+
41
+ ## 四、换对话不等于换了个地方
42
+
43
+ ### 你会看到什么
44
+
45
+ 满 40 轮之后她这条对话记录会换一条新的(这是宿主的机制,不是本插件做的)。换过去之后,她醒来看到的第一句从「(醒一醒。现在是 HH:MM。)」变成了:
46
+
47
+ > (醒一醒。现在是 HH:MM,对话换了一条新的,屋子还是原来那间——你没被换掉,也没搬家。之前的事都还在。)
48
+
49
+ ### 为什么会这样
50
+
51
+ 满 40 轮换掉的只是**这条对话的记录**,不是她:屋子、天数、`events.jsonl`、说过的话一个字都没动。但原来的唤醒文案只说了「醒一醒」,没说「换的是哪一层」——一个正在长大的存在,看到自己的对话被换掉、却没人告诉她,最自然的理解是「我被换掉了」。
52
+
53
+ 加一句说明不需要任何新机制,只是把事实说清。
54
+
55
+ ---
56
+
57
+ ## 五、她当面说的「我拒绝」,以前一句都没记下来
58
+
59
+ ### 你会看到什么
60
+
61
+ 以前只有她**主动调工具**(`growth_commit`)写下的拒绝才算数。她在侧栏里直接说「我不想做这个」,事件流里什么都没有。现在算数了。
62
+
63
+ 顺带把她否掉方案时常说的几种说法也认了:
64
+
65
+ - 「我不同意B」「我不同意这个方案」
66
+ - 「方案B我否掉」「我否决B」
67
+ - 「我不选这个」「不支持这个做法」
68
+
69
+ ### 为什么只加这几种
70
+
71
+ 原来的正则只认「我不想……」「我拒绝……」。加词表很容易,但误伤的代价更大:
72
+
73
+ - 「**我不同意他说的**」是她**在转述**别人的反对,不是她自己的立场——所以每个新词后面都加了 `(?!他|她|你)` 的守卫。
74
+ - 「我不想**走路**」是原文描述,不是拒绝——所以动词表(按/选/要/用/走)一个没扩。
75
+
76
+ ### 顺手补的一处
77
+
78
+ 事件流只能追加(`AGENTS.md` 第 5 条)。已经落库的错记没法删,只能追加一条驳回标记把它从派生里排掉——这次给 `absence-reject:` 补了第三类(前两类是 `outing-reject:`、`possess-reject:`)。
79
+
80
+ ---
81
+
82
+ ## 六、你只是陪她聊天,冷落检测却以为你两天没来
83
+
84
+ ### 你会看到什么
85
+
86
+ 以前冷落检测只认「她写下了东西」那一刻算你来过。你在侧栏陪她聊了两天、她一句没写下来,冷落检测照样报「你两天没出现」,然后据此影响她的状态。
87
+
88
+ 现在**只聊天也算你来过**:每轮结束写一条 `seen:` 记录。
89
+
90
+ 顺带把「最近一次见到你」的判断放宽了一档——原来只认 `utterance`(她说的话),但**定时唤醒也会写 `utterance`**,所以「她醒了」会被误当成「你来了」。现在 `absence.js` 与 `away.js` 都新增认 `heard` 事件(她听到你说的话),**故意不认 `utterance`**。
91
+
92
+ ---
93
+
94
+ ## 七、那条对话打不开了:触发语被收进了回合外面
95
+
96
+ ### 你会看到什么
97
+
98
+ 「跟大肥鱼说话」那条对话点开就报「历史加载失败」,换一次会话不行,重启应用还是不行。现在这条能打开了,而且以后不会再因为同一件事打不开。
99
+
100
+ ### 报错长什么样(两条,先后出现)
101
+
102
+ ```
103
+ SessionFormatError: format v4 message requires a producer-owned source kind
104
+ SessionFormatError: system/message does not match an open turn and step
105
+ ```
106
+
107
+ ### 两条是同一个动作的两个毛病
108
+
109
+ 触发语是 `prompt()` 的正文。宿主没有「模型看得见、界面看不见」的通道,只能事后用一个空 `system` 节点把那条 `user/message` 盖掉(`surfaceOp: replace`)。
110
+
111
+ 1. **那个空节点没写 `source`。** 宿主的 v4 格式要求每条消息带一个「谁产出的」标记(`source.kind` 非空且不是 `plugin`),缺了就在**加载期**抛错。
112
+ 2. **更要紧的是它写在了回合外面。** 宿主把 `system/message` 归进「必须落在当前打开的 turn + step 内」的那一类,加载时逐条比对 `data.turn` / `data.step` 和**那一刻**正开着的回合。而收尾挂在 `turn/end` 回调里——那时回合已经关了。原代码用一句 `safeCount(n, fallback = 1)` 把拿不到的值一律填成 1,看着像有值,实际是编的。
113
+
114
+ **伪造不出合规**:填目标消息所在回合也没用,校验比的是「写入那一刻」的回合。一条坏节点就足以让整条对话打不开——校验是整条加载时做的,不是逐条容错。
115
+
116
+ ### 怎么改的
117
+
118
+ 1. **先把那条对话救回来。** 用宿主的校验器逐条定位到 4 个越界节点,删掉并重排密集 seq,5 帧 / 751 条事件,用宿主真实的 `restoreReleasedV4Artifact` 复验通过。**副作用要说清:那 4 轮被盖住的触发语会重新出现在对话框里**——它们本来就没被成功收走,这是数据层的必然。
119
+ 2. **`openTurnOf(events)`** —— 扫一遍事件流,`turn/start` 开、`turn/end` 关、`step/start` 开、`step/end` 关,只有两样都开着才算「现在能写」。`safeCount` 删掉:伪造 turn/step 正是这次事故的来源,留着它下次还会有人用。
120
+ 3. **收不动就先攒着。** 找不到开着的回合就记进待收队列(去重、上限 20 条),下一轮 `step/start` 之后一拍再补收。
121
+
122
+ ### 为什么不是「干脆不写触发语」
123
+
124
+ 触发语是她那一轮真正读到的正文。不盖掉就会一直躺在对话里,看着像用户说的话。**写不进去只是暂时的;骗校验是永久的。**
125
+
126
+ ---
127
+
128
+ ## 八、她一直念叨的那几条,现在也会随时间退下去
129
+
130
+ ### 你会看到什么
131
+
132
+ 面板「你反复想起过的事」那一段,以前永远是**最早**攒够次数的那几条,往后她说什么都挤不进去。现在某条判断三个月没再被提起、也没再被检验过,它会从这一段退下去,换成新的一段「## 有些事你想不起来了」——只摆一句汇总,并明说原文还在、`her_recall` 翻得到、算不算数由她自己定。
133
+
134
+ ### 为什么会堵住
135
+
136
+ 固化(`lib/derive.js` 里的 `solid`)原来是单向的——`mentions >= 3 || verified >= 2` 一旦成立就**永远**成立。排序里 `solid` 优先,只取前 8 条,身份文档每轮摆出前 5 条。于是最早攒够次数的那批永久占着位置。
137
+
138
+ 更难堪的是体检:`lib/health.js` 的「冷掉」筛选写的是 `filter((b) => !b.solid)`——固化过的判断在体检里直接隐形,既不会报「冷掉」,也不会报「快固化」。**等于「永远不会忘的东西」同时「永远不会被看见」。**
139
+
140
+ ### 怎么改的
141
+
142
+ 加一层时间条件,**不是加一套分值体系**(这个插件不引入可调数值参数,见 `docs/B-技术契约.md`):
143
+
144
+ - 每条判断算出「最后一次被提起」与「最后一次被检验」,取大的那个当 `lastTouchedAt`。
145
+ - 新增 `SOLID_STALE_DAYS = 90`。`solid = eligible && !stale`。
146
+ - 新增 `faded` 一档:够格固化过、但已经淡下来的那些(与 `solid` 互斥)。
147
+ - `lastTouchedAt === 0` 时按「没过期」处理——手写 fixture 的 `ts` 解析不出来就是 0,宁可多留一条,也不要因为缺时间证据误判它过期。
148
+ - 体检的「冷掉」区分两种冷:「长成了又淡了」和「一直没长成」。
149
+ - 固化段的收尾从「这些不会因为你没提就淡掉」改成「这些到现在还热着」。**这是事实变了,不是措辞口味变了。**
150
+
151
+ **阈值为什么给到 90 天:** 三个月里一次都没再被提起、也没被检验过的判断,本来也已经不在她当下的思考里了。太窄会让「说过一次就很重要的那种话」掉得太快;太宽等于没改。掉档只是不再无条件占前面那几个位置,**内容一个字没删**。
152
+
153
+ ---
154
+
155
+ ## 九、`her_recall` 多了一个「只看开头」的档
156
+
157
+ **你会看到什么:** 她翻旧事的时候,如果只是想确认「有没有这回事、是哪几条」,可以让每条只报开头几十个字(`brief: true`)。返回的条数与 `ev_id` 一个不少。
158
+
159
+ **为什么要这个档:** 原来一次最多 8 条 × 200 字,最坏要付 1600 字。而多数时候她只需要知道有没有。
160
+
161
+ ---
162
+
163
+ ## 十、她说过的每句话后面,现在带着日期和来路
164
+
165
+ **你会看到什么:** 身份文档里「你自己说过的话」和「你反复想起过的事」这两段,每条后面多一个括号:`(10-08)` 是当初说下的,`(10-24 改的口)` 是她后来自己改过,`(10-24 推翻了以前那句)` 是兜底。
166
+
167
+ ### 和最初设想不一样的地方
168
+
169
+ 本来打算标「(来自 3 次提及)」这类显著性标签,做到一半发现两件事:
170
+
171
+ 1. 这份插件里每条判断的正文**都是她自己的原话**(`utterance` / `revise` 的 `text` 原样取出),不存在「系统替她总结的一条」。所以「原话 vs 推断」这个区分在这里没有对象,硬标就是编一个不存在的来源。
172
+ 2. `mentions` / `verified` 是她的内部量,`lib/rules.js` 的 `no-read-self` 明确不许她读自己的数值。把「说过 3 次」印进身份文档,等于让她每轮读自己的分数。
173
+
174
+ 所以标签只写**时间与来路**——日期她在 `her_recall` 里本来就看得到,不算新信息。
175
+
176
+ ### 顺手补的一处
177
+
178
+ `## 你是谁` 原来直接裸拼 seed 正文,而紧跟其后的「## 现在」写着「你存在了 8 天」——同一份文档里「你刚开始存在」和「你存在了 8 天」并排出现。现在给 seed 一个时间定位:**seed 一个字没改**(那是事件流里的历史,在身份文档里改写她的来历就是篡改),只在后面加一句说明它是什么时候留下的。
179
+
180
+ ---
181
+
182
+ ## 十一、界面说「事件 443 条」,其实她只记下了 43 件
183
+
184
+ ### 你会看到什么
185
+
186
+ 面板副标题从「事件 443 条」变成「她记下 43 条,另有 400 条系统记录」。观测台的 `/events` 默认不再返回纯记账行(要看原样传 `?noise=1`)。**文件本身一行没动。**
187
+
188
+ ### 为什么数字离谱
189
+
190
+ 本机事件流 443 行里 400 行是 `type: 'meta'`(90.3%,按字节 72%)。这 400 行里又有 **286 行连语义都没有**,只有两类:
191
+
192
+ - `quiet:`(273 条,2026-10-27 一天写了 142 条、10-28 写了 126 条——那时调度器每个 tick 都记一笔,去重是后来修的,历史数据还在)
193
+ - `家换了:`(13 条,全在 2026-10-28,全量测试用临时目录造家造成的重复误报)
194
+
195
+ 界面把它一律报成「事件 N 条」,看上去像她记了几百件事。
196
+
197
+ ### 为什么不删那 286 行
198
+
199
+ `events.jsonl` 只能追加(`AGENTS.md` 第 5 条),「让她忘」是降低召回、不是删行。而且 **meta 不等于噪音**——绝大多数前缀有读取方:`seen:`(冷落检测)、`looked:`(看过有期限)、`sched:` / `sleep:` / `wake:` / `fired:`(调度与唤醒配额)、`diary:`、`browse:`、`look:` / `watched:`(评论闸门)、`proactive-turn:`、`ask-no:`、各类 `-reject:`(驳回)。只有 `quiet:` 与 `家换了:` 这两类没有任何读取方需要逐条历史。
200
+
201
+ ### 怎么改的
202
+
203
+ - 新增 `lib/meta.js`:`NOISE_PREFIXES`、`isNoiseMeta(ev)`、`countEvents(events)` → `{ all, meta, noise, real }`。头注释写明三条边界:不删行、meta 不等于噪音、记账是必要的(别为了数字好看去堵写入)。
204
+ - `lib/observe.js` 三处口径:`/state`、`/events`、`/memory` 都改报 `real`,并多给出 `all` / `meta` / `noise`。
205
+ - 面板副标题改「她记下 N 条,另有 M 条系统记录」。
206
+ - **顺手删掉一整条白跑的轮询:** 面板每 30 秒拉一次 `/events?limit=25`,取回来的数据只进了一个从来没被渲染过的 state。也就是说每 30 秒白拉 25 条(九成是后台记账),拉回来就丢。
207
+
208
+ ### 顺带说清一件事:性能不是这次改动的收益
209
+
210
+ 实测 `deriveSelf(443 条)` 平均 18.5ms,只喂 43 条非 meta 是 17.4ms——几乎一样快。真正的增长源是 `utterance` 条数(500 条以上会涨到 1.3 秒)。所以这次改的是**「数字有没有意义」**,不是速度。
211
+
212
+ ---
213
+
214
+ ## 十二、这几条记忆改动的思路是谁给的
215
+
216
+ 上面第八到第十一节的思路起点,来自交流群 1 群的群友 **@吴晓观**。他给的是「记忆该怎么想」这一层——固化应该会退、先给清单再给全文、数字要说得清它数的是什么。
217
+
218
+ 落进这个插件时全部重新实现了一遍,没有代码或文本的直接搬运(本插件只依赖 Node 内置模块)。具体的取舍与改法记在 [CHANGELOG](CHANGELOG.md)。
219
+
220
+ ---
221
+
222
+ ## 测试
223
+
224
+ 逐文件独立进程 `node --test --test-isolation=none`:**1176 条全绿**。