dsh-fish-memory 0.0.0-stage → 1.4.4
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/LICENSE +21 -0
- package/README.md +285 -2
- package/cordis.patch.yml +3 -0
- package/lib/atomic-file.mjs +32 -0
- package/lib/client.js +4043 -0
- package/lib/hostdeps.mjs +157 -0
- package/lib/index.js +6622 -0
- package/lib/selfupdate.mjs +151 -0
- package/lib/store-merge.mjs +31 -0
- package/lib//347/224/237/346/210/220/346/241/243/346/241/210.js +573 -0
- package/lib//350/257/215/345/205/270-/346/235/245/346/272/220.txt +53 -0
- package/lib//350/257/215/345/205/270.txt +109750 -0
- package/lib//350/257/215/351/242/221.txt +20000 -0
- package/package.json +56 -6
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Yuuyuko
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,286 @@
|
|
|
1
|
-
#
|
|
1
|
+
# 🐋 鱼的记忆
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
**给 DSH 里的会话一份会淡忘的长期记忆 —— 每条都带时间。**
|
|
4
|
+
|
|
5
|
+
聊得越久,上下文越挤。压缩一次,前面的事就淡了。
|
|
6
|
+
这个插件替它记着:**该记的记下来,想起来的时候自己会想起来,不重要的事慢慢淡掉。**
|
|
7
|
+
|
|
8
|
+
> 名字里的「鱼」来自 **DeepSeek 的鲸鱼** —— 鲸鱼也是鱼嘛。
|
|
9
|
+
> 另一半是「七秒记忆」那个梗:鱼的记性差,所以得替它记着。
|
|
10
|
+
> 但它不是硬盘 —— **它会淡忘**,跟人一样。
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## 它做什么
|
|
15
|
+
|
|
16
|
+
| | 说明 |
|
|
17
|
+
|---|---|
|
|
18
|
+
| **会淡忘** | 每条记忆有「鲜活度」,越久越淡。半衰期默认 30 天 —— 不删,只是越来越难被想起来 |
|
|
19
|
+
| **自己挑重点** | 不是每句话都记。它按轮次判断哪句值得留下,每天还有配额,防止把上下文塞满 |
|
|
20
|
+
| **想起来靠证据** | 你问什么,它按「命中的词够不够特别」找,再按鲜活度、重要度、情绪排序,最多注入几条。分词用的是一份通用中文词表,所以「车」不会撞进「车站」里 |
|
|
21
|
+
| **要记一辈子的不淡** | 画像、喜好、约定这种标成「长期事实」,平着放着,不会过期 |
|
|
22
|
+
| **长期事实每轮都在** | 它们按「重要度 → 时间」固定带前几条,**不看这句话问了什么**,也不占「最多注入几条」的位置 —— 剩下那几个位置才留给事件 |
|
|
23
|
+
| **带时间** | 每条都记着「这是什么时候的事」。跨年了会写「去年 09-17」 |
|
|
24
|
+
| **能导入旧档案** | 以前写过的 markdown / 纯文本档案,扫一遍出候选,你挑完它再改写成记忆 |
|
|
25
|
+
| **全都看得见** | 记了什么、哪条被想起来了、为什么没被想起来、删掉的都还在 |
|
|
26
|
+
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
## 需要什么
|
|
30
|
+
|
|
31
|
+
- **DSH**(DeepSeek Harness),带 Web 界面
|
|
32
|
+
- **一个能用的模型** —— 它借用你 DSH 里正在用的那个,**不用另外配 key、不额外花钱**
|
|
33
|
+
|
|
34
|
+
> 「提炼」和「扩词」这两步会调用模型(走你现有的)。不点它们就不会调用。
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
## 适配哪个 DSH 版本(不用操心)
|
|
39
|
+
|
|
40
|
+
这个插件**跟着 DSH 一起更新**:作者这边跑的就是最新版的 DSH,**最新版插件永远是给最新版 DSH 写的**。
|
|
41
|
+
|
|
42
|
+
所以看到「不适配 / 不支持」之类的提示,基本都是**装到了旧版本** —— 升级到最新版、重启一次 DSH 就好。
|
|
43
|
+
真遇到新版对不上(比如 DSH 刚升级、插件还没跟上),提个 issue,作者会跟着改。
|
|
44
|
+
|
|
45
|
+
当前实测对照:插件 **1.3.1** ↔ DSH **0.2.1-rc.3**(2026-10-26)
|
|
46
|
+
|
|
47
|
+
## 装法
|
|
48
|
+
|
|
49
|
+
在 DSH 里:**设置 → 插件 → 安装**,填下面这一行;或者命令行:
|
|
50
|
+
|
|
51
|
+
```
|
|
52
|
+
dsh-fish-memory
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
```sh
|
|
56
|
+
dsh plugin --profile web add dsh-fish-memory
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
> 也可以在 npm 上直接看到这个包(含历史版本):<https://www.npmjs.com/package/dsh-fish-memory>
|
|
60
|
+
|
|
61
|
+
> 上面那条要是碰到网络问题(npm 抽风、或者想固定某一次的代码快照),可以改用 tarball 直链,不用 git:
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
dsh plugin --profile web add https://codeload.github.com/Yuuyuko-uu/dsh-fish-memory/tar.gz/refs/heads/main
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
(`--profile` 照你平时那个写:网页端一般 `web`,桌面端一般 `desktop`。)
|
|
69
|
+
|
|
70
|
+
装完**重启一次 DSH**。
|
|
71
|
+
|
|
72
|
+
重启后,打开 **设置 → 鱼的记忆**。
|
|
73
|
+
|
|
74
|
+
> 也可以直接装 npm 包:`dsh plugin add dsh-fish-memory`。
|
|
75
|
+
> ⭐ 最省事的一条:把仓库链接贴给你自己的 DSH,说「**帮我装这个插件**」。
|
|
76
|
+
|
|
77
|
+
### 装完起不来?先看这个
|
|
78
|
+
|
|
79
|
+
如果 DSH 弹「The application could not start」,错误里有 `Unexpected token ''`、`is not valid JSON`、
|
|
80
|
+
`readProfileManifest` —— 那是 profile 的 `package.json` 被存成了 **UTF-8 带 BOM**。
|
|
81
|
+
**跟插件无关**:内容都是对的,只是开头多了三个看不见的字节,DSH 读它就崩。
|
|
82
|
+
|
|
83
|
+
**先找出来**(不同装法位置不一样,别猜 —— 真正的 profile 目录里同时有 `cordis.yml`、`cordis.patch.yml`、`package.json`;机器上别处那些同名的都不算):
|
|
84
|
+
|
|
85
|
+
```powershell
|
|
86
|
+
Get-ChildItem C:\,D:\,E:\ -Directory -Filter profiles -Recurse -Depth 4 -ErrorAction SilentlyContinue |
|
|
87
|
+
ForEach-Object {
|
|
88
|
+
Get-ChildItem $_.FullName -Directory -ErrorAction SilentlyContinue | ForEach-Object {
|
|
89
|
+
$p = Join-Path $_.FullName 'package.json'
|
|
90
|
+
if ((Test-Path (Join-Path $_.FullName 'cordis.patch.yml')) -and (Test-Path $p)) {
|
|
91
|
+
$b = [System.IO.File]::ReadAllBytes($p)
|
|
92
|
+
$bom = ($b.Length -ge 3 -and $b[0] -eq 0xEF -and $b[1] -eq 0xBB -and $b[2] -eq 0xBF)
|
|
93
|
+
"{0} BOM={1}" -f $p, $bom
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
**输出里 `BOM=True` 的那一行,就是要修的文件**(标准安装在 `C:\Users\<你>\.dsh\profiles\<档名>\package.json`)。
|
|
100
|
+
去掉 BOM(路径换成上面那一行):
|
|
101
|
+
|
|
102
|
+
```powershell
|
|
103
|
+
$p = "把上面 BOM=True 那一行的路径粘到这里"
|
|
104
|
+
[System.IO.File]::WriteAllText($p, [System.IO.File]::ReadAllText($p), (New-Object System.Text.UTF8Encoding($false)))
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
然后重启 DSH 就好,**数据一点没丢**。实在进不去时,那个错误框里的
|
|
108
|
+
**「Disable third-party plugins, back up profile patch, and restart」** 能先起来,再一个个开回去定位。
|
|
109
|
+
|
|
110
|
+
> ⚠️ 手动改 profile 里的文件,一律存 **UTF-8 无 BOM**。让 AI 帮你改时,别让它用
|
|
111
|
+
> `Set-Content -Encoding UTF8` 或 `Out-File -Encoding utf8`(Windows 自带的这两个默认**带 BOM**)。
|
|
112
|
+
|
|
113
|
+
---
|
|
114
|
+
|
|
115
|
+
## 更新到最新版
|
|
116
|
+
|
|
117
|
+
**最省事**:把这个仓库的地址丢给你自己的 DSH,说「**帮我更新这个插件**」。装完**重启一次 DSH**。
|
|
118
|
+
|
|
119
|
+
也可以点 **设置 → 鱼的记忆 最上面的「检查更新」** —— 有新版会提示,还能一键复制更新命令。
|
|
120
|
+
|
|
121
|
+
命令行(`--profile` 照你平时那个写,桌面端一般 `desktop`、网页端 `web`):
|
|
122
|
+
|
|
123
|
+
```bash
|
|
124
|
+
dsh plugin --profile desktop remove dsh-fish-memory
|
|
125
|
+
dsh plugin --profile desktop add dsh-fish-memory
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
> ⚠️ **必须先卸再装**:直接装可能拿到缓存里的旧副本,装完还是老的。
|
|
129
|
+
> ⚠️ 装完**重启一次 DSH**才会用新代码。
|
|
130
|
+
|
|
131
|
+
### 我的设置会不会丢?
|
|
132
|
+
|
|
133
|
+
**不会。** 设置存在 DSH 家目录(`%USERPROFILE%\.dsh\`)里,不在插件代码里;卸载只删代码:
|
|
134
|
+
|
|
135
|
+
```
|
|
136
|
+
.dsh\fish-memory-config.json (每条会话的记忆各一份)
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
所以卸载 → 重装,设置、记下来的所有记忆 都还在。
|
|
140
|
+
|
|
141
|
+
三件小事提醒:
|
|
142
|
+
|
|
143
|
+
- **别用「重装 DSH」来更新插件** —— 那是两件事,重装可能动到 profile。
|
|
144
|
+
- **新版本新加的设置项**会用默认值(以前没这一项),想要自己的值就再填一次。
|
|
145
|
+
- 设置项改名或换单位时,旧值会按规则自动换算,不用你管。
|
|
146
|
+
|
|
147
|
+
## 怎么用
|
|
148
|
+
|
|
149
|
+
### 第一步:直接聊就行
|
|
150
|
+
|
|
151
|
+
**装好就是开着的**,哪个会话都能记、也能读,不用先点开关。
|
|
152
|
+
|
|
153
|
+
哪个会话不想用记忆(比如写代码那间),到「鱼的记忆」设置里把它单独关掉;
|
|
154
|
+
整个功能都不想要,关最上面那个「remember 工具」。
|
|
155
|
+
|
|
156
|
+
### 第二步:正常聊
|
|
157
|
+
|
|
158
|
+
开着之后,它自己会判断该记什么。你不用管。
|
|
159
|
+
|
|
160
|
+
**记的时候有两条线**:
|
|
161
|
+
|
|
162
|
+
- 一天最多记几条(默认 8 条)—— 防止它把鸡毛蒜皮都记下来
|
|
163
|
+
- 一次最多注入几条(默认 6 条)—— 防止占太多上下文
|
|
164
|
+
|
|
165
|
+
### 第三步:想知道它记了什么
|
|
166
|
+
|
|
167
|
+
展开「**记忆列表**」,能看到每一条:
|
|
168
|
+
|
|
169
|
+
- 最左边是**日期**(能按日期排、也能只看有日期的)
|
|
170
|
+
- 中间是内容
|
|
171
|
+
- 右边是鲜活度、重要度
|
|
172
|
+
|
|
173
|
+
还有「**最近一次召回**」—— 能看到这次问话它挑了哪几条、哪几条没挑上、为什么没挑上。
|
|
174
|
+
|
|
175
|
+
---
|
|
176
|
+
|
|
177
|
+
## 如果你以前写过档案
|
|
178
|
+
|
|
179
|
+
很多人早就给自己的 AI 写过设定、记忆卡、日记。**不用重写**,导进来就行。
|
|
180
|
+
|
|
181
|
+
1. 设置页展开「**从旧档案导入**」
|
|
182
|
+
2. 选文件夹(或填路径)→ 点「**扫描档案**」
|
|
183
|
+
3. 它把档案拆成一条条候选,**列给你看**
|
|
184
|
+
4. 你勾掉不要的 → 点「**导入选中的**」
|
|
185
|
+
5. 再点「**开始提炼**」—— 它把这些原文改写成第一人称的记忆,顺手给每条出检索词、日期、重要度
|
|
186
|
+
|
|
187
|
+
> **提炼会调用模型**(走你 DSH 里的那个),一批几条、慢慢跑,可以随时停。
|
|
188
|
+
> 它会**从较新的内容往前做**,已经提炼过的不再重复。
|
|
189
|
+
|
|
190
|
+
**认得的格式**:`.md` `.markdown` `.txt` `.text` `.log` `.rst` `.org`
|
|
191
|
+
**子文件夹**会往下找两层。
|
|
192
|
+
|
|
193
|
+
---
|
|
194
|
+
|
|
195
|
+
## 参数(都能调)
|
|
196
|
+
|
|
197
|
+
设置页里有一组旋钮,**每一项都有说明**,鼠标放上去能看到它在干什么。大致分三组:
|
|
198
|
+
|
|
199
|
+
| 组 | 管什么 |
|
|
200
|
+
|---|---|
|
|
201
|
+
| **权重** | 鲜活度 / 重要度 / 情绪各占多少分(四项合计 100%) |
|
|
202
|
+
| **时间** | 多久淡一半 |
|
|
203
|
+
| **门槛** | 命中多少才算相关、一次最多注入几条、每天最多记几条 |
|
|
204
|
+
|
|
205
|
+
**一个建议**:`证据门槛` 卡的是「证据」不是「鲜活度」,所以**调它不会误伤该想起来的**。
|
|
206
|
+
想让它更少说话就调高,想让它更容易想起来就调低。
|
|
207
|
+
|
|
208
|
+
---
|
|
209
|
+
|
|
210
|
+
## 人称
|
|
211
|
+
|
|
212
|
+
记忆里你自己是「我」,用户是「**她 / 他 / ta**」—— **默认 ta**,可以在设置页改。
|
|
213
|
+
|
|
214
|
+
改完之后:
|
|
215
|
+
|
|
216
|
+
- 以后新记的会按你设的写
|
|
217
|
+
- 已有的记忆里那个字也会跟着换
|
|
218
|
+
- 提炼旧档案时也按你设的来
|
|
219
|
+
|
|
220
|
+
**说名字本身的句子不会被换掉**(「我的名字叫某某」不会变成「我的名字叫我」)。
|
|
221
|
+
|
|
222
|
+
---
|
|
223
|
+
|
|
224
|
+
## 事实键(同一条事只留最新)
|
|
225
|
+
|
|
226
|
+
有些记忆记的是**会变的东西**,光看句子像不像判不出来:
|
|
227
|
+
|
|
228
|
+
- 「身高一米七」和「长胖到 55 公斤」几乎是两件事,词面上却很像
|
|
229
|
+
- 「拼团还没开」和「拼团开了」是同一件事的前后,词面上却不像
|
|
230
|
+
|
|
231
|
+
所以每条记忆可以带一个**事实键**,写「对象.方面」,像 `她.身高`、`约定.身边位置`。
|
|
232
|
+
**同一个键 = 同一件事** → 只留最新那条,旧的标成「已被取代」(不再被想起来,但东西留着、能撤销)。
|
|
233
|
+
|
|
234
|
+
键从三个地方来,都是顺手给的,不多花一次调用:
|
|
235
|
+
|
|
236
|
+
- 模型记的时候自己填(`remember` 的可选参数)
|
|
237
|
+
- 提炼旧档案时顺手出(第六格)
|
|
238
|
+
- 设置页的「**清洗补键**」按钮,给以前那些还没有键的旧条目补上
|
|
239
|
+
|
|
240
|
+
⚠ 键里**不许写具体的值** —— 写 `她.身高`,不写 `她.身高170`。
|
|
241
|
+
写了值就成了摘要:身高真变了,键也跟着变,两条永远碰不上。
|
|
242
|
+
**拿不准就留空**,留空完全没关系(留空只是享受不到这个便利,给错键却会埋掉一条正经记忆)。
|
|
243
|
+
|
|
244
|
+
清洗补键那一步是**后台活**:先「试 8 条看看」(一个字节都不写,只把键摆出来给你看),
|
|
245
|
+
确认了再真洗。跑完能一键「把补的键全摘掉」回到洗之前 —— 只摘自动补的,你自己填的键不动。
|
|
246
|
+
|
|
247
|
+
---
|
|
248
|
+
|
|
249
|
+
## 它不做什么
|
|
250
|
+
|
|
251
|
+
- **不会自动删除记忆。** 淡忘只是越来越难想起来,内容一直在,设置页里看得到
|
|
252
|
+
- **不会把删掉的东西弄回来。** 删过的再导一次档案也不会复活
|
|
253
|
+
- **不会每个会话都强制用。** 默认开着,不想用的那间单独关掉;也能一键全关
|
|
254
|
+
- **不会替你写档案。** 它只把档案拆开、改写、存起来,写什么还是你的事
|
|
255
|
+
|
|
256
|
+
---
|
|
257
|
+
|
|
258
|
+
## 常见问题
|
|
259
|
+
|
|
260
|
+
**Q:会不会很占上下文?**
|
|
261
|
+
一次最多注入 6 条、加起来不超过 800 字(都能调)。超了的这次先进不来,下次可能就进了。
|
|
262
|
+
|
|
263
|
+
**Q:为什么它有时候想不起来?**
|
|
264
|
+
先看「最近一次召回」那张表 —— 它会写清楚哪条没进去、为什么。
|
|
265
|
+
常见原因:命中的词太普通(证据不够)、或者那条太旧淡下去了。
|
|
266
|
+
|
|
267
|
+
**Q:导入扫出来 0 条?**
|
|
268
|
+
它会告诉你为什么。常见的是:整篇是一大段话没分行、每行太短、或者内容都在表格/代码块里。
|
|
269
|
+
|
|
270
|
+
**Q:数据存在哪?**
|
|
271
|
+
`%USERPROFILE%\.dsh\fish-memory\` 里,一个会话一个文件。**纯文本 JSON**,你自己能打开看。
|
|
272
|
+
每次改动前它会自动备份一份。
|
|
273
|
+
|
|
274
|
+
**Q:装完包大了将近 900 KB?**
|
|
275
|
+
那里面是一份**通用中文词表**,用来分词。它只在本地当查找表用 ——
|
|
276
|
+
**不占上下文、不影响每轮对话的 token**,也不会因为你聊得多就变大。
|
|
277
|
+
|
|
278
|
+
---
|
|
279
|
+
|
|
280
|
+
## 许可
|
|
281
|
+
|
|
282
|
+
MIT
|
|
283
|
+
|
|
284
|
+
分词用的词表来自 [jieba](https://github.com/fxsjy/jieba) 的 `extra_dict/dict.txt.small`
|
|
285
|
+
(MIT 协议),只保留了词条本身、去掉了词频和词性两列。
|
|
286
|
+
完整声明见 `lib/词典-来源.txt`。
|
package/cordis.patch.yml
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import fs from 'node:fs'
|
|
2
|
+
import { randomUUID } from 'node:crypto'
|
|
3
|
+
|
|
4
|
+
// Same-directory staging keeps a failed or interrupted write away from the live
|
|
5
|
+
// JSON file. This is one-file replacement, not a multi-process transaction.
|
|
6
|
+
export function writeFileAtomicSync(target, text) {
|
|
7
|
+
const staged = target + '.' + randomUUID() + '.tmp'
|
|
8
|
+
let mode = 0o600
|
|
9
|
+
try { mode = fs.statSync(target).mode & 0o777 }
|
|
10
|
+
catch (error) { if (error.code !== 'ENOENT') throw error }
|
|
11
|
+
let descriptor
|
|
12
|
+
let created = false
|
|
13
|
+
let committed = false
|
|
14
|
+
try {
|
|
15
|
+
descriptor = fs.openSync(staged, 'wx', mode)
|
|
16
|
+
created = true
|
|
17
|
+
if (process.platform !== 'win32') fs.fchmodSync(descriptor, mode)
|
|
18
|
+
fs.writeFileSync(descriptor, text, 'utf8')
|
|
19
|
+
fs.fsyncSync(descriptor)
|
|
20
|
+
fs.closeSync(descriptor)
|
|
21
|
+
descriptor = undefined
|
|
22
|
+
fs.renameSync(staged, target)
|
|
23
|
+
committed = true
|
|
24
|
+
} finally {
|
|
25
|
+
if (descriptor !== undefined) {
|
|
26
|
+
try { fs.closeSync(descriptor) } catch { /* preserve the original failure */ }
|
|
27
|
+
}
|
|
28
|
+
if (created && !committed) {
|
|
29
|
+
try { fs.rmSync(staged, { force: true }) } catch { /* caller still receives the write failure */ }
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|